Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / recoveryFile / recoveryFile.ts
blob2d1247ba450b57cb139e0dcbbde647689860e0bd
1 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto';
2 import { CryptoProxy, VERIFICATION_STATUS } from '@proton/crypto';
3 import { uint8ArrayToBase64String } from '@proton/shared/lib/helpers/encoding';
4 import isTruthy from '@proton/utils/isTruthy';
5 import mergeUint8Arrays from '@proton/utils/mergeUint8Arrays';
7 import type { APP_NAMES } from '../constants';
8 import { APPS, RECOVERY_FILE_FILE_NAME } from '../constants';
9 import downloadFile from '../helpers/downloadFile';
10 import type { Address, DecryptedKey, Key, KeyWithRecoverySecret, User } from '../interfaces';
11 import type { ArmoredKeyWithInfo } from '../keys';
12 import { getHasMigratedAddressKeys, getPrimaryKey } from '../keys';
14 const decryptRecoveryFile = (recoverySecrets: KeyWithRecoverySecret[]) => async (file: string) => {
15     try {
16         return await Promise.any(
17             recoverySecrets.map(async ({ RecoverySecret }) => {
18                 const { data } = await CryptoProxy.decryptMessage({
19                     armoredMessage: file,
20                     passwords: RecoverySecret,
21                     format: 'binary',
22                 });
24                 return data;
25             })
26         );
27     } catch (error: any) {
28         return undefined;
29     }
32 export const parseRecoveryFiles = async (filesAsStrings: string[] = [], recoverySecrets: KeyWithRecoverySecret[]) => {
33     const decryptedFiles = (await Promise.all(filesAsStrings.map(decryptRecoveryFile(recoverySecrets)))).filter(
34         isTruthy
35     );
36     const decryptedArmoredKeys = (
37         await Promise.all(
38             decryptedFiles.map((concatenatedBinaryKeys) =>
39                 CryptoProxy.getArmoredKeys({ binaryKeys: concatenatedBinaryKeys })
40             )
41         )
42     ).flat();
44     return Promise.all(
45         decryptedArmoredKeys.map(
46             async (armoredKey): Promise<ArmoredKeyWithInfo> => ({
47                 ...(await CryptoProxy.getKeyInfo({ armoredKey })),
48                 armoredKey,
49             })
50         )
51     );
54 export const generateRecoverySecret = async (privateKey: PrivateKeyReference) => {
55     const length = 32;
56     const randomValues = crypto.getRandomValues(new Uint8Array(length));
57     const recoverySecret = uint8ArrayToBase64String(randomValues);
59     const signature = await CryptoProxy.signMessage({
60         textData: recoverySecret,
61         stripTrailingSpaces: true,
62         signingKeys: privateKey,
63         detached: true,
64     });
66     return {
67         signature,
68         recoverySecret,
69     };
72 export const generateRecoveryFileMessage = async ({
73     recoverySecret,
74     privateKeys,
75 }: {
76     recoverySecret: string;
77     privateKeys: PrivateKeyReference[];
78 }) => {
79     const userKeysArray = await Promise.all(
80         privateKeys.map((privateKey) =>
81             CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: null, format: 'binary' })
82         )
83     );
85     const { message } = await CryptoProxy.encryptMessage({
86         binaryData: mergeUint8Arrays(userKeysArray),
87         passwords: [recoverySecret],
88     });
90     return message;
93 export const exportRecoveryFile = async ({
94     recoverySecret,
95     userKeys,
96 }: {
97     recoverySecret: string;
98     userKeys: DecryptedKey[];
99 }) => {
100     const message = await generateRecoveryFileMessage({
101         recoverySecret,
102         privateKeys: userKeys.map(({ privateKey }) => privateKey),
103     });
104     const blob = new Blob([message], { type: 'text/plain' });
105     downloadFile(blob, RECOVERY_FILE_FILE_NAME);
108 export const validateRecoverySecret = async (recoverySecret: KeyWithRecoverySecret, publicKey: PublicKeyReference) => {
109     const { RecoverySecret, RecoverySecretSignature } = recoverySecret;
111     const { verified } = await CryptoProxy.verifyMessage({
112         textData: RecoverySecret,
113         stripTrailingSpaces: true,
114         verificationKeys: publicKey,
115         armoredSignature: RecoverySecretSignature,
116     });
118     return verified === VERIFICATION_STATUS.SIGNED_AND_VALID;
121 export const getKeyWithRecoverySecret = (key: Key | undefined) => {
122     if (!key?.RecoverySecret || !key?.RecoverySecretSignature) {
123         return;
124     }
125     return key as KeyWithRecoverySecret;
128 export const getRecoverySecrets = (Keys: Key[] = []): KeyWithRecoverySecret[] => {
129     return Keys.map(getKeyWithRecoverySecret).filter(isTruthy);
132 export const getPrimaryRecoverySecret = (Keys: Key[] = []): KeyWithRecoverySecret | undefined => {
133     return getKeyWithRecoverySecret(Keys?.[0]);
136 export const getHasOutdatedRecoveryFile = (keys: Key[] = []) => {
137     const primaryRecoverySecret = getPrimaryRecoverySecret(keys);
138     const recoverySecrets = getRecoverySecrets(keys);
140     return recoverySecrets?.length > 0 && !primaryRecoverySecret;
143 export const getIsRecoveryFileAvailable = ({
144     user,
145     addresses,
146     userKeys,
147     appName,
148 }: {
149     user: User;
150     addresses: Address[];
151     userKeys: DecryptedKey[];
152     appName: APP_NAMES;
153 }) => {
154     const hasMigratedKeys = getHasMigratedAddressKeys(addresses);
155     const primaryKey = getPrimaryKey(userKeys);
157     const isPrivateUser = Boolean(user.Private);
159     return !!primaryKey?.privateKey && hasMigratedKeys && isPrivateUser && appName !== APPS.PROTONVPN_SETTINGS;