forked from mystiq/hydrogen-web
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() {
|
||||
this._entrySubscription = this._entrySubscription();
|
||||
for(let i = 0; i < this._tiles.length; i+= 1) {
|
||||
this._tiles[i].dispose();
|
||||
}
|
||||
this._tiles = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue