basic PoC of image decryption working

needs looooaaads of cleanup still
This commit is contained in:
Bruno Windels 2020-10-23 17:18:11 +02:00
parent 8125173420
commit 3a6268f0c1
9 changed files with 109 additions and 31 deletions

View file

@ -95,6 +95,9 @@ export class TilesCollection extends BaseObservableList {
onUnsubscribeLast() {
this._entrySubscription = this._entrySubscription();
for(let i = 0; i < this._tiles.length; i+= 1) {
this._tiles[i].dispose();
}
this._tiles = null;
}

View file

@ -20,20 +20,47 @@ const MAX_HEIGHT = 300;
const MAX_WIDTH = 400;
export class ImageTile extends MessageTile {
constructor(options) {
super(options);
this._decryptedUrl = null;
this.load();
}
async load() {
const thumbnailFile = this._getContent().file;
if (thumbnailFile) {
const buffer = await this._mediaRepository.downloadEncryptedFile(thumbnailFile);
// TODO: fix XSS bug here by not checking mimetype
const blob = new Blob([buffer], {type: thumbnailFile.mimetype});
if (this.isDisposed) {
return;
}
this._decryptedUrl = URL.createObjectURL(blob);
this.emitChange("thumbnailUrl");
}
}
get thumbnailUrl() {
if (this._decryptedUrl) {
return this._decryptedUrl;
}
const mxcUrl = this._getContent()?.url;
if (typeof mxcUrl === "string") {
return this._mediaRepository.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
}
return null;
return "";
}
get url() {
if (this._decryptedUrl) {
return this._decryptedUrl;
}
const mxcUrl = this._getContent()?.url;
if (typeof mxcUrl === "string") {
return this._mediaRepository.mxcUrl(mxcUrl);
}
return null;
return "";
}
_scaleFactor() {
@ -62,4 +89,12 @@ export class ImageTile extends MessageTile {
get shape() {
return "image";
}
dispose() {
if (this._decryptedUrl) {
URL.revokeObjectURL(this._decryptedUrl);
this._decryptedUrl = null;
}
super.dispose();
}
}

View file

@ -168,6 +168,11 @@ export class SessionContainer {
}
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
this._requestScheduler.start();
const mediaRepository = new MediaRepository({
homeServer: sessionInfo.homeServer,
cryptoDriver: this._cryptoDriver,
request: this._request,
});
this._session = new Session({
storage: this._storage,
sessionInfo: filteredSessionInfo,
@ -176,7 +181,7 @@ export class SessionContainer {
clock: this._clock,
olmWorker,
cryptoDriver: this._cryptoDriver,
mediaRepository: new MediaRepository(sessionInfo.homeServer)
mediaRepository
});
await this._session.load();
if (isNewLogin) {

View file

@ -75,7 +75,8 @@ export class HomeServerApi {
method,
headers,
body: bodyString,
timeout: options?.timeout
timeout: options?.timeout,
format: "json"
});
const wrapper = new RequestWrapper(method, url, requestResult);

View file

@ -15,17 +15,20 @@ limitations under the License.
*/
import {encodeQueryParams} from "./common.js";
import {decryptAttachment} from "../e2ee/attachment.js";
export class MediaRepository {
constructor(homeserver) {
this._homeserver = homeserver;
constructor({homeServer, cryptoDriver, request}) {
this._homeServer = homeServer;
this._cryptoDriver = cryptoDriver;
this._request = request;
}
mxcUrlThumbnail(url, width, height, method) {
const parts = this._parseMxcUrl(url);
if (parts) {
const [serverName, mediaId] = parts;
const httpUrl = `${this._homeserver}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
const httpUrl = `${this._homeServer}/_matrix/media/r0/thumbnail/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
return httpUrl + "?" + encodeQueryParams({width, height, method});
}
return null;
@ -35,7 +38,7 @@ export class MediaRepository {
const parts = this._parseMxcUrl(url);
if (parts) {
const [serverName, mediaId] = parts;
return `${this._homeserver}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
return `${this._homeServer}/_matrix/media/r0/download/${encodeURIComponent(serverName)}/${encodeURIComponent(mediaId)}`;
} else {
return null;
}
@ -49,4 +52,11 @@ export class MediaRepository {
return null;
}
}
async downloadEncryptedFile(fileEntry) {
const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._request(url, {format: "buffer"}).response();
const decryptedBuffer = await decryptAttachment(this._cryptoDriver, encryptedBuffer, fileEntry);
return decryptedBuffer;
}
}

View file

@ -51,8 +51,9 @@ class RequestResult {
}
export function createFetchRequest(createTimeout) {
return function fetchRequest(url, options) {
return function fetchRequest(url, {method, headers, body, timeout, format}) {
const controller = typeof AbortController === "function" ? new AbortController() : null;
let options = {method, body};
if (controller) {
options = Object.assign(options, {
signal: controller.signal
@ -76,18 +77,22 @@ export function createFetchRequest(createTimeout) {
// cache: "no-store",
cache: "default",
});
if (options.headers) {
const headers = new Headers();
for(const [name, value] of options.headers.entries()) {
headers.append(name, value);
if (headers) {
const fetchHeaders = new Headers();
for(const [name, value] of headers.entries()) {
fetchHeaders.append(name, value);
}
options.headers = headers;
options.headers = fetchHeaders;
}
const promise = fetch(url, options).then(async response => {
const {status} = response;
let body;
try {
body = await response.json();
if (format === "json") {
body = await response.json();
} else if (format === "buffer") {
body = await response.arrayBuffer();
}
} catch (err) {
// some error pages return html instead of json, ignore error
if (!(err.name === "SyntaxError" && status >= 400)) {
@ -105,14 +110,14 @@ export function createFetchRequest(createTimeout) {
//
// One could check navigator.onLine to rule out the first
// but the 2 latter ones are indistinguishable from javascript.
throw new ConnectionError(`${options.method} ${url}: ${err.message}`);
throw new ConnectionError(`${method} ${url}: ${err.message}`);
}
throw err;
});
const result = new RequestResult(promise, controller);
if (options.timeout) {
result.promise = abortOnTimeout(createTimeout, options.timeout, result, result.promise);
if (timeout) {
result.promise = abortOnTimeout(createTimeout, timeout, result, result.promise);
}
return result;

View file

@ -64,8 +64,11 @@ export class SecretStorage {
throw new Error("Bad MAC");
}
const plaintextBytes = await this._cryptoDriver.aes.decrypt(
aesKey, base64.decode(encryptedData.iv), ciphertextBytes);
const plaintextBytes = await this._cryptoDriver.aes.decryptCTR({
key: aesKey,
iv: base64.decode(encryptedData.iv),
data: ciphertextBytes
});
return textDecoder.decode(plaintextBytes);
}

View file

@ -158,22 +158,26 @@ class CryptoAESDriver {
}
/**
* [decrypt description]
* @param {BufferSource} key [description]
* @param {string} keyFormat "raw" or "jwk"
* @param {BufferSource | Object} key [description]
* @param {BufferSource} iv [description]
* @param {BufferSource} ciphertext [description]
* @param {BufferSource} data [description]
* @param {Number} counterLength the size of the counter, in bits
* @return {BufferSource} [description]
*/
async decrypt(key, iv, ciphertext) {
async decryptCTR({key, jwkKey, iv, data, counterLength = 64}) {
const opts = {
name: "AES-CTR",
counter: iv,
length: 64,
length: counterLength,
};
let aesKey;
try {
const selectedKey = key || jwkKey;
const format = jwkKey ? "jwk" : "raw";
aesKey = await subtleCryptoResult(this._subtleCrypto.importKey(
'raw',
key,
format,
selectedKey,
opts,
false,
['decrypt'],
@ -186,7 +190,7 @@ class CryptoAESDriver {
// see https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
opts,
aesKey,
ciphertext,
data,
), "decrypt");
return new Uint8Array(plaintext);
} catch (err) {
@ -205,12 +209,24 @@ class CryptoLegacyAESDriver {
* @param {BufferSource} key [description]
* @param {BufferSource} iv [description]
* @param {BufferSource} ciphertext [description]
* @param {Number} counterLength the size of the counter, in bits
* @return {BufferSource} [description]
*/
async decrypt(key, iv, ciphertext) {
async decryptCTR({key, jwkKey, iv, data, counterLength = 64}) {
// TODO: support counterLength and jwkKey
const aesjs = this._aesjs;
// This won't work as aesjs throws with iv.length !== 16
// const nonceLength = 8;
// const expectedIVLength = (counterLength / 8) + nonceLength;
// if (iv.length < expectedIVLength) {
// const newIV = new Uint8Array(expectedIVLength);
// for(let i = 0; i < iv.length; ++i) {
// newIV[i] = iv[i];
// }
// iv = newIV;
// }
var aesCtr = new aesjs.ModeOfOperation.ctr(new Uint8Array(key), new aesjs.Counter(new Uint8Array(iv)));
return aesCtr.decrypt(new Uint8Array(ciphertext));
return aesCtr.decrypt(new Uint8Array(data));
}
}

View file

@ -23,14 +23,14 @@ export class ImageView extends TemplateView {
const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
const image = t.img({
className: "picture",
src: vm.thumbnailUrl,
src: vm => vm.thumbnailUrl,
width: vm.thumbnailWidth,
height: vm.thumbnailHeight,
loading: "lazy",
alt: vm.label,
});
const linkContainer = t.a({
href: vm.url,
href: vm => vm.url,
target: "_blank",
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
}, image);