diff --git a/web/media/aes-crypto-utils.js b/web/media/aes-crypto-utils.js new file mode 100644 index 000000000..e65565596 --- /dev/null +++ b/web/media/aes-crypto-utils.js @@ -0,0 +1,74 @@ +// @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 };