Merge pull request #546 from matrix-org/revert-533-markjh/encrypted-attachments

Revert "Encrypt attachments in encrypted rooms,"
This commit is contained in:
Mark Haines 2016-11-11 12:00:43 +00:00 committed by GitHub
commit f6b3248c9d
7 changed files with 108 additions and 398 deletions

View file

@ -42,7 +42,6 @@
},
"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",
@ -55,7 +54,6 @@
"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",

View file

@ -23,8 +23,6 @@ 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();
@ -83,24 +81,6 @@ 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) {
const deferred = q.defer();
const 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() {
@ -157,26 +137,10 @@ class ContentMessages {
this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'});
var encryptInfo = null;
var error;
var self = this;
return def.promise.then(function() {
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) {
@ -185,19 +149,7 @@ class ContentMessages {
dis.dispatch({action: 'upload_progress', upload: upload});
}
}).then(function(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;

View file

@ -21,67 +21,29 @@ 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,
decryptedUrl: null,
playing: false
}
}
onPlayToggle() {
this.setState({
playing: !this.state.playing
});
}
_getContentUrl() {
const 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).done((url) => {
this.setState({
decryptedUrl: url
});
}, (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() {
const content = this.props.mxEvent.getContent();
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>
);
}
const contentUrl = this._getContentUrl();
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
return (
<span className="mx_MAudioBody">
<audio src={contentUrl} controls />
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
<audio src={cli.mxcUrlToHttp(content.url)} controls />
<MFileBody {...this.props} />
</span>
);
}

View file

@ -16,22 +16,15 @@ limitations under the License.
'use strict';
import React from 'react';
import filesize from 'filesize';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import {decryptFile} from '../../../utils/DecryptFile';
var React = require('react');
var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var dis = require("../../../dispatcher");
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) {
@ -54,88 +47,22 @@ module.exports = React.createClass({
return linkText;
},
_getContentUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
},
componentDidMount: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
decryptFile(content.file).done((url) => {
this.setState({
decryptedUrl: url,
});
}, (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: function() {
const content = this.props.mxEvent.getContent();
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
const text = this.presentableTextForFile(content);
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>
);
}
const contentUrl = this._getContentUrl();
const 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 (contentUrl) {
if (httpUrl) {
if (this.props.tileShape === "file_grid") {
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a className="mx_ImageBody_downloadLink" href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
{ fileName }
<a className="mx_ImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{ content.body && content.body.length > 0 ? content.body : "Attachment" }
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
@ -148,7 +75,7 @@ module.exports = React.createClass({
return (
<span className="mx_MFileBody">
<div className="mx_MImageBody_download">
<a href={contentUrl} target="_blank" rel="noopener" download={downloadAttr}>
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {text}
</a>

View file

@ -16,15 +16,14 @@ limitations under the License.
'use strict';
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import ImageUtils from '../../../ImageUtils';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import {decryptFile} from '../../../utils/DecryptFile';
var React = require('react');
var filesize = require('filesize');
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',
@ -34,20 +33,13 @@ module.exports = React.createClass({
mxEvent: React.PropTypes.object.isRequired,
},
getInitialState: function() {
return {
decryptedUrl: null,
};
},
onClick: function onClick(ev) {
if (ev.button == 0 && !ev.metaKey) {
ev.preventDefault();
const content = this.props.mxEvent.getContent();
const httpUrl = this._getContentUrl();
const ImageView = sdk.getComponent("elements.ImageView");
const params = {
var content = this.props.mxEvent.getContent();
var httpUrl = MatrixClientPeg.get().mxcUrlToHttp(content.url);
var ImageView = sdk.getComponent("elements.ImageView");
var params = {
src: httpUrl,
mxEvent: this.props.mxEvent
};
@ -63,7 +55,7 @@ module.exports = React.createClass({
},
_isGif: function() {
const content = this.props.mxEvent.getContent();
var content = this.props.mxEvent.getContent();
return (content && content.info && content.info.mimetype === "image/gif");
},
@ -72,7 +64,9 @@ module.exports = React.createClass({
return;
}
var imgElement = e.target;
imgElement.src = this._getContentUrl();
imgElement.src = MatrixClientPeg.get().mxcUrlToHttp(
this.props.mxEvent.getContent().url
);
},
onImageLeave: function(e) {
@ -83,40 +77,14 @@ module.exports = React.createClass({
imgElement.src = this._getThumbUrl();
},
_getContentUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
},
_getThumbUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
// TODO: Decrypt and use the thumbnail file if one is present.
return this.state.decryptedUrl;
} else {
var content = this.props.mxEvent.getContent();
return MatrixClientPeg.get().mxcUrlToHttp(content.url, 800, 600);
}
},
componentDidMount: function() {
this.dispatcherRef = dis.register(this.onAction);
this.fixupHeight();
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
decryptFile(content.file).done((url) => {
this.setState({
decryptedUrl: url,
});
}, (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";
});
}
},
componentWillUnmount: function() {
@ -135,13 +103,14 @@ module.exports = React.createClass({
return;
}
const content = this.props.mxEvent.getContent();
const timelineWidth = this.refs.body.offsetWidth;
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
var content = this.props.mxEvent.getContent();
var thumbHeight = null;
var timelineWidth = this.refs.body.offsetWidth;
var maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
//console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
var thumbHeight = null;
if (content.info) {
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
}
@ -150,35 +119,45 @@ module.exports = React.createClass({
},
render: function() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const content = this.props.mxEvent.getContent();
var TintableSvg = sdk.getComponent("elements.TintableSvg");
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_MImageBody" ref="body">
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
alt={content.body} />
</span>
var download;
if (this.props.tileShape === "file_grid") {
download = (
<div className="mx_MImageBody_download">
<a className="mx_MImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{content.body}
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
</div>
</div>
);
}
else {
download = (
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</div>
);
}
const contentUrl = this._getContentUrl();
const thumbUrl = this._getThumbUrl();
var thumbUrl = this._getThumbUrl();
if (thumbUrl) {
return (
<span className="mx_MImageBody" ref="body">
<a href={contentUrl} onClick={ this.onClick }>
<a href={cli.mxcUrlToHttp(content.url)} onClick={ this.onClick }>
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
alt={content.body}
onMouseEnter={this.onImageEnter}
onMouseLeave={this.onImageLeave} />
</a>
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
{ download }
</span>
);
} else if (content.body) {

View file

@ -16,24 +16,16 @@ limitations under the License.
'use strict';
import React from 'react';
import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Model from '../../../Modal';
import sdk from '../../../index';
import { decryptFile } from '../../../utils/DecryptFile';
import q from 'q';
var React = require('react');
var filesize = require('filesize');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var Modal = require('../../../Modal');
var sdk = require('../../../index');
module.exports = React.createClass({
displayName: 'MVideoBody',
getInitialState: function() {
return {
decryptedUrl: null,
decryptedThumbnailUrl: null,
};
},
thumbScale: function(fullWidth, fullHeight, thumbWidth, thumbHeight) {
if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
@ -56,92 +48,59 @@ module.exports = React.createClass({
}
},
_getContentUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedUrl;
} else {
return MatrixClientPeg.get().mxcUrlToHttp(content.url);
}
},
_getThumbUrl: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined) {
return this.state.decryptedThumbnailUrl;
} else if (content.info.thumbnail_url) {
return MatrixClientPeg.get().mxcUrlToHttp(content.info.thumbnail_url);
} else {
return null;
}
},
componentDidMount: function() {
const content = this.props.mxEvent.getContent();
if (content.file !== undefined && this.state.decryptedUrl === null) {
var thumbnailPromise = q(null);
if (content.info.thumbnail_file) {
thumbnailPromise = decryptFile(
content.info.thumbnail_file
);
}
thumbnailPromise.then((thumbnailUrl) => {
decryptFile(content.file).then((contentUrl) => {
this.setState({
decryptedUrl: contentUrl,
decryptedThumbnailUrl: thumbnailUrl,
});
});
}).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";
}).done();
}
},
render: function() {
const content = this.props.mxEvent.getContent();
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_MImageBody" ref="body">
<img className="mx_MImageBody_thumbnail" src="img/spinner.gif" ref="image"
alt={content.body} />
</span>
);
}
const contentUrl = this._getContentUrl();
const thumbUrl = this._getThumbUrl();
var content = this.props.mxEvent.getContent();
var cli = MatrixClientPeg.get();
var height = null;
var width = null;
var poster = null;
var preload = "metadata";
if (content.info) {
const scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
var scale = this.thumbScale(content.info.w, content.info.h, 480, 360);
if (scale) {
width = Math.floor(content.info.w * scale);
height = Math.floor(content.info.h * scale);
}
if (thumbUrl) {
poster = thumbUrl;
if (content.info.thumbnail_url) {
poster = cli.mxcUrlToHttp(content.info.thumbnail_url);
preload = "none";
}
}
var download;
if (this.props.tileShape === "file_grid") {
download = (
<div className="mx_MImageBody_download">
<a className="mx_MImageBody_downloadLink" href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
{content.body}
</a>
<div className="mx_MImageBody_size">
{ content.info && content.info.size ? filesize(content.info.size) : "" }
</div>
</div>
);
}
else {
var TintableSvg = sdk.getComponent("elements.TintableSvg");
download = (
<div className="mx_MImageBody_download">
<a href={cli.mxcUrlToHttp(content.url)} target="_blank" rel="noopener">
<TintableSvg src="img/download.svg" width="12" height="14"/>
Download {content.body} ({ content.info && content.info.size ? filesize(content.info.size) : "Unknown size" })
</a>
</div>
);
}
return (
<span className="mx_MVideoBody">
<video className="mx_MVideoBody" src={contentUrl} alt={content.body}
<video className="mx_MVideoBody" src={cli.mxcUrlToHttp(content.url)} alt={content.body}
controls preload={preload} autoPlay={false}
height={height} width={width} poster={poster}>
</video>
<MFileBody {...this.props} decryptedUrl={this.state.decryptedUrl} />
{ download }
</span>
);
},

View file

@ -1,67 +0,0 @@
/*
Copyright 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Pull in the encryption lib so that we can decrypt attachments.
import encrypt from 'browser-encrypt-attachment';
// Pull in a fetch polyfill so we can download encrypted attachments.
import 'isomorphic-fetch';
// Grab the client so that we can turn mxc:// URLs into https:// URLS.
import MatrixClientPeg from '../MatrixClientPeg';
import q from 'q';
/**
* Read blob as a data:// URI.
* @return {Promise} A promise that resolves with the data:// URI.
*/
function readBlobAsDataUri(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.readAsDataURL(file);
return deferred.promise;
}
/**
* Decrypt a file attached to a matrix event.
* @param file {Object} The json taken from the matrix event.
* This passed to [link]{@link https://github.com/matrix-org/browser-encrypt-attachments}
* as the encryption info object, so will also have the those keys in addition to
* the keys below.
* @param file.url {string} An mxc:// URL for the encrypted file.
* @param file.mimetype {string} The MIME-type of the plaintext file.
*/
export function decryptFile(file) {
const url = MatrixClientPeg.get().mxcUrlToHttp(file.url);
// Download the encrypted file as an array buffer.
return q(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 give it the correct MIME-type.
var blob = new Blob([dataArray], {type: file.mimetype});
return readBlobAsDataUri(blob);
});
}