Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / lib / cache / crypto.ts
blob3b62e911a8126d245e863c4ba7d444cdeae56732
1 import { ARGON2_PARAMS, CryptoProxy } from '@proton/crypto/lib';
2 import { deriveKey } from '@proton/crypto/lib/subtle/aesGcm';
3 import { encryptData, generateKey, importSymmetricKey } from '@proton/pass/lib/crypto/utils/crypto-helpers';
4 import { PassCryptoError } from '@proton/pass/lib/crypto/utils/errors';
5 import { type Maybe, PassEncryptionTag } from '@proton/pass/types';
6 import { logger } from '@proton/pass/utils/logger';
7 import { stringToUint8Array, uint8ArrayToString } from '@proton/shared/lib/helpers/encoding';
9 type Argon2Params = (typeof ARGON2_PARAMS)[keyof typeof ARGON2_PARAMS];
11 export type OfflineConfig = { salt: string; params: Argon2Params };
13 export type OfflineComponents = {
14     /** Argon2 derivation of the user password. Kept encrypted
15      * in the persisted session blob. */
16     offlineKD: string;
17     /** The salt & argon2 parameters used to generated the offline
18      * key derivation. Defaults to `ARGON2_PARAMS.RECOMMENDED` */
19     offlineConfig: OfflineConfig;
20     /** A random 32 bytes string encrypted with the offlineKD.
21      * Allows verifying a local password via argon2 derivation. */
22     offlineVerifier: string;
25 export const CACHE_SALT_LENGTH = 32;
26 const HKDF_INFO = stringToUint8Array('pass-extension-cache-key'); // context identifier for domain separation
28 /**
29  * Get the key to use for local cache encryption. We use a HKDF derivation
30  * step since `keyPassword` has high entropy as it is salted using bcrypt.
31  * HKDF derivation would not be secure otherwise.
32  * @param salt - randomly generated salt of `SALT_LENGTH` size, ideally regenerated at each snapshot
33  * @returns key to use with `encryptData` and `decryptData`
34  */
35 export const getCacheEncryptionKey = async (
36     keyPassword: string,
37     salt: Uint8Array,
38     sessionLockToken: Maybe<string>
39 ): Promise<CryptoKey> => {
40     // We run a key derivation step (HKDF) to achieve domain separation (preventing the encrypted data to be
41     // decrypted outside of the booting context) and to better protect the password in case of e.g.
42     // future GCM key-recovery attacks.
43     // Since the password is already salted using bcrypt, we consider it entropic enough for HKDF: see
44     // discussion on key-stretching step in https://eprint.iacr.org/2010/264.pdf (Section 9).
45     const saltedUserPassword = `${keyPassword}${sessionLockToken ?? ''}`;
46     const passwordBytes = stringToUint8Array(saltedUserPassword);
48     const cacheEncryptionKey = await deriveKey(
49         passwordBytes,
50         salt,
51         HKDF_INFO,
52         { extractable: true } // exportable in order to re-encrypt for offline
53     );
55     return cacheEncryptionKey;
58 export const OFFLINE_ARGON2_PARAMS = ARGON2_PARAMS.RECOMMENDED;
60 /** Computes the raw bytes of the offline key by deriving it using an Argon2 algorithm
61  * from the encryption password and a randomly generated salt. The encryption password
62  * refers to the plain-text secondary password - typically the user's master password in
63  * one-password mode - for future-proofing against SSO support and two-password mode. */
64 export const getOfflineKeyDerivation = async (
65     loginPassword: string,
66     salt: Uint8Array,
67     params: Argon2Params = OFFLINE_ARGON2_PARAMS
68 ): Promise<Uint8Array> => CryptoProxy.computeArgon2({ params, password: loginPassword, salt });
70 /** Encrypts the raw cache key with the offline key */
71 export const encryptOfflineCacheKey = async (cacheKey: CryptoKey, offlineKD: Uint8Array): Promise<Uint8Array> => {
72     const rawCacheKey = await crypto.subtle.exportKey('raw', cacheKey);
73     const offlineKey = await importSymmetricKey(offlineKD);
75     return encryptData(offlineKey, new Uint8Array(rawCacheKey), PassEncryptionTag.Offline);
78 export const getOfflineVerifier = async (offlineKD: Uint8Array): Promise<string> => {
79     const offlineKey = await importSymmetricKey(offlineKD);
80     const verifier = generateKey();
81     const offlineVerifier = await encryptData(offlineKey, verifier, PassEncryptionTag.Offline);
83     return uint8ArrayToString(offlineVerifier);
86 export const getOfflineComponents = async (loginPassword: string): Promise<OfflineComponents> => {
87     const offlineSalt = crypto.getRandomValues(new Uint8Array(CACHE_SALT_LENGTH));
89     const offlineKD = await getOfflineKeyDerivation(loginPassword, offlineSalt).catch((error) => {
90         logger.warn(`[Argon2] Offline derivation error`, error);
91         throw new PassCryptoError('Argon2 failure');
92     });
94     return {
95         offlineConfig: { salt: uint8ArrayToString(offlineSalt), params: OFFLINE_ARGON2_PARAMS },
96         offlineKD: uint8ArrayToString(offlineKD),
97         offlineVerifier: await getOfflineVerifier(offlineKD),
98     };