feat(INDA-383): daily stats.
[ProtonMail-WebClient.git] / packages / shared / lib / recoveryFile / deviceRecovery.ts
blob902366cd4767123c1a09d4878f456ac15134c6a9
1 import type { PrivateKeyReference } from '@proton/crypto';
2 import { CryptoProxy } from '@proton/crypto';
3 import type { AuthenticationStore } from '@proton/shared/lib/authentication/createAuthenticationStore';
4 import arraysContainSameElements from '@proton/utils/arraysContainSameElements';
5 import isTruthy from '@proton/utils/isTruthy';
6 import noop from '@proton/utils/noop';
7 import uniqueBy from '@proton/utils/uniqueBy';
9 import { setNewRecoverySecret } from '../api/settingsRecovery';
10 import type { APP_NAMES } from '../constants';
11 import { getItem, removeItem, setItem } from '../helpers/storage';
12 import type {
13     Address,
14     Api,
15     DecryptedKey,
16     InactiveKey,
17     KeyPair,
18     PreAuthKTVerify,
19     User,
20     UserSettings,
21 } from '../interfaces';
22 import { getDecryptedAddressKeysHelper, getDecryptedUserKeysHelper, reactivateKeysProcess } from '../keys';
23 import {
24     getAllKeysReactivationRequests,
25     getInactiveKeys,
26     getInitialStates,
27     getLikelyHasKeysToReactivate,
28 } from '../keys/getInactiveKeys';
29 import type { KeyReactivationRequestStateData } from '../keys/reactivation/interface';
30 import {
31     generateRecoveryFileMessage,
32     generateRecoverySecret,
33     getIsRecoveryFileAvailable,
34     getKeyWithRecoverySecret,
35     getRecoverySecrets,
36     parseRecoveryFiles,
37     validateRecoverySecret,
38 } from './recoveryFile';
40 const getRecoveryMessageId = (userID: string) => `dr-${userID}`;
42 const setRecoveryMessage = (userID: string, recoveryMessage: string) => {
43     setItem(getRecoveryMessageId(userID), recoveryMessage);
46 const getRecoveryMessage = (userID: string) => {
47     return getItem(getRecoveryMessageId(userID));
50 export const getHasRecoveryMessage = (userID: string) => {
51     return !!getRecoveryMessage(userID);
53 export const removeDeviceRecovery = (userID: string) => {
54     removeItem(getRecoveryMessageId(userID));
57 export const getKeysFromDeviceRecovery = async (user: User) => {
58     const recoveryMessage = getRecoveryMessage(user.ID);
59     const recoverySecrets = getRecoverySecrets(user.Keys);
61     if (!recoveryMessage || !recoverySecrets.length) {
62         return;
63     }
65     const armouredKeys = await parseRecoveryFiles([recoveryMessage], recoverySecrets);
67     return Promise.all(
68         armouredKeys.map(({ armoredKey }) => CryptoProxy.importPrivateKey({ armoredKey, passphrase: null }))
69     );
72 export const attemptDeviceRecovery = async ({
73     user,
74     addresses,
75     keyPassword,
76     api,
77     preAuthKTVerify,
78 }: {
79     user: User;
80     addresses: Address[] | undefined;
81     keyPassword: string;
82     api: Api;
83     preAuthKTVerify: PreAuthKTVerify;
84 }) => {
85     const privateUser = Boolean(user.Private);
86     if (!addresses || !privateUser) {
87         return;
88     }
90     const hasKeysToReactivate = getLikelyHasKeysToReactivate(user, addresses);
91     if (!hasKeysToReactivate) {
92         return;
93     }
95     const userKeys = await getDecryptedUserKeysHelper(user, keyPassword);
96     const addressesKeys = await Promise.all(
97         addresses.map(async (address) => {
98             const keys = await getDecryptedAddressKeysHelper(address.Keys, user, userKeys, keyPassword);
99             return {
100                 address,
101                 keys,
102                 inactiveKeys: await getInactiveKeys(address.Keys, keys),
103             };
104         })
105     );
107     const inactiveKeys = {
108         user: await getInactiveKeys(user.Keys, userKeys),
109         addresses: addressesKeys.reduce<{ [key: string]: InactiveKey[] }>((acc, { address, inactiveKeys }) => {
110             acc[address.ID] = inactiveKeys;
111             return acc;
112         }, {}),
113     };
114     const allKeysToReactivate = getAllKeysReactivationRequests({
115         addresses,
116         user,
117         inactiveKeys,
118     });
119     const initialStates = getInitialStates(allKeysToReactivate);
120     const keys = await getKeysFromDeviceRecovery(user);
122     if (!keys) {
123         return;
124     }
126     const mapToUploadedPrivateKey = ({ id, Key, fingerprint }: KeyReactivationRequestStateData) => {
127         const uploadedPrivateKey = keys.find((decryptedBackupKey) => {
128             return fingerprint === decryptedBackupKey.getFingerprint();
129         });
130         if (!uploadedPrivateKey) {
131             return;
132         }
133         return {
134             id,
135             Key,
136             privateKey: uploadedPrivateKey,
137         };
138     };
140     const keyReactivationRecords = initialStates
141         .map((keyReactivationRecordState) => {
142             const uploadedKeysToReactivate = keyReactivationRecordState.keysToReactivate
143                 .map(mapToUploadedPrivateKey)
144                 .filter(isTruthy);
146             if (!uploadedKeysToReactivate.length) {
147                 return;
148             }
150             return {
151                 ...keyReactivationRecordState,
152                 keysToReactivate: uploadedKeysToReactivate,
153             };
154         })
155         .filter(isTruthy);
157     const keyTransparencyVerify = preAuthKTVerify(userKeys);
159     let numberOfReactivatedKeys = 0;
160     await reactivateKeysProcess({
161         api,
162         user,
163         userKeys,
164         addresses,
165         addressesKeys,
166         keyReactivationRecords,
167         keyPassword,
168         onReactivation: (_, result) => {
169             if (result === 'ok') {
170                 numberOfReactivatedKeys++;
171             }
172         },
173         keyTransparencyVerify,
174     });
176     return numberOfReactivatedKeys;
179 const storeRecoveryMessage = async ({
180     user,
181     userKeys,
182     recoverySecret,
183 }: {
184     user: User;
185     userKeys: KeyPair[];
186     recoverySecret: string;
187 }) => {
188     const currentDeviceRecoveryKeys = (await getKeysFromDeviceRecovery(user)) || [];
190     // Merge current device recovery keys with new keys to store. This way the act of storing device recovery information is not destructive.
191     const keysToStore = [...userKeys.map(({ privateKey }) => privateKey), ...currentDeviceRecoveryKeys];
192     const uniqueKeysToStore = uniqueBy(keysToStore, (key: PrivateKeyReference) => key.getFingerprint());
194     const recoveryMessage = await generateRecoveryFileMessage({ recoverySecret, privateKeys: uniqueKeysToStore });
195     setRecoveryMessage(user.ID, recoveryMessage);
198 export const storeDeviceRecovery = async ({
199     api,
200     user,
201     userKeys,
202 }: {
203     api: Api;
204     user: User;
205     userKeys: DecryptedKey[];
206 }) => {
207     const privateUser = Boolean(user.Private);
208     if (!privateUser) {
209         return;
210     }
212     const primaryUserKey = userKeys?.[0];
213     if (!primaryUserKey) {
214         return;
215     }
217     const primaryRecoverySecret = getKeyWithRecoverySecret(user.Keys.find((key) => key.ID === primaryUserKey.ID));
218     if (!primaryRecoverySecret) {
219         const { recoverySecret, signature } = await generateRecoverySecret(primaryUserKey.privateKey);
221         const silentApi = <T>(config: any) => api<T>({ ...config, silence: true });
222         await silentApi(
223             setNewRecoverySecret({
224                 RecoverySecret: recoverySecret,
225                 Signature: signature,
226             })
227         );
229         await storeRecoveryMessage({ user, userKeys, recoverySecret });
230         return true;
231     }
233     const valid = await validateRecoverySecret(primaryRecoverySecret, primaryUserKey.publicKey).catch(noop);
234     if (!valid) {
235         return;
236     }
238     await storeRecoveryMessage({
239         user,
240         userKeys,
241         recoverySecret: primaryRecoverySecret.RecoverySecret,
242     });
245 export const getIsDeviceRecoveryAvailable = getIsRecoveryFileAvailable;
247 export const getIsDeviceRecoveryEnabled = (userSettings: UserSettings, authentication: AuthenticationStore) => {
248     return userSettings.DeviceRecovery && authentication.getTrusted();
251 export const syncDeviceRecovery = async ({
252     api,
253     user,
254     userKeys,
255     userSettings,
256     appName,
257     addresses,
258     signal,
259     authentication,
260 }: {
261     api: Api;
262     user: User;
263     userKeys: DecryptedKey[];
264     userSettings: UserSettings;
265     appName: APP_NAMES;
266     addresses: Address[];
267     signal?: AbortSignal;
268     authentication: AuthenticationStore;
269 }) => {
270     const hasRecoveryMessage = getHasRecoveryMessage(user.ID);
271     const isDeviceRecoveryEnabled = getIsDeviceRecoveryEnabled(userSettings, authentication);
273     const shouldRemoveDeviceRecovery = hasRecoveryMessage && !isDeviceRecoveryEnabled;
274     if (shouldRemoveDeviceRecovery) {
275         removeDeviceRecovery(user.ID);
276         return;
277     }
279     const isRecoveryFileAvailable = getIsRecoveryFileAvailable({
280         user,
281         addresses,
282         userKeys,
283         appName,
284     });
285     const isDeviceRecoveryAvailable = authentication.getTrusted() && isRecoveryFileAvailable;
287     const privateKeyFingerPrints = userKeys?.map((key) => key.privateKey.getFingerprint()) || [];
289     const shouldStoreDeviceRecovery = isDeviceRecoveryAvailable && (isDeviceRecoveryEnabled || hasRecoveryMessage);
290     if (!privateKeyFingerPrints.length || !shouldStoreDeviceRecovery) {
291         return;
292     }
294     const storedKeys = (await getKeysFromDeviceRecovery(user)) || [];
295     if (signal?.aborted) {
296         return;
297     }
298     const storedKeyFingerprints = storedKeys.map((key) => key.getFingerprint());
299     const userKeysHaveUpdated = !arraysContainSameElements(storedKeyFingerprints, privateKeyFingerPrints);
301     if (!userKeysHaveUpdated) {
302         return;
303     }
305     await storeDeviceRecovery({ api, user, userKeys });
306     return true;