forked from mystiq/hydrogen-web
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
return this._composerVM;
|
||||
}
|
||||
|
|
|
@ -33,14 +33,14 @@ export class FileTile extends MessageTile {
|
|||
const filename = content.body;
|
||||
this._downloading = true;
|
||||
this.emitChange("label");
|
||||
let bufferHandle;
|
||||
let blob;
|
||||
try {
|
||||
bufferHandle = await this._mediaRepository.downloadAttachment(content);
|
||||
this.platform.offerSaveBufferHandle(bufferHandle, filename);
|
||||
blob = await this._mediaRepository.downloadAttachment(content);
|
||||
this.platform.saveFileAs(blob, filename);
|
||||
} catch (err) {
|
||||
this._error = err;
|
||||
} finally {
|
||||
bufferHandle?.dispose();
|
||||
blob?.dispose();
|
||||
this._downloading = false;
|
||||
}
|
||||
this.emitChange("label");
|
||||
|
|
|
@ -35,12 +35,12 @@ export class ImageTile extends MessageTile {
|
|||
}
|
||||
|
||||
async _loadEncryptedFile(file) {
|
||||
const bufferHandle = await this._mediaRepository.downloadEncryptedFile(file, true);
|
||||
const blob = await this._mediaRepository.downloadEncryptedFile(file, true);
|
||||
if (this.isDisposed) {
|
||||
bufferHandle.dispose();
|
||||
blob.dispose();
|
||||
return;
|
||||
}
|
||||
return this.track(bufferHandle);
|
||||
return this.track(blob);
|
||||
}
|
||||
|
||||
async load() {
|
||||
|
|
|
@ -56,3 +56,21 @@ export async function decryptAttachment(crypto, ciphertextBuffer, info) {
|
|||
});
|
||||
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 {body: encryptedBuffer} = await this._platform.request(url, {method: "GET", format: "buffer", cache}).response();
|
||||
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) {
|
||||
const url = this.mxcUrl(mxcUrl);
|
||||
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) {
|
||||
|
@ -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 {estimateStorageUsage} from "./dom/StorageEstimate.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";
|
||||
|
||||
function addScript(src) {
|
||||
|
@ -131,15 +131,37 @@ export class Platform {
|
|||
this._serviceWorkerHandler?.setNavigation(navigation);
|
||||
}
|
||||
|
||||
createBufferHandle(buffer, mimetype) {
|
||||
return new BufferHandle(buffer, mimetype);
|
||||
createBlob(buffer, mimetype) {
|
||||
return BlobHandle.fromBuffer(buffer, mimetype);
|
||||
}
|
||||
|
||||
offerSaveBufferHandle(bufferHandle, filename) {
|
||||
saveFileAs(blobHandle, filename) {
|
||||
if (navigator.msSaveBlob) {
|
||||
navigator.msSaveBlob(bufferHandle.blob, filename);
|
||||
navigator.msSaveBlob(blobHandle.blob, filename);
|
||||
} 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,
|
||||
};
|
||||
|
||||
export class BufferHandle {
|
||||
constructor(buffer, mimetype) {
|
||||
export class BlobHandle {
|
||||
constructor(blob, buffer = null) {
|
||||
this.blob = blob;
|
||||
this._buffer = buffer;
|
||||
this._url = null;
|
||||
}
|
||||
|
||||
static fromBuffer(buffer, mimetype) {
|
||||
mimetype = mimetype ? mimetype.split(";")[0].trim() : '';
|
||||
if (!ALLOWED_BLOB_MIMETYPES[mimetype]) {
|
||||
mimetype = 'application/octet-stream';
|
||||
}
|
||||
this.blob = new Blob([buffer], {type: mimetype});
|
||||
this._url = null;
|
||||
return new BlobHandle(new Blob([buffer], {type: mimetype}), buffer);
|
||||
}
|
||||
|
||||
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() {
|
||||
|
@ -86,6 +124,14 @@ export class BufferHandle {
|
|||
return this._url;
|
||||
}
|
||||
|
||||
get size() {
|
||||
return this.blob.size;
|
||||
}
|
||||
|
||||
get mimeType() {
|
||||
return this.blob.type;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this._url) {
|
||||
URL.revokeObjectURL(this._url);
|
|
@ -153,8 +153,9 @@ class DeriveCrypto {
|
|||
}
|
||||
|
||||
class AESCrypto {
|
||||
constructor(subtleCrypto) {
|
||||
constructor(subtleCrypto, crypto) {
|
||||
this._subtleCrypto = subtleCrypto;
|
||||
this._crypto = crypto;
|
||||
}
|
||||
/**
|
||||
* [decrypt description]
|
||||
|
@ -228,6 +229,27 @@ class AESCrypto {
|
|||
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
|
||||
// but in practice IE11 doesn't have this
|
||||
if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
|
||||
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs);
|
||||
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs, crypto);
|
||||
} else {
|
||||
this.aes = new AESCrypto(subtleCrypto);
|
||||
this.aes = new AESCrypto(subtleCrypto, crypto);
|
||||
}
|
||||
this.hmac = new HMACCrypto(subtleCrypto);
|
||||
this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras);
|
||||
|
|
Loading…
Reference in a new issue