basic PoC of image decryption working
needs looooaaads of cleanup still
This commit is contained in:
parent
8125173420
commit
3a6268f0c1
9 changed files with 109 additions and 31 deletions
|
@ -95,6 +95,9 @@ export class TilesCollection extends BaseObservableList {
|
||||||
|
|
||||||
onUnsubscribeLast() {
|
onUnsubscribeLast() {
|
||||||
this._entrySubscription = this._entrySubscription();
|
this._entrySubscription = this._entrySubscription();
|
||||||
|
for(let i = 0; i < this._tiles.length; i+= 1) {
|
||||||
|
this._tiles[i].dispose();
|
||||||
|
}
|
||||||
this._tiles = null;
|
this._tiles = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,20 +20,47 @@ const MAX_HEIGHT = 300;
|
||||||
const MAX_WIDTH = 400;
|
const MAX_WIDTH = 400;
|
||||||
|
|
||||||
export class ImageTile extends MessageTile {
|
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() {
|
get thumbnailUrl() {
|
||||||
|
if (this._decryptedUrl) {
|
||||||
|
return this._decryptedUrl;
|
||||||
|
}
|
||||||
const mxcUrl = this._getContent()?.url;
|
const mxcUrl = this._getContent()?.url;
|
||||||
if (typeof mxcUrl === "string") {
|
if (typeof mxcUrl === "string") {
|
||||||
return this._mediaRepository.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
|
return this._mediaRepository.mxcUrlThumbnail(mxcUrl, this.thumbnailWidth, this.thumbnailHeight, "scale");
|
||||||
}
|
}
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
get url() {
|
get url() {
|
||||||
|
if (this._decryptedUrl) {
|
||||||
|
return this._decryptedUrl;
|
||||||
|
}
|
||||||
const mxcUrl = this._getContent()?.url;
|
const mxcUrl = this._getContent()?.url;
|
||||||
if (typeof mxcUrl === "string") {
|
if (typeof mxcUrl === "string") {
|
||||||
return this._mediaRepository.mxcUrl(mxcUrl);
|
return this._mediaRepository.mxcUrl(mxcUrl);
|
||||||
}
|
}
|
||||||
return null;
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
_scaleFactor() {
|
_scaleFactor() {
|
||||||
|
@ -62,4 +89,12 @@ export class ImageTile extends MessageTile {
|
||||||
get shape() {
|
get shape() {
|
||||||
return "image";
|
return "image";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
if (this._decryptedUrl) {
|
||||||
|
URL.revokeObjectURL(this._decryptedUrl);
|
||||||
|
this._decryptedUrl = null;
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,11 @@ export class SessionContainer {
|
||||||
}
|
}
|
||||||
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
|
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
|
||||||
this._requestScheduler.start();
|
this._requestScheduler.start();
|
||||||
|
const mediaRepository = new MediaRepository({
|
||||||
|
homeServer: sessionInfo.homeServer,
|
||||||
|
cryptoDriver: this._cryptoDriver,
|
||||||
|
request: this._request,
|
||||||
|
});
|
||||||
this._session = new Session({
|
this._session = new Session({
|
||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
sessionInfo: filteredSessionInfo,
|
sessionInfo: filteredSessionInfo,
|
||||||
|
@ -176,7 +181,7 @@ export class SessionContainer {
|
||||||
clock: this._clock,
|
clock: this._clock,
|
||||||
olmWorker,
|
olmWorker,
|
||||||
cryptoDriver: this._cryptoDriver,
|
cryptoDriver: this._cryptoDriver,
|
||||||
mediaRepository: new MediaRepository(sessionInfo.homeServer)
|
mediaRepository
|
||||||
});
|
});
|
||||||
await this._session.load();
|
await this._session.load();
|
||||||
if (isNewLogin) {
|
if (isNewLogin) {
|
||||||
|
|
|
@ -75,7 +75,8 @@ export class HomeServerApi {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: bodyString,
|
body: bodyString,
|
||||||
timeout: options?.timeout
|
timeout: options?.timeout,
|
||||||
|
format: "json"
|
||||||
});
|
});
|
||||||
|
|
||||||
const wrapper = new RequestWrapper(method, url, requestResult);
|
const wrapper = new RequestWrapper(method, url, requestResult);
|
||||||
|
|
|
@ -15,17 +15,20 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {encodeQueryParams} from "./common.js";
|
import {encodeQueryParams} from "./common.js";
|
||||||
|
import {decryptAttachment} from "../e2ee/attachment.js";
|
||||||
|
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor(homeserver) {
|
constructor({homeServer, cryptoDriver, request}) {
|
||||||
this._homeserver = homeserver;
|
this._homeServer = homeServer;
|
||||||
|
this._cryptoDriver = cryptoDriver;
|
||||||
|
this._request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
mxcUrlThumbnail(url, width, height, method) {
|
mxcUrlThumbnail(url, width, height, method) {
|
||||||
const parts = this._parseMxcUrl(url);
|
const parts = this._parseMxcUrl(url);
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = 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 httpUrl + "?" + encodeQueryParams({width, height, method});
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -35,7 +38,7 @@ export class MediaRepository {
|
||||||
const parts = this._parseMxcUrl(url);
|
const parts = this._parseMxcUrl(url);
|
||||||
if (parts) {
|
if (parts) {
|
||||||
const [serverName, mediaId] = 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 {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -49,4 +52,11 @@ export class MediaRepository {
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,8 +51,9 @@ class RequestResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createFetchRequest(createTimeout) {
|
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;
|
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
||||||
|
let options = {method, body};
|
||||||
if (controller) {
|
if (controller) {
|
||||||
options = Object.assign(options, {
|
options = Object.assign(options, {
|
||||||
signal: controller.signal
|
signal: controller.signal
|
||||||
|
@ -76,18 +77,22 @@ export function createFetchRequest(createTimeout) {
|
||||||
// cache: "no-store",
|
// cache: "no-store",
|
||||||
cache: "default",
|
cache: "default",
|
||||||
});
|
});
|
||||||
if (options.headers) {
|
if (headers) {
|
||||||
const headers = new Headers();
|
const fetchHeaders = new Headers();
|
||||||
for(const [name, value] of options.headers.entries()) {
|
for(const [name, value] of headers.entries()) {
|
||||||
headers.append(name, value);
|
fetchHeaders.append(name, value);
|
||||||
}
|
}
|
||||||
options.headers = headers;
|
options.headers = fetchHeaders;
|
||||||
}
|
}
|
||||||
const promise = fetch(url, options).then(async response => {
|
const promise = fetch(url, options).then(async response => {
|
||||||
const {status} = response;
|
const {status} = response;
|
||||||
let body;
|
let body;
|
||||||
try {
|
try {
|
||||||
body = await response.json();
|
if (format === "json") {
|
||||||
|
body = await response.json();
|
||||||
|
} else if (format === "buffer") {
|
||||||
|
body = await response.arrayBuffer();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// some error pages return html instead of json, ignore error
|
// some error pages return html instead of json, ignore error
|
||||||
if (!(err.name === "SyntaxError" && status >= 400)) {
|
if (!(err.name === "SyntaxError" && status >= 400)) {
|
||||||
|
@ -105,14 +110,14 @@ export function createFetchRequest(createTimeout) {
|
||||||
//
|
//
|
||||||
// One could check navigator.onLine to rule out the first
|
// One could check navigator.onLine to rule out the first
|
||||||
// but the 2 latter ones are indistinguishable from javascript.
|
// 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;
|
throw err;
|
||||||
});
|
});
|
||||||
const result = new RequestResult(promise, controller);
|
const result = new RequestResult(promise, controller);
|
||||||
|
|
||||||
if (options.timeout) {
|
if (timeout) {
|
||||||
result.promise = abortOnTimeout(createTimeout, options.timeout, result, result.promise);
|
result.promise = abortOnTimeout(createTimeout, timeout, result, result.promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -64,8 +64,11 @@ export class SecretStorage {
|
||||||
throw new Error("Bad MAC");
|
throw new Error("Bad MAC");
|
||||||
}
|
}
|
||||||
|
|
||||||
const plaintextBytes = await this._cryptoDriver.aes.decrypt(
|
const plaintextBytes = await this._cryptoDriver.aes.decryptCTR({
|
||||||
aesKey, base64.decode(encryptedData.iv), ciphertextBytes);
|
key: aesKey,
|
||||||
|
iv: base64.decode(encryptedData.iv),
|
||||||
|
data: ciphertextBytes
|
||||||
|
});
|
||||||
|
|
||||||
return textDecoder.decode(plaintextBytes);
|
return textDecoder.decode(plaintextBytes);
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,22 +158,26 @@ class CryptoAESDriver {
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* [decrypt description]
|
* [decrypt description]
|
||||||
* @param {BufferSource} key [description]
|
* @param {string} keyFormat "raw" or "jwk"
|
||||||
|
* @param {BufferSource | Object} key [description]
|
||||||
* @param {BufferSource} iv [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]
|
* @return {BufferSource} [description]
|
||||||
*/
|
*/
|
||||||
async decrypt(key, iv, ciphertext) {
|
async decryptCTR({key, jwkKey, iv, data, counterLength = 64}) {
|
||||||
const opts = {
|
const opts = {
|
||||||
name: "AES-CTR",
|
name: "AES-CTR",
|
||||||
counter: iv,
|
counter: iv,
|
||||||
length: 64,
|
length: counterLength,
|
||||||
};
|
};
|
||||||
let aesKey;
|
let aesKey;
|
||||||
try {
|
try {
|
||||||
|
const selectedKey = key || jwkKey;
|
||||||
|
const format = jwkKey ? "jwk" : "raw";
|
||||||
aesKey = await subtleCryptoResult(this._subtleCrypto.importKey(
|
aesKey = await subtleCryptoResult(this._subtleCrypto.importKey(
|
||||||
'raw',
|
format,
|
||||||
key,
|
selectedKey,
|
||||||
opts,
|
opts,
|
||||||
false,
|
false,
|
||||||
['decrypt'],
|
['decrypt'],
|
||||||
|
@ -186,7 +190,7 @@ class CryptoAESDriver {
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
|
// see https://developer.mozilla.org/en-US/docs/Web/API/AesCtrParams
|
||||||
opts,
|
opts,
|
||||||
aesKey,
|
aesKey,
|
||||||
ciphertext,
|
data,
|
||||||
), "decrypt");
|
), "decrypt");
|
||||||
return new Uint8Array(plaintext);
|
return new Uint8Array(plaintext);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -205,12 +209,24 @@ class CryptoLegacyAESDriver {
|
||||||
* @param {BufferSource} key [description]
|
* @param {BufferSource} key [description]
|
||||||
* @param {BufferSource} iv [description]
|
* @param {BufferSource} iv [description]
|
||||||
* @param {BufferSource} ciphertext [description]
|
* @param {BufferSource} ciphertext [description]
|
||||||
|
* @param {Number} counterLength the size of the counter, in bits
|
||||||
* @return {BufferSource} [description]
|
* @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;
|
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)));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,14 +23,14 @@ export class ImageView extends TemplateView {
|
||||||
const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
|
const heightRatioPercent = (vm.thumbnailHeight / vm.thumbnailWidth) * 100;
|
||||||
const image = t.img({
|
const image = t.img({
|
||||||
className: "picture",
|
className: "picture",
|
||||||
src: vm.thumbnailUrl,
|
src: vm => vm.thumbnailUrl,
|
||||||
width: vm.thumbnailWidth,
|
width: vm.thumbnailWidth,
|
||||||
height: vm.thumbnailHeight,
|
height: vm.thumbnailHeight,
|
||||||
loading: "lazy",
|
loading: "lazy",
|
||||||
alt: vm.label,
|
alt: vm.label,
|
||||||
});
|
});
|
||||||
const linkContainer = t.a({
|
const linkContainer = t.a({
|
||||||
href: vm.url,
|
href: vm => vm.url,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
|
style: `padding-top: ${heightRatioPercent}%; width: ${vm.thumbnailWidth}px;`
|
||||||
}, image);
|
}, image);
|
||||||
|
|
Reference in a new issue