Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Encrypt attachments in encrypted rooms, #533

Merged
merged 19 commits into from
Nov 9, 2016
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"dependencies": {
"babel-runtime": "^6.11.6",
"browser-encrypt-attachment": "0.0.0",
"browser-request": "^0.3.3",
"classnames": "^2.1.2",
"draft-js": "^0.8.1",
Expand All @@ -54,6 +55,7 @@
"fuse.js": "^2.2.0",
"glob": "^5.0.14",
"highlight.js": "^8.9.1",
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"marked": "^0.3.5",
Expand Down
52 changes: 50 additions & 2 deletions src/ContentMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ var MatrixClientPeg = require('./MatrixClientPeg');
var sdk = require('./index');
var Modal = require('./Modal');

var encrypt = require("browser-encrypt-attachment");

function infoForImageFile(imageFile) {
var deferred = q.defer();

Expand Down Expand Up @@ -81,6 +83,24 @@ function infoForVideoFile(videoFile) {
return deferred.promise;
}

/**
* Read the file as an ArrayBuffer.
* @return {Promise} A promise that resolves with an ArrayBuffer when the file
* is read.
*/
function readFileAsArrayBuffer(file) {
var deferred = q.defer();
var reader = new FileReader();
reader.onload = function(e) {
deferred.resolve(e.target.result);
};
reader.onerror = function(e) {
deferred.reject(e);
};
reader.readAsArrayBuffer(file);
return deferred.promise;
}


class ContentMessages {
constructor() {
Expand Down Expand Up @@ -137,10 +157,26 @@ class ContentMessages {
this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'});

var encryptInfo = null;
var error;
var self = this;
return def.promise.then(function() {
upload.promise = matrixClient.uploadContent(file);
if (matrixClient.isRoomEncrypted(roomId)) {
// If the room is encrypted then encrypt the file before uploading it.
// First read the file into memory.
upload.promise = readFileAsArrayBuffer(file).then(function(data) {
// Then encrypt the file.
return encrypt.encryptAttachment(data);
}).then(function(encryptResult) {
// Record the information needed to decrypt the attachment.
encryptInfo = encryptResult.info;
// Pass the encrypted data as a Blob to the uploader.
var blob = new Blob([encryptResult.data]);
return matrixClient.uploadContent(blob);
});
} else {
upload.promise = matrixClient.uploadContent(file);
}
return upload.promise;
}).progress(function(ev) {
if (ev) {
Expand All @@ -149,7 +185,19 @@ class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
}).then(function(url) {
content.url = url;
if (encryptInfo === null) {
// If the attachment isn't encrypted then include the URL directly.
content.url = url;
} else {
// If the attachment is encrypted then bundle the URL along
// with the information needed to decrypt the attachment and
// add it under a file key.
encryptInfo.url = url;
if (file.type) {
encryptInfo.mimetype = file.type;
}
content.file = encryptInfo;
}
return matrixClient.sendMessage(roomId, content);
}, function(err) {
error = err;
Expand Down
48 changes: 43 additions & 5 deletions src/components/views/messages/MAudioBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,67 @@ import MFileBody from './MFileBody';

import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile';

export default class MAudioBody extends React.Component {
constructor(props) {
super(props);
this.state = {
playing: false
playing: false,
decryptedUrl: null,
}
}

onPlayToggle() {
this.setState({
playing: !this.state.playing
});
}

_getContentUrl() {
var content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
}

componentDidMount() {
var content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
decryptFile(content.file).then((url) => {
this.setState({
decryptedUrl: url
});
}).catch((err) => {
console.warn("Unable to decrypt attachment: ", err)
// Set a placeholder image when we can't decrypt the image.
this.refs.image.src = "img/warning.svg";
});
}
}

render() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();

if (content.file !== undefined && this.state.decryptedUrl === null) {
// Need to decrypt the attachment
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a spinner.
return (
<span className="mx_MAudioBody">
<img src="img/spinner.gif" ref="image"
alt={content.body} />
</span>
);
}

var contentUrl = this._getContentUrl();

return (
<span className="mx_MAudioBody">
<audio src={cli.mxcUrlToHttp(content.url)} controls />
<MFileBody {...this.props} />
<audio src={contentUrl} controls />
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
</span>
);
}
Expand Down
88 changes: 81 additions & 7 deletions src/components/views/messages/MFileBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,18 @@ var React = require('react');
var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
var DecryptFile = require('../../../utils/DecryptFile');


module.exports = React.createClass({
displayName: 'MFileBody',

getInitialState: function() {
return {
decryptedUrl: (this.props.decryptedUrl ? this.props.decryptedUrl : null),
};
},

presentableTextForFile: function(content) {
var linkText = 'Attachment';
if (content.body && content.body.length > 0) {
Expand All @@ -47,22 +54,89 @@ module.exports = React.createClass({
return linkText;
},

_getContentUrl: function() {
var content = this.props.mxEvent.getContent();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const

if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
},

componentDidMount: function() {
var content = this.props.mxEvent.getContent();
var self = this;
if (content.file !== undefined && this.state.decryptedUrl === null) {
DecryptFile.decryptFile(content.file).then(function(url) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/function(x)/(x) => / then you can ditch the var self = this and just use this.

self.setState({
decryptedUrl: url,
});
}).catch(function (err) {
console.warn("Unable to decrypt attachment: ", err)
// Set a placeholder image when we can't decrypt the image.
self.refs.image.src = "img/warning.svg";
});
}
},

render: function() {
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();

var httpUrl = cli.mxcUrlToHttp(content.url);
var text = this.presentableTextForFile(content);

var TintableSvg = sdk.getComponent("elements.TintableSvg");
if (content.file !== undefined && this.state.decryptedUrl === null) {

// Need to decrypt the attachment
// The attachment is decrypted in componentDidMount.
// For now add an img tag with a spinner.
return (
<span className="mx_MFileBody" ref="body">
<img src="img/spinner.gif" ref="image"
alt={content.body} />
</span>
);
}

var contentUrl = this._getContentUrl();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more consts please


var fileName = content.body && content.body.length > 0 ? content.body : "Attachment";

var downloadAttr = undefined;
if (this.state.decryptedUrl) {
// If the file is encrypted then we MUST download it rather than displaying it
// because Firefox is vunerable to XSS attacks in data:// URLs
// and all browsers are vunerable to XSS attacks in blob: URLs
// created with window.URL.createObjectURL
// See https://bugzilla.mozilla.org/show_bug.cgi?id=255107
// See https://w3c.github.io/FileAPI/#originOfBlobURL
//
// This is not a problem for unencrypted links because they are
// either fetched from a different domain so are safe because of
// the same-origin policy or they are fetch from the same domain,
// in which case we trust that the homeserver will set a
// Content-Security-Policy that disables script execution.
// It is reasonable to trust the homeserver in that case since
// it is the same domain that controls this javascript.
//
// We can't apply the same workaround for encrypted files because
// we can't supply HTTP headers when the user clicks on a blob:
// or data:// uri.
//
// We should probably provide a download attribute anyway so that
// the file will have the correct name when the user tries to
// download it. We can't provide a Content-Disposition header
// like we would for HTTP.
downloadAttr = fileName;
}

if (httpUrl) {
if (contentUrl) {
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a className="mx_ImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{ content.body && content.body.length > 0 ? content.body : "Attachment" }
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
{ fileName }
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
Expand All @@ -75,7 +149,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {text}
</a>
Expand Down
Loading