From e0cea74c7ec9dc7891be1ab604e4f9904cbb2c40 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Wed, 2 Nov 2016 16:26:10 +0000 Subject: [PATCH] Encrypt attachments in encrypted rooms, decrypt image attachments when displaying them --- package.json | 1 + src/ContentMessages.js | 49 ++++++++++++++++++++- src/components/views/messages/MImageBody.js | 44 +++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 498514a2b0..3bcccd668e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/ContentMessages.js b/src/ContentMessages.js index fd18b22d30..a3f6d548c3 100644 --- a/src/ContentMessages.js +++ b/src/ContentMessages.js @@ -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(); @@ -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() { @@ -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(room_id)) { + // 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) { @@ -149,7 +185,16 @@ 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; + content.file = encryptInfo; + } return matrixClient.sendMessage(roomId, content); }, function(err) { error = err; diff --git a/src/components/views/messages/MImageBody.js b/src/components/views/messages/MImageBody.js index 526fc6a3a5..087a337bd2 100644 --- a/src/components/views/messages/MImageBody.js +++ b/src/components/views/messages/MImageBody.js @@ -19,12 +19,18 @@ limitations under the License. var React = require('react'); var filesize = require('filesize'); +// Pull in the encryption lib so that we can decrypt attachments. +var encrypt = require("browser-encrypt-attachment"); +// Pull in a fetch polyfill so we can download encrypted attachments. +require("isomorphic-fetch"); + var MatrixClientPeg = require('../../../MatrixClientPeg'); var ImageUtils = require('../../../ImageUtils'); var Modal = require('../../../Modal'); var sdk = require('../../../index'); var dis = require("../../../dispatcher"); + module.exports = React.createClass({ displayName: 'MImageBody', @@ -85,6 +91,33 @@ module.exports = React.createClass({ componentDidMount: function() { this.dispatcherRef = dis.register(this.onAction); this.fixupHeight(); + var content = this.props.mxEvent.getContent(); + if (content.file !== undefined) { + // TODO: hook up an error handler to the promise. + this.decryptFile(content.file); + } + }, + + decryptFile: function(file) { + var url = MatrixClientPeg.get().mxcUrlToHttp(file.url); + var self = this; + // Download the encrypted file as an array buffer. + return fetch(url).then(function (response) { + return response.arrayBuffer(); + }).then(function (responseData) { + // Decrypt the array buffer using the information taken from + // the event content. + return encrypt.decryptAttachment(responseData, file); + }).then(function(dataArray) { + // Turn the array into a Blob and use createObjectURL to make + // a url that we can use as an img src. + var blob = new Blob([dataArray]); + var blobUrl = window.URL.createObjectURL(blob); + self.refs.image.src = blobUrl; + self.refs.image.onload = function() { + window.URL.revokeObjectURL(blobUrl); + }; + }); }, componentWillUnmount: function() { @@ -148,7 +181,16 @@ module.exports = React.createClass({ } var thumbUrl = this._getThumbUrl(); - if (thumbUrl) { + if (content.file !== undefined) { + // Need to decrypt the attachment + // The attachment is decrypted in componentDidMount. + return ( + + {content.body} + + ); + } else if (thumbUrl) { return (