1 import mergeUint8Arrays from '@proton/utils/mergeUint8Arrays';
3 export const KEY_LENGTH_BYTES = 32;
4 const IV_LENGTH_BYTES = 12;
5 export const ENCRYPTION_ALGORITHM = 'AES-GCM';
6 export type AesCryptoKey = CryptoKey;
8 type AesGcmKeyUsage = 'decrypt' | 'encrypt';
9 interface AesGcmKeyOptions {
10 /** allowed key operations */
11 keyUsage?: AesGcmKeyUsage[];
12 /** whether the key can be exported using `subtle.crypto.exportKey` */
13 extractable?: boolean;
17 * Import an AES-GCM key in order to use it with `encryptData` and `decryptData`.
19 export const importKey = async (
21 keyUsage: AesGcmKeyUsage[] = ['decrypt', 'encrypt']
22 ): Promise<AesCryptoKey> => {
23 return crypto.subtle.importKey('raw', key, ENCRYPTION_ALGORITHM, false, keyUsage);
27 * Generate key (bytes) for AES-GCM.
28 * The key needs to be imported using `importKey` to used for `encryptData` and `decryptData`.
30 export const generateKey = (): Uint8Array => crypto.getRandomValues(new Uint8Array(KEY_LENGTH_BYTES));
33 * Use HKDF to derive AES-GCM key material from some high-entropy secret.
34 * NB: this is NOT designed to derive keys from relatively low-entropy inputs such as passwords.
35 * @param highEntropySecret - input key material for HKDF
36 * @param salt - HKDF salt
37 * @param info - context or application specific information to bind to the derived key
39 export const deriveKey = async (
40 highEntropySecret: Uint8Array,
43 { keyUsage = ['decrypt', 'encrypt'], extractable = false }: AesGcmKeyOptions = {}
45 // This is meant more as a sanity check than a security safe-guard, since entropy might still be
46 // too low even for longer inputs
47 if (highEntropySecret.length < 16) {
48 throw new Error('Unexpected HKDF input size: secret input is too short');
51 const inputKeyMaterial = await crypto.subtle.importKey('raw', highEntropySecret, 'HKDF', false, ['deriveKey']);
53 return crypto.subtle.deriveKey(
54 { name: 'HKDF', salt, info, hash: 'SHA-256' },
56 { name: ENCRYPTION_ALGORITHM, length: KEY_LENGTH_BYTES * 8 },
63 * Encrypt data using AES-GCM
64 * @param key - WebCrypto key for encryption
65 * @param data - data to encrypt
66 * @param additionalData - additional data to authenticate
68 export const encryptData = async (key: AesCryptoKey, data: Uint8Array, additionalData?: Uint8Array) => {
69 const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH_BYTES));
70 const ciphertext = await crypto.subtle.encrypt(
71 { name: ENCRYPTION_ALGORITHM, iv, ...(additionalData !== undefined ? { additionalData } : undefined) },
76 return mergeUint8Arrays([iv, new Uint8Array(ciphertext)]);
80 * Decrypt data using AES-GCM
81 * @param key - WebCrypto key for decryption
82 * @param data - ciphertext to decrypt
83 * @param additionalData - additional authenticated data
84 * @param with16ByteIV - whether a non-standard IV size of 16 bytes was used on encryption
86 export const decryptData = async (
89 additionalData?: Uint8Array,
92 const ivLength = with16ByteIV ? 16 : IV_LENGTH_BYTES;
93 const iv = data.slice(0, ivLength);
94 const ciphertext = data.slice(ivLength, data.length);
95 const result = await crypto.subtle.decrypt(
96 { name: ENCRYPTION_ALGORITHM, iv, ...(additionalData !== undefined ? { additionalData } : undefined) },
101 return new Uint8Array(result);
105 * DEPRECATED: use `encryptData` instead.
106 * This function encrypts using a non-standard IV of 16 bytes.
107 * @param key - WebCrypto key for encryption
108 * @param data - data to encrypt
109 * @param additionalData - additional data to authenticate
110 * @deprecated use `encryptData` instead; this helper is kept around for legacy use-cases only.
112 export const encryptDataWith16ByteIV = async (key: AesCryptoKey, data: Uint8Array, additionalData?: Uint8Array) => {
114 // A random 16-byte IV is non-standard, but it does not negatively affect the max number of encryptable messages using the same key,
115 // nor does it increase the risk of nonce collision; see summary at https://crypto.stackexchange.com/a/80390.
116 const iv = crypto.getRandomValues(new Uint8Array(ivLength));
117 const ciphertext = await crypto.subtle.encrypt(
118 { name: ENCRYPTION_ALGORITHM, iv, ...(additionalData !== undefined ? { additionalData } : undefined) },
123 return mergeUint8Arrays([iv, new Uint8Array(ciphertext)]);