This commit is contained in:
Bruno Windels 2020-11-10 22:36:26 +01:00
parent a37d8c0223
commit 2cfffa015d
9 changed files with 189 additions and 22 deletions

View file

@ -0,0 +1,23 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script type="text/javascript">
const bytes = [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100];
const buffer = new Uint8Array(bytes.length);
for (let i = 0; i < buffer.length; i += 1) {
buffer[i] = bytes[i];
}
const blob = new Blob([buffer], {type: "text/plain"});
const reader = new FileReader();
reader.addEventListener("load", function(evt) {
const result = evt.target.result;
console.log("result", result);
});
reader.addEventListener("error", function(evt) {reject(evt.target.error);});
reader.readAsText(blob, "utf-8");
</script>
</body>
</html>

View file

@ -164,6 +164,36 @@ export class RoomViewModel extends ViewModel {
return false; return false;
} }
async _sendFile() {
const file = this.platform.openFile();
let blob = file.blob;
let encryptedFile;
if (this._room.isEncrypted) {
const {data, info} = await this._room.encryptAttachment(blob);
blob = data;
encryptedFile = Object.assign(info, {
mimetype: file.blob.mimeType,
url: null
});
}
const mxcUrl = await this._room.mediaRepository.upload(blob, file.name);
const content = {
body: file.name,
msgtype: "m.file",
info: {
size: blob.size,
mimetype: file.blob.mimeType,
},
};
if (encryptedFile) {
encryptedFile.url = mxcUrl;
content.file = encryptedFile;
} else {
content.url = mxcUrl;
}
await this._room.sendEvent("m.room.message", content);
}
get composerViewModel() { get composerViewModel() {
return this._composerVM; return this._composerVM;
} }

View file

@ -33,14 +33,14 @@ export class FileTile extends MessageTile {
const filename = content.body; const filename = content.body;
this._downloading = true; this._downloading = true;
this.emitChange("label"); this.emitChange("label");
let bufferHandle; let blob;
try { try {
bufferHandle = await this._mediaRepository.downloadAttachment(content); blob = await this._mediaRepository.downloadAttachment(content);
this.platform.offerSaveBufferHandle(bufferHandle, filename); this.platform.saveFileAs(blob, filename);
} catch (err) { } catch (err) {
this._error = err; this._error = err;
} finally { } finally {
bufferHandle?.dispose(); blob?.dispose();
this._downloading = false; this._downloading = false;
} }
this.emitChange("label"); this.emitChange("label");

View file

@ -35,12 +35,12 @@ export class ImageTile extends MessageTile {
} }
async _loadEncryptedFile(file) { async _loadEncryptedFile(file) {
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file, true); const blob = await this._mediaRepository.downloadEncryptedFile(file, true);
if (this.isDisposed) { if (this.isDisposed) {
bufferHandle.dispose(); blob.dispose();
return; return;
} }
return this.track(bufferHandle); return this.track(blob);
} }
async load() { async load() {

View file

@ -56,3 +56,21 @@ export async function decryptAttachment(crypto, ciphertextBuffer, info) {
}); });
return decryptedBuffer; return decryptedBuffer;
} }
export async function encryptAttachment(crypto, data) {
const iv = await crypto.aes.generateIV();
const key = await crypto.aes.generateKey("jwk", 256);
const ciphertext = await crypto.aes.encryptCTR({key, iv, data});
const digest = await crypto.digest("SHA-256", ciphertext);
return {
data: ciphertext,
info: {
v: "v2",
key,
iv: base64.encode(iv),
hashes: {
sha256: base64.encode(digest)
}
}
};
}

View file

@ -56,13 +56,13 @@ export class MediaRepository {
const url = this.mxcUrl(fileEntry.url); const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); const {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
const decryptedBuffer = await decryptAttachment(this._platform.crypto, encryptedBuffer, fileEntry); const decryptedBuffer = await decryptAttachment(this._platform.crypto, encryptedBuffer, fileEntry);
return this._platform.createBufferHandle(decryptedBuffer, fileEntry.mimetype); return this._platform.createBlob(decryptedBuffer, fileEntry.mimetype);
} }
async downloadPlaintextFile(mxcUrl, mimetype, cache = false) { async downloadPlaintextFile(mxcUrl, mimetype, cache = false) {
const url = this.mxcUrl(mxcUrl); const url = this.mxcUrl(mxcUrl);
const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response(); const {body: buffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
return this._platform.createBufferHandle(buffer, mimetype); return this._platform.createBlob(buffer, mimetype);
} }
async downloadAttachment(content, cache = false) { async downloadAttachment(content, cache = false) {
@ -73,4 +73,10 @@ export class MediaRepository {
} }
} }
async upload(bufferHandle, filename) {
const url = `${this._homeServer}/_matrix/media/r0/upload?filename=${encodeURIComponent(filename)}`;
// TODO: body doesn't take a bufferHandle currently
const {content_uri} = await this._platform.request(url, {method: "POST", body: bufferHandle}).response();
return content_uri;
}
} }

View file

