diff --git a/web/media/encryption-utils.js b/web/media/encryption-utils.js index 041119d6e..77d4c9643 100644 --- a/web/media/encryption-utils.js +++ b/web/media/encryption-utils.js @@ -1,231 +1,239 @@ // @flow import invariant from 'invariant'; import { hexToUintArray, uintArrayToHexString } from 'lib/media/data-utils.js'; import { fileInfoFromData } from 'lib/media/file-utils.js'; import { fetchableMediaURI } from 'lib/media/media-utils.js'; import type { MediaMissionFailure, MediaMissionStep, } from 'lib/types/media-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { calculatePaddedLength, pad, unpad } from 'lib/utils/pkcs7-padding.js'; import * as AES from './aes-crypto-utils.js'; const PADDING_THRESHOLD = 5000000; // 5MB type EncryptFileResult = { +success: true, +file: File, +uri: string, +encryptionKey: string, + +sha256Hash: string, }; async function encryptFile(input: File): Promise<{ steps: $ReadOnlyArray, result: EncryptFileResult | MediaMissionFailure, }> { const steps = []; let success = true, exceptionMessage; // Step 1: Read the file into an ArrayBuffer let data; const arrayBufferStart = Date.now(); try { const inputBuffer = await input.arrayBuffer(); data = new Uint8Array(inputBuffer); } catch (e) { success = false; exceptionMessage = getMessageForException(e); } steps.push({ step: 'array_buffer_from_blob', success, exceptionMessage, time: Date.now() - arrayBufferStart, }); if (!success || !data) { return { steps, result: { success: false, reason: 'array_buffer_failed' } }; } // Step 2: Encrypt the data const startEncrypt = Date.now(); const paddedLength = calculatePaddedLength(data.length); const shouldPad = paddedLength <= PADDING_THRESHOLD; - let key, encryptedData; + let key, encryptedData, sha256; try { const plaintextData = shouldPad ? pad(data) : data; key = await AES.generateKey(); encryptedData = await AES.encrypt(key, plaintextData); + + const hashBytes = await crypto.subtle.digest('SHA-256', encryptedData); + sha256 = btoa(String.fromCharCode(...new Uint8Array(hashBytes))); } catch (e) { success = false; exceptionMessage = getMessageForException(e); } steps.push({ step: 'encrypt_data', dataSize: encryptedData?.byteLength ?? -1, isPadded: shouldPad, time: Date.now() - startEncrypt, - sha256: null, + sha256, success, exceptionMessage, }); - if (!success || !encryptedData || !key) { + if (encryptedData && !sha256) { + return { steps, result: { success: false, reason: 'digest_failed' } }; + } + if (!success || !encryptedData || !key || !sha256) { return { steps, result: { success: false, reason: 'encryption_failed' } }; } // Step 3: Create a File from the encrypted data const output = new File([encryptedData], input.name, { type: input.type }); return { steps, result: { success: true, file: output, uri: URL.createObjectURL(output), encryptionKey: uintArrayToHexString(key), + sha256Hash: sha256, }, }; } type DecryptFileStep = | { +step: 'fetch_buffer', +url: string, +time: number, +success: boolean, +exceptionMessage: ?string, } | { +step: 'decrypt_data', +dataSize: number, +time: number, +isPadded: boolean, +success: boolean, +exceptionMessage: ?string, } | { +step: 'save_blob', +objectURL: ?string, +mimeType: string, +time: number, +success: boolean, +exceptionMessage: ?string, }; type DecryptionFailure = | MediaMissionFailure | { +success: false, +reason: 'decrypt_data_failed' | 'save_blob_failed', }; /** * Fetches the encrypted media for given {@link holder}, decrypts it, * and stores it in a blob. Returns the object URL of the blob. * * The returned object URL should be revoked when the media is no longer needed. */ async function decryptMedia( holder: string, encryptionKey: string, ): Promise<{ steps: $ReadOnlyArray, result: { success: true, uri: string } | DecryptionFailure, }> { let success = true; let exceptionMessage; const steps: DecryptFileStep[] = []; // Step 1 - Fetch the encrypted media and convert it to a Uint8Array let data; const fetchStartTime = Date.now(); const url = fetchableMediaURI(holder); try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } const buffer = await response.arrayBuffer(); data = new Uint8Array(buffer); } catch (e) { success = false; exceptionMessage = getMessageForException(e); } steps.push({ step: 'fetch_buffer', url, time: Date.now() - fetchStartTime, success, exceptionMessage, }); if (!success || !data) { return { steps, result: { success: false, reason: 'fetch_failed' }, }; } // Step 2 - Decrypt the data let decryptedData; const decryptStartTime = Date.now(); try { const keyBytes = hexToUintArray(encryptionKey); const plaintext = await AES.decrypt(keyBytes, data); decryptedData = plaintext.byteLength > PADDING_THRESHOLD ? plaintext : unpad(plaintext); } catch (e) { success = false; exceptionMessage = getMessageForException(e); } steps.push({ step: 'decrypt_data', dataSize: decryptedData?.byteLength ?? -1, time: Date.now() - decryptStartTime, isPadded: data.byteLength > PADDING_THRESHOLD, success, exceptionMessage, }); if (!success || !decryptedData) { return { steps, result: { success: false, reason: 'decrypt_data_failed' } }; } // Step 3 - Create a blob from the decrypted data and return it const saveStartTime = Date.now(); const { mime } = fileInfoFromData(decryptedData); if (!mime) { return { steps, result: { success: false, reason: 'mime_check_failed', mime }, }; } let objectURL; try { invariant(mime, 'mime type should be defined'); const decryptedBlob = new Blob([decryptedData], { type: mime }); objectURL = URL.createObjectURL(decryptedBlob); } catch (e) { success = false; exceptionMessage = getMessageForException(e); } steps.push({ step: 'save_blob', objectURL, mimeType: mime, time: Date.now() - saveStartTime, success, exceptionMessage, }); if (!success || !objectURL) { return { steps, result: { success: false, reason: 'save_blob_failed' }, }; } return { steps, result: { success: true, uri: objectURL } }; } export { encryptFile, decryptMedia }; diff --git a/web/olm/olm-utils.js b/web/olm/olm-utils.js index 7345736ad..05ddbcee7 100644 --- a/web/olm/olm-utils.js +++ b/web/olm/olm-utils.js @@ -1,16 +1,29 @@ // @flow +import type { Utility } from '@commapp/olm'; import olm from '@commapp/olm'; declare var olmFilename: string; async function initOlm(): Promise { if (!olmFilename) { return await olm.init(); } const locateFile = (wasmFilename: string, httpAssetsHost: string) => httpAssetsHost + olmFilename; return await olm.init({ locateFile }); } -export { initOlm }; +let olmUtilityInstance; +function olmUtility(): Promise { + if (!olmUtilityInstance) { + olmUtilityInstance = (async () => { + await initOlm(); + olmUtilityInstance = new olm.Utility(); + return olmUtilityInstance; + })(); + } + return Promise.resolve(olmUtilityInstance); +} + +export { initOlm, olmUtility };