WIP
This commit is contained in:
parent
a37d8c0223
commit
2cfffa015d
9 changed files with 189 additions and 22 deletions
23
prototypes/ie11-textdecoder.html
Normal file
23
prototypes/ie11-textdecoder.html
Normal 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>
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
@ -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);
|
||||||
|
|
Reference in a new issue