@ -27,7 +27,7 @@ import {OnlineStatus} from "./dom/OnlineStatus.js";
import {Crypto} from "./dom/Crypto.js"; import {Crypto} from "./dom/Crypto.js";
import {estimateStorageUsage} from "./dom/StorageEstimate.js"; import {estimateStorageUsage} from "./dom/StorageEstimate.js";
import {WorkerPool} from "./dom/WorkerPool.js"; import {WorkerPool} from "./dom/WorkerPool.js";
import {BufferHandle} from "./dom/BufferHandle.js"; import {BlobHandle} from "./dom/BlobHandle.js";
import {downloadInIframe} from "./dom/download.js"; import {downloadInIframe} from "./dom/download.js";
function addScript(src) { function addScript(src) {
@ -131,15 +131,37 @@ export class Platform {
this._serviceWorkerHandler?.setNavigation(navigation); this._serviceWorkerHandler?.setNavigation(navigation);
} }
createBufferHandle(buffer, mimetype) { createBlob(buffer, mimetype) {
return new BufferHandle(buffer, mimetype); return BlobHandle.fromBuffer(buffer, mimetype);
} }
offerSaveBufferHandle(bufferHandle, filename) { saveFileAs(blobHandle, filename) {
if (navigator.msSaveBlob) { if (navigator.msSaveBlob) {
navigator.msSaveBlob(bufferHandle.blob, filename); navigator.msSaveBlob(blobHandle.blob, filename);
} else { } else {
downloadInIframe(this._container, this._paths.downloadSandbox, bufferHandle.blob, filename); downloadInIframe(this._container, this._paths.downloadSandbox, blobHandle.blob, filename);
} }
} }
openFile(mimeType = null) {
const input = document.createElement("input");
input.setAttribute("type", "file");
if (mimeType) {
input.setAttribute("accept", mimeType);
}
const promise = new Promise((resolve, reject) => {
const checkFile = () => {
input.removeEventListener("change", checkFile, true);
const file = input.files[0];
if (file) {
resolve({name: file.name, blob: BlobHandle.fromFile(file)});
} else {
reject(new Error("No file selected"));
}
}
input.addEventListener("change", checkFile, true);
});
input.click();
return promise;
}
} }

View file

@ -69,14 +69,52 @@ const ALLOWED_BLOB_MIMETYPES = {
'audio/x-flac': true, 'audio/x-flac': true,
}; };
export class BufferHandle { export class BlobHandle {
constructor(buffer, mimetype) { constructor(blob, buffer = null) {
this.blob = blob;
this._buffer = buffer;
this._url = null;
}
static fromBuffer(buffer, mimetype) {
mimetype = mimetype ? mimetype.split(";")[0].trim() : ''; mimetype = mimetype ? mimetype.split(";")[0].trim() : '';
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) { if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
mimetype = 'application/octet-stream'; mimetype = 'application/octet-stream';
} }
this.blob = new Blob([buffer], {type: mimetype}); return new BlobHandle(new Blob([buffer], {type: mimetype}), buffer);
this._url = null; }
static fromFile(file) {
// ok to not filter mimetypes as these are local files
return new BlobHandle(file);
}
async readAsBuffer() {
if (this._buffer) {
return this._buffer;
} else {
const reader = new FileReader();
const promise = new Promise((resolve, reject) => {
reader.addEventListener("load", evt => resolve(evt.target.result));
reader.addEventListener("error", evt => reject(evt.target.error));
});
reader.readAsArrayBuffer(this.blob);
return promise;
}
}
async readAsText() {
if (this._buffer) {
return this._buffer;
} else {
const reader = new FileReader();
const promise = new Promise((resolve, reject) => {
reader.addEventListener("load", evt => resolve(evt.target.result));
reader.addEventListener("error", evt => reject(evt.target.error));
});
reader.readAsText(this.blob, "utf-8");
return promise;
}
} }
get url() { get url() {
@ -86,6 +124,14 @@ export class BufferHandle {
return this._url; return this._url;
} }
get size() {
return this.blob.size;
}
get mimeType() {
return this.blob.type;
}
dispose() { dispose() {
if (this._url) { if (this._url) {
URL.revokeObjectURL(this._url); URL.revokeObjectURL(this._url);

View file

@ -153,8 +153,9 @@ class DeriveCrypto {
} }
class AESCrypto { class AESCrypto {
constructor(subtleCrypto) { constructor(subtleCrypto, crypto) {
this._subtleCrypto = subtleCrypto; this._subtleCrypto = subtleCrypto;
this._crypto = crypto;
} }
/** /**
* [decrypt description] * [decrypt description]
@ -228,6 +229,27 @@ class AESCrypto {
throw new Error(`Could not decrypt with AES-CTR: ${err.message}`); throw new Error(`Could not decrypt with AES-CTR: ${err.message}`);
} }
} }
/**
* Generate a CTR key
* @param {String} format "raw" or "jwk"
* @param {Number} length 128 or 256
* @return {Promise<Object>} an object for jwk, or a BufferSource for raw
*/
async generateKey(format, length = 256) {
const cryptoKey = await subtleCryptoResult(this._subtleCrypto.generateKey(
{"name": "AES-CTR", length}, true, ["encrypt", "decrypt"]));
return subtleCryptoResult(this._subtleCrypto.exportKey("jwk", cryptoKey));
}
async generateIV() {
const randomBytes = this._crypto.getRandomValues(new Uint8Array(8));
const ivArray = new Uint8Array(16);
for (let i = 0; i < randomBytes.length; i += 1) {
ivArray[i] = randomBytes[i];
}
return ivArray;
}
} }
@ -291,9 +313,9 @@ export class Crypto {
// not exactly guaranteeing AES-CTR support // not exactly guaranteeing AES-CTR support
// but in practice IE11 doesn't have this // but in practice IE11 doesn't have this
if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) { if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs); this.aes = new AESLegacyCrypto(cryptoExtras.aesjs, crypto);
} else { } else {
this.aes = new AESCrypto(subtleCrypto); this.aes = new AESCrypto(subtleCrypto, crypto);
} }
this.hmac = new HMACCrypto(subtleCrypto); this.hmac = new HMACCrypto(subtleCrypto);
this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras); this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras);