diff --git a/web/account/qr-code-login.react.js b/web/account/qr-code-login.react.js --- a/web/account/qr-code-login.react.js +++ b/web/account/qr-code-login.react.js @@ -4,10 +4,10 @@ import * as React from 'react'; import { qrCodeLinkURL } from 'lib/facts/links.js'; +import { generateKeyCommon } from 'lib/media/aes-crypto-utils-common.js'; import { uintArrayToHexString } from 'lib/media/data-utils.js'; import css from './qr-code-login.css'; -import { generateKey } from '../media/aes-crypto-utils.js'; import { useSelector } from '../redux/redux-utils.js'; function QrCodeLogin(): React.Node { @@ -22,7 +22,7 @@ return; } - const rawAESKey: Uint8Array = await generateKey(); + const rawAESKey: Uint8Array = await generateKeyCommon(crypto); const aesKeyAsHexString: string = uintArrayToHexString(rawAESKey); const url = qrCodeLinkURL(aesKeyAsHexString, ed25519Key); diff --git a/web/media/aes-crypto-utils.js b/web/media/aes-crypto-utils.js deleted file mode 100644 --- a/web/media/aes-crypto-utils.js +++ /dev/null @@ -1,74 +0,0 @@ -// @flow - -const KEY_SIZE = 32; // bytes -const IV_LENGTH = 12; // bytes - unique Initialization Vector (nonce) -const TAG_LENGTH = 16; // bytes - GCM auth tag - -async function generateKey(): Promise { - const algorithm = { name: 'AES-GCM', length: 256 }; - const key = await crypto.subtle.generateKey(algorithm, true, [ - 'encrypt', - 'decrypt', - ]); - const keyData = await crypto.subtle.exportKey('raw', key); - return new Uint8Array(keyData); -} - -async function encrypt( - keyBytes: Uint8Array, - plaintext: Uint8Array, -): Promise { - if (keyBytes.length !== KEY_SIZE) { - throw new Error('Invalid AES key size'); - } - - // we're creating the buffer now so we can avoid reallocating it later - const outputBuffer = new ArrayBuffer( - plaintext.length + IV_LENGTH + TAG_LENGTH, - ); - const ivBytes = new Uint8Array(outputBuffer, 0, IV_LENGTH); - const iv = crypto.getRandomValues(ivBytes); - - const algorithm = { name: 'AES-GCM', iv: iv, tagLength: TAG_LENGTH * 8 }; - const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, [ - 'encrypt', - ]); - const ciphertextWithTag = await crypto.subtle.encrypt( - algorithm, - key, - plaintext, - ); - - const result = new Uint8Array(outputBuffer); - result.set(new Uint8Array(ciphertextWithTag), iv.length); - return result; -} - -async function decrypt( - keyBytes: Uint8Array, - sealedData: Uint8Array, -): Promise { - if (keyBytes.length !== KEY_SIZE) { - throw new Error('Invalid AES key size'); - } - if (sealedData.length < IV_LENGTH + TAG_LENGTH) { - throw new Error('Invalid ciphertext size'); - } - - const iv = sealedData.subarray(0, IV_LENGTH); - const ciphertextWithTag = sealedData.subarray(IV_LENGTH); - - const algorithm = { name: 'AES-GCM', iv, tagLength: TAG_LENGTH * 8 }; - const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, [ - 'decrypt', - ]); - - const plaintextBuffer = await crypto.subtle.decrypt( - algorithm, - key, - ciphertextWithTag, - ); - return new Uint8Array(plaintextBuffer); -} - -export { generateKey, encrypt, decrypt }; diff --git a/web/media/aes-crypto-utils.test.js b/web/media/aes-crypto-utils.test.js --- a/web/media/aes-crypto-utils.test.js +++ b/web/media/aes-crypto-utils.test.js @@ -1,6 +1,10 @@ // @flow -import { generateKey, encrypt, decrypt } from './aes-crypto-utils.js'; +import { + generateKeyCommon, + encryptCommon, + decryptCommon, +} from 'lib/media/aes-crypto-utils-common.js'; // some mock data const testPlaintext = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); @@ -20,69 +24,77 @@ describe('generateKey', () => { it('generates 32-byte AES key', async () => { - const key = await generateKey(); + const key = await generateKeyCommon(crypto); expect(key.length).toBe(32); }); }); describe('encrypt', () => { it('generates ciphertext with IV and tag included', async () => { - const encrypted = await encrypt(testEncryptionKey, testPlaintext); + const encrypted = await encryptCommon( + crypto, + testEncryptionKey, + testPlaintext, + ); // IV and tag are randomly generated, so we can't check the exact value // IV + plaintext + tag = 12 + 11 + 16 = 39 expect(encrypted.length).toBe(testPlaintext.length + 12 + 16); }); it('is decryptable by decrypt()', async () => { - const key = await generateKey(); - const encrypted = await encrypt(key, randomData); - const decrypted = await decrypt(key, encrypted); + const key = await generateKeyCommon(crypto); + const encrypted = await encryptCommon(crypto, key, randomData); + const decrypted = await decryptCommon(crypto, key, encrypted); expect(decrypted).toEqual(randomData); }); }); describe('decrypt', () => { it('decrypts ciphertext', async () => { - const decrypted = await decrypt(testEncryptionKey, testSealedData); + const decrypted = await decryptCommon( + crypto, + testEncryptionKey, + testSealedData, + ); expect(decrypted).toEqual(testPlaintext); }); it('fails with wrong key', async () => { - const key = await generateKey(); - const encrypted = await encrypt(key, randomData); + const key = await generateKeyCommon(crypto); + const encrypted = await encryptCommon(crypto, key, randomData); - const wrongKey = await generateKey(); - await expect(decrypt(wrongKey, encrypted)).rejects.toThrow(); + const wrongKey = await generateKeyCommon(crypto); + await expect(decryptCommon(crypto, wrongKey, encrypted)).rejects.toThrow(); }); it('fails with wrong ciphertext', async () => { - const key = await generateKey(); - const encrypted = await encrypt(key, randomData); + const key = await generateKeyCommon(crypto); + const encrypted = await encryptCommon(crypto, key, randomData); // change the first byte of the ciphertext (it's 13th byte in the buffer) // first 12 bytes are IV, so changing the first byte of the ciphertext encrypted[12] = encrypted[12] ^ 1; - await expect(decrypt(key, encrypted)).rejects.toThrow(); + await expect(decryptCommon(crypto, key, encrypted)).rejects.toThrow(); }); it('fails with wrong IV', async () => { - const key = await generateKey(); - const encrypted = await encrypt(key, randomData); + const key = await generateKeyCommon(crypto); + const encrypted = await encryptCommon(crypto, key, randomData); // change the first byte of the IV (it's 1st byte in the buffer) encrypted[0] = encrypted[0] ^ 1; - await expect(decrypt(key, encrypted)).rejects.toThrow(); + await expect(decryptCommon(crypto, key, encrypted)).rejects.toThrow(); }); it('fails with wrong tag', async () => { - const key = await generateKey(); - const encrypted = await encrypt(key, randomData); + const key = await generateKeyCommon(crypto); + const encrypted = await encryptCommon(crypto, key, randomData); // change the last byte of the tag (tag is the last 16 bytes of the buffer) encrypted[encrypted.length - 1] = encrypted[encrypted.length - 1] ^ 1; - await expect(decrypt(key, encrypted)).rejects.toThrow(); + await expect(decryptCommon(crypto, key, encrypted)).rejects.toThrow(); }); }); diff --git a/web/media/encryption-utils.js b/web/media/encryption-utils.js --- a/web/media/encryption-utils.js +++ b/web/media/encryption-utils.js @@ -3,6 +3,7 @@ import invariant from 'invariant'; import { thumbHashToDataURL } from 'thumbhash'; +import * as AES from 'lib/media/aes-crypto-utils-common.js'; 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'; @@ -13,7 +14,6 @@ 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'; import { base64DecodeBuffer } from '../utils/base64-utils.js'; const PADDING_THRESHOLD = 5000000; // 5MB @@ -61,8 +61,8 @@ let key, encryptedData, sha256; try { const plaintextData = shouldPad ? pad(data) : data; - key = await AES.generateKey(); - encryptedData = await AES.encrypt(key, plaintextData); + key = await AES.generateKeyCommon(crypto); + encryptedData = await AES.encryptCommon(crypto, key, plaintextData); const hashBytes = await crypto.subtle.digest('SHA-256', encryptedData); sha256 = btoa(String.fromCharCode(...new Uint8Array(hashBytes))); @@ -182,7 +182,7 @@ const decryptStartTime = Date.now(); try { const keyBytes = hexToUintArray(encryptionKey); - const plaintext = await AES.decrypt(keyBytes, data); + const plaintext = await AES.decryptCommon(crypto, keyBytes, data); decryptedData = plaintext.byteLength > PADDING_THRESHOLD ? plaintext : unpad(plaintext); } catch (e) { @@ -243,7 +243,8 @@ keyHex: string, ): Promise { const encryptedData = base64DecodeBuffer(encryptedThumbHash); - const thumbhashBytes = await AES.decrypt( + const thumbhashBytes = await AES.decryptCommon( + crypto, hexToUintArray(keyHex), encryptedData, ); diff --git a/web/media/image-utils.js b/web/media/image-utils.js --- a/web/media/image-utils.js +++ b/web/media/image-utils.js @@ -3,6 +3,7 @@ import EXIF from 'exif-js'; import { rgbaToThumbHash } from 'thumbhash'; +import * as AES from 'lib/media/aes-crypto-utils-common.js'; import { hexToUintArray } from 'lib/media/data-utils.js'; import type { GetOrientationMediaMissionStep, @@ -11,7 +12,6 @@ } from 'lib/types/media-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; -import * as AES from './aes-crypto-utils.js'; import { preloadImage } from './media-utils.js'; import { base64EncodeBuffer } from '../utils/base64-utils.js'; @@ -103,7 +103,8 @@ if (encryptionKey) { try { - const encryptedThumbHash = await AES.encrypt( + const encryptedThumbHash = await AES.encryptCommon( + crypto, hexToUintArray(encryptionKey), binaryThumbHash, );