Use source loader for email sprite icons
[ProtonMail-WebClient.git] / packages / wallet / utils / crypto.ts
blob65ce1d39a961d00a9479b13559c7dc66e31cd203
1 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto/lib';
2 import { CryptoProxy, VERIFICATION_STATUS } from '@proton/crypto/lib';
3 import {
4     decryptData,
5     encryptData,
6     generateKey as generateAesGcmKey,
7     importKey as importAesGcmKey,
8 } from '@proton/crypto/lib/subtle/aesGcm';
9 import { signData as computeHmacSignature, importKey as importHmacKey } from '@proton/crypto/lib/subtle/hmac';
10 import { stringToUtf8Array } from '@proton/crypto/lib/utils';
11 import { base64StringToUint8Array, uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
12 import type { DecryptedKey } from '@proton/shared/lib/interfaces';
14 export enum WalletSignatureContextEnum {
15     WALLET_KEY = 'wallet.key',
16     BITCOIN_ADDRESS = 'wallet.bitcoin-address',
19 export type WalletSignatureContext = `${WalletSignatureContextEnum}`;
21 const encoder = new TextEncoder();
22 const decoder = new TextDecoder();
24 export type EncryptedWalletPart = Partial<
25     { mnemonic: string; publicKey: undefined } | { mnemonic: undefined; publicKey: string }
28 export const decryptTextData = async (armoredMessage: string, keys: PrivateKeyReference[]) => {
29     const { data } = await CryptoProxy.decryptMessage({
30         armoredMessage,
31         decryptionKeys: keys,
32         verificationKeys: keys,
33     });
35     return data;
38 type DecryptReturnType<T extends 'binary' | 'utf8'> = T extends 'binary' ? Uint8Array : string;
40 export const decryptPgp = async <T extends 'binary' | 'utf8'>(
41     armoredMessage: string,
42     format: T,
43     keys: PrivateKeyReference[]
44 ): Promise<DecryptReturnType<T>> => {
45     const { data } = await CryptoProxy.decryptMessage({
46         armoredMessage,
47         decryptionKeys: keys,
48         verificationKeys: keys,
49         format,
50     });
52     return data as DecryptReturnType<T>;
55 const isString = (data: string | Uint8Array): data is string => typeof data === 'string';
57 export const encryptPgp = async <T extends string | Uint8Array>(
58     data: T,
59     keys: PublicKeyReference[],
60     signingKeys?: PrivateKeyReference[]
61 ) => {
62     const common = {
63         encryptionKeys: keys,
64         signingKeys,
65         format: 'armored',
66     } as const;
68     let message: string;
69     if (isString(data)) {
70         message = await CryptoProxy.encryptMessage({
71             textData: data,
72             ...common,
73         }).then(({ message }) => message);
74     } else {
75         message = await CryptoProxy.encryptMessage({
76             binaryData: data as Uint8Array,
77             ...common,
78         }).then(({ message }) => message);
79     }
81     return message;
84 export const signData = async <T extends string | Uint8Array>(
85     data: T,
86     context: WalletSignatureContext,
87     keys: PrivateKeyReference[]
88 ) => {
89     const common = {
90         signingKeys: keys,
91         detached: true,
92         format: 'armored',
93         context: { critical: true, value: context },
94     } as const;
96     let signature;
97     if (isString(data)) {
98         signature = await CryptoProxy.signMessage({
99             textData: data,
100             ...common,
101         });
102     } else {
103         signature = await CryptoProxy.signMessage({
104             binaryData: data as Uint8Array,
105             ...common,
106         });
107     }
109     return signature;
112 export const verifySignedData = async <T extends string | Uint8Array>(
113     data: T,
114     signature: string,
115     context: WalletSignatureContext,
116     keys: (PrivateKeyReference | PublicKeyReference)[]
117 ) => {
118     const common = {
119         armoredSignature: signature,
120         verificationKeys: keys,
121         context: { required: true, value: context },
122     } as const;
124     if (isString(data)) {
125         return CryptoProxy.verifyMessage({
126             textData: data,
127             ...common,
128         }).then(({ verified }) => verified === VERIFICATION_STATUS.SIGNED_AND_VALID);
129     } else {
130         return CryptoProxy.verifyMessage({
131             binaryData: data as Uint8Array,
132             ...common,
133         }).then(({ verified }) => verified === VERIFICATION_STATUS.SIGNED_AND_VALID);
134     }
137 const decryptArmoredWalletKey = async (walletKey: string, walletKeySignature: string, keys: DecryptedKey[]) => {
138     const decryptedEntropy = await decryptPgp(
139         walletKey,
140         'binary',
141         keys.map((k) => k.privateKey)
142     );
144     const isKeyVerified = await verifySignedData(
145         decryptedEntropy,
146         walletKeySignature,
147         'wallet.key',
148         keys.map((k) => k.publicKey)
149     );
151     if (!isKeyVerified) {
152         throw new Error('Key could not be verified');
153     }
155     return decryptedEntropy;
158 export const decryptWalletKey = async (walletKey: string, walletKeySignature: string, keys: DecryptedKey[]) => {
159     const decryptedEntropy = await decryptArmoredWalletKey(walletKey, walletKeySignature, keys);
160     return importAesGcmKey(decryptedEntropy);
163 export const decryptWalletKeyForHmac = async (walletKey: string, walletKeySignature: string, keys: DecryptedKey[]) => {
164     const decryptedEntropy = await decryptArmoredWalletKey(walletKey, walletKeySignature, keys);
165     return importHmacKey(decryptedEntropy);
168 export const hmac = async (hmacKey: CryptoKey, data: string) => {
169     return computeHmacSignature(hmacKey, stringToUtf8Array(data));
173  * Encrypts a wallet's data with provided key
175  * @param dataToEncrypt an array containing the data to encrypt with the provided wallet key
176  * @param key key to use to encrypt wallet data
177  * @returns an array containing the data encrypted
178  */
179 export const encryptWalletDataWithWalletKey = async (dataToEncrypt: string[], key: CryptoKey): Promise<string[]> => {
180     const encryptedData = await Promise.all(
181         dataToEncrypt.map(async (data) => {
182             const binaryData = encoder.encode(data);
184             const encryptedMnemonic = await encryptData(key, binaryData);
185             return uint8ArrayToBase64String(encryptedMnemonic);
186         })
187     );
189     return encryptedData;
193  * Encrypts a wallet's data
195  * @param dataToEncrypt an array containing the data to encrypt with the generated wallet key
196  * @param userKey user key to use to encrypt generated wallet key
197  * @returns a tupple containing encrypted data and a nested tupple with the encrypted wallet key, its signature, the decrypted wallet key and the id of the user key used to encrypt the wallet key
198  */
199 export const encryptWalletData = async (
200     dataToEncrypt: string[],
201     userKey: DecryptedKey
202 ): Promise<[string[], [string, string, CryptoKey, string]]> => {
203     const secretEntropy = generateAesGcmKey();
204     const key = await importAesGcmKey(secretEntropy);
206     const encryptedData = await encryptWalletDataWithWalletKey(dataToEncrypt, key);
208     const entropySignature = await signData(secretEntropy, 'wallet.key', [userKey.privateKey]);
209     const encryptedEntropy = await encryptPgp(secretEntropy, [userKey.publicKey]);
211     return [encryptedData, [encryptedEntropy, entropySignature, key, userKey.ID]];
215  * Decrypts an array of data using provided wallet key
217  * @param dataToDecrypt an array containing wallet's data to decrypt with provided walletKey
218  * @param walletKey (a.k.a encrypted entropy) used to encrypt wallet's data
219  * @returns an array containing wallet's decrypted wallet
220  */
221 export const decryptWalletData = async (dataToDecrypt: (string | null)[], walletKey: CryptoKey) => {
222     const decryptedData = await Promise.all(
223         dataToDecrypt.map(async (data) => {
224             if (!data) {
225                 return null;
226             }
228             try {
229                 const decodedEncryptedMnemonic = base64StringToUint8Array(data);
231                 const decryptedBinaryMnemonic = await decryptData(walletKey, decodedEncryptedMnemonic);
232                 return decoder.decode(decryptedBinaryMnemonic);
233             } catch (e) {
234                 return null;
235             }
236         })
237     );
239     return decryptedData;
243  * Decrypts a mnemonic encrypted with user key
245  * @param encryptedMnemonic encrypted mnemonic with user key
246  * @param keys used to encrypt mnemonic
247  * @returns wallet's mnemonic encrypted with walletKey (a.k.a encrypted entropy)
248  */
249 export const decryptMnemonicWithUserKey = async (encryptedMnemonic: string | null, keys: DecryptedKey[]) => {
250     if (encryptedMnemonic === null) {
251         return null;
252     }
253     const { data: decryptedBinaryMnemonic } = await CryptoProxy.decryptMessage({
254         binaryMessage: base64StringToUint8Array(encryptedMnemonic),
255         decryptionKeys: keys.map((k) => k.privateKey),
256         format: 'binary',
257     });
259     return uint8ArrayToBase64String(decryptedBinaryMnemonic);