Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / keys / reactivation / reactivateKeysProcessV2.ts
blob2e312262aed47707fdb7ecb2829b72ac63845522
1 import type { PrivateKeyReference, PrivateKeyReferenceV4, PrivateKeyReferenceV6 } from '@proton/crypto';
2 import { CryptoProxy } from '@proton/crypto';
3 import { getDefaultKeyFlags } from '@proton/shared/lib/keys';
5 import { getApiError } from '../../api/helpers/apiErrorHelper';
6 import { reactivateUserKeyRouteV2, reactiveLegacyAddressKeyRouteV2 } from '../../api/keys';
7 import { HTTP_STATUS_CODE } from '../../constants';
8 import type {
9     ActiveKeyWithVersion,
10     ActiveAddressKeysByVersion
11 } from '../../interfaces';
12 import {
13     type ActiveKey,
14     type Address,
15     type Api,
16     type DecryptedKey,
17     type KeyTransparencyVerify,
18     isActiveKeyV6,
19     type Address as tsAddress,
20     type User as tsUser,
21 } from '../../interfaces';
22 import type { SimpleMap } from '../../interfaces/utils';
23 import { generateAddressKeyTokens } from '../addressKeys';
24 import {
25     getActiveAddressKeys,
26     getActiveKeyObject,
27     getActiveUserKeys,
28     getNormalizedActiveAddressKeys,
29     getNormalizedActiveUserKeys,
30     getPrimaryFlag,
31     getReactivatedKeyFlag,
32 } from '../getActiveKeys';
33 import { getDecryptedAddressKeysHelper } from '../getDecryptedAddressKeys';
34 import { getPrimaryKey } from '../getPrimaryKey';
35 import { getHasMigratedAddressKey } from '../keyMigration';
36 import { getSignedKeyListWithDeferredPublish } from '../signedKeyList';
37 import type { KeyReactivationData, KeyReactivationRecord, OnKeyReactivationCallback } from './interface';
38 import { getAddressReactivationPayload, getReactivatedAddressesKeys, resetUserId } from './reactivateKeyHelper';
40 interface ReactivateUserKeysArguments {
41     addressRecordsInV2Format: KeyReactivationRecord[];
42     api: Api;
43     addresses: Address[];
44     user: tsUser;
45     activeKeys: ActiveKey[]; // user keys do not need to be differentiated by key version
46     onReactivation: OnKeyReactivationCallback;
47     keysToReactivate: KeyReactivationData[];
48     keyPassword: string;
49     keyTransparencyVerify: KeyTransparencyVerify;
52 export const reactivateUserKeys = async ({
53     addressRecordsInV2Format,
54     api,
55     addresses,
56     user,
57     activeKeys,
58     keysToReactivate,
59     onReactivation,
60     keyPassword,
61     keyTransparencyVerify,
62 }: ReactivateUserKeysArguments) => {
63     const keyReactivationDataMap = addressRecordsInV2Format.reduce<SimpleMap<KeyReactivationData>>((acc, record) => {
64         record.keysToReactivate.forEach((keyData) => {
65             acc[keyData.Key.ID] = keyData;
66         });
67         return acc;
68     }, {});
69     const reactivatedAddressKeysMap: SimpleMap<boolean> = {};
70     const allReactivatedAddressKeysMap: SimpleMap<boolean> = {};
72     let mutableActiveKeys = activeKeys;
73     let mutableAddresses = addresses;
75     for (const keyToReactivate of keysToReactivate) {
76         const { id, Key, privateKey: reactivatedKey } = keyToReactivate;
77         const { ID } = Key;
78         try {
79             if (!reactivatedKey) {
80                 throw new Error('Missing key');
81             }
83             await resetUserId(Key, reactivatedKey);
85             const privateKeyArmored = await CryptoProxy.exportPrivateKey({
86                 privateKey: reactivatedKey,
87                 passphrase: keyPassword,
88             });
89             const newActiveKey = await getActiveKeyObject(
90                 reactivatedKey as PrivateKeyReferenceV4 | PrivateKeyReferenceV6,
91                 {
92                     ID,
93                     primary: getPrimaryFlag(mutableActiveKeys),
94                     flags: getDefaultKeyFlags(undefined),
95                 }
96             );
97             const updatedActiveKeys = getNormalizedActiveUserKeys(undefined, [...mutableActiveKeys, newActiveKey]);
99             const reactivatedAddressKeysResult = await getReactivatedAddressesKeys({
100                 addresses,
101                 oldUserKeys: mutableActiveKeys,
102                 newUserKeys: updatedActiveKeys,
103                 user,
104                 keyPassword,
105                 keyTransparencyVerify,
106             });
107             const addressReactivationPayload = getAddressReactivationPayload(reactivatedAddressKeysResult);
108             mutableAddresses = mutableAddresses.map((address) => {
109                 const updatedSignedKeyList = addressReactivationPayload.SignedKeyLists[address.ID];
110                 if (updatedSignedKeyList) {
111                     return {
112                         ...address,
113                         SignedKeyList: {
114                             ...updatedSignedKeyList,
115                             MinEpochID: null,
116                             MaxEpochID: null,
117                         },
118                     };
119                 }
120                 return address;
121             });
122             await api(
123                 reactivateUserKeyRouteV2({
124                     ID,
125                     PrivateKey: privateKeyArmored,
126                     ...addressReactivationPayload,
127                 })
128             );
129             // Only once the SKLs have been successfully posted we add it to the KT commit state.
130             await Promise.all(
131                 reactivatedAddressKeysResult.map(({ onSKLPublishSuccess }) =>
132                     onSKLPublishSuccess ? onSKLPublishSuccess() : Promise.resolve()
133                 )
134             );
136             mutableActiveKeys = updatedActiveKeys;
138             onReactivation(id, 'ok');
140             // Notify all the address keys that got reactivated from this user key
141             reactivatedAddressKeysResult.forEach(({ reactivatedKeys }) => {
142                 reactivatedKeys?.forEach(({ ID }) => {
143                     allReactivatedAddressKeysMap[ID] = true;
144                     const reactivationData = keyReactivationDataMap[ID];
145                     if (reactivationData) {
146                         onReactivation(reactivationData.id, 'ok');
147                         reactivatedAddressKeysMap[reactivationData.id] = true;
148                     }
149                 });
150             });
151         } catch (e: any) {
152             onReactivation(id, e);
153             const { status } = getApiError(e);
154             if (status === HTTP_STATUS_CODE.FORBIDDEN) {
155                 // The password prompt has been cancelled. No need to attempt to reactivate the other keys.
156                 break;
157             }
158         }
159     }
161     addressRecordsInV2Format.forEach(({ keysToReactivate }) => {
162         keysToReactivate.forEach(({ id, privateKey }) => {
163             if (!reactivatedAddressKeysMap[id] && !privateKey) {
164                 onReactivation(id, new Error('User key inactivate'));
165             }
166         });
167     });
169     return {
170         userKeys: mutableActiveKeys,
171         addresses: mutableAddresses,
172         allReactivatedAddressKeysMap,
173     };
176 interface ReactivateAddressKeysV2Arguments {
177     api: Api;
178     address: Address;
179     activeKeys: ActiveAddressKeysByVersion;
180     userKey: PrivateKeyReference;
181     onReactivation: OnKeyReactivationCallback;
182     keysToReactivate: KeyReactivationData[];
183     keyTransparencyVerify: KeyTransparencyVerify;
186 export const reactivateAddressKeysV2 = async ({
187     api,
188     address,
189     activeKeys,
190     keysToReactivate,
191     onReactivation,
192     userKey,
193     keyTransparencyVerify,
194 }: ReactivateAddressKeysV2Arguments) => {
195     let mutableActiveKeys = activeKeys;
197     for (const keyToReactivate of keysToReactivate) {
198         const { id, Key, privateKey: reactivatedKey } = keyToReactivate;
199         const { ID, Flags } = Key;
200         try {
201             if (!reactivatedKey) {
202                 throw new Error('Missing key');
203             }
205             await resetUserId(Key, reactivatedKey);
207             const { token, encryptedToken, signature } = await generateAddressKeyTokens(userKey);
208             const privateKeyArmored = await CryptoProxy.exportPrivateKey({
209                 privateKey: reactivatedKey,
210                 passphrase: token,
211             });
212             const newActiveKey = (await getActiveKeyObject(
213                 reactivatedKey as PrivateKeyReferenceV4 | PrivateKeyReferenceV6,
214                 {
215                     ID,
216                     // We do not automatically set a v6 key as primary, because it will fail if forwarding is enabled for the address
217                     primary: reactivatedKey.isPrivateKeyV6() ? 0 : getPrimaryFlag(mutableActiveKeys.v4),
218                     flags: getReactivatedKeyFlag(address, Flags),
219                 }
220             )) as ActiveKeyWithVersion;
221             const toNormalize = isActiveKeyV6(newActiveKey)
222                 ? { v4: mutableActiveKeys.v4, v6: [...mutableActiveKeys.v6, newActiveKey] }
223                 : { v4: [...mutableActiveKeys.v4, newActiveKey], v6: mutableActiveKeys.v6 };
224             const updatedActiveKeys = getNormalizedActiveAddressKeys(address, toNormalize);
225             const [signedKeyList, onSKLPublishSuccess] = await getSignedKeyListWithDeferredPublish(
226                 updatedActiveKeys,
227                 address,
228                 keyTransparencyVerify
229             );
230             await api(
231                 reactiveLegacyAddressKeyRouteV2({
232                     ID,
233                     PrivateKey: privateKeyArmored,
234                     SignedKeyList: signedKeyList,
235                     Token: encryptedToken,
236                     Signature: signature,
237                 })
238             );
239             await onSKLPublishSuccess();
241             mutableActiveKeys = updatedActiveKeys;
243             onReactivation(id, 'ok');
244         } catch (e: any) {
245             onReactivation(id, e);
246         }
247     }
249     return mutableActiveKeys;
252 export interface ReactivateKeysProcessV2Arguments {
253     api: Api;
254     user: tsUser;
255     keyPassword: string;
256     addresses: tsAddress[];
257     userKeys: DecryptedKey[];
258     keyReactivationRecords: KeyReactivationRecord[];
259     onReactivation: OnKeyReactivationCallback;
260     keyTransparencyVerify: KeyTransparencyVerify;
263 const reactivateKeysProcessV2 = async ({
264     api,
265     user,
266     keyReactivationRecords,
267     onReactivation,
268     keyPassword,
269     addresses: oldAddresses,
270     userKeys: oldUserKeys,
271     keyTransparencyVerify,
272 }: ReactivateKeysProcessV2Arguments) => {
273     const { userRecord, addressRecordsInV2Format, addressRecordsInLegacyFormatOrWithBackup } =
274         keyReactivationRecords.reduce<{
275             addressRecordsInV2Format: KeyReactivationRecord[];
276             addressRecordsInLegacyFormatOrWithBackup: KeyReactivationRecord[];
277             userRecord?: KeyReactivationRecord;
278         }>(
279             (acc, record) => {
280                 const { user, address, keysToReactivate } = record;
281                 if (user) {
282                     acc.userRecord = record;
283                 }
284                 if (address) {
285                     const keysInV2Format = keysToReactivate.filter(
286                         ({ privateKey, Key }) => !privateKey && getHasMigratedAddressKey(Key)
287                     );
288                     const keysInLegacyFormatOrWithBackup = keysToReactivate.filter(
289                         ({ privateKey, Key }) => privateKey || !getHasMigratedAddressKey(Key)
290                     );
292                     if (keysInV2Format.length) {
293                         acc.addressRecordsInV2Format.push({
294                             address,
295                             keysToReactivate: keysInV2Format,
296                         });
297                     }
299                     if (keysInLegacyFormatOrWithBackup.length) {
300                         acc.addressRecordsInLegacyFormatOrWithBackup.push({
301                             address,
302                             keysToReactivate: keysInLegacyFormatOrWithBackup,
303                         });
304                     }
305                 }
306                 return acc;
307             },
309             { addressRecordsInV2Format: [], addressRecordsInLegacyFormatOrWithBackup: [], userRecord: undefined }
310         );
312     let userKeys = oldUserKeys;
313     let addresses = oldAddresses;
314     let allReactivatedAddressKeysMap: SimpleMap<boolean> = {};
315     if (userRecord) {
316         try {
317             const activeUserKeys = await getActiveUserKeys(user.Keys, userKeys);
318             const userKeysReactivationResult = await reactivateUserKeys({
319                 api,
320                 user,
321                 activeKeys: activeUserKeys,
322                 addresses: oldAddresses,
323                 keysToReactivate: userRecord.keysToReactivate,
324                 onReactivation,
325                 keyPassword,
326                 addressRecordsInV2Format,
327                 keyTransparencyVerify,
328             });
329             userKeys = userKeysReactivationResult.userKeys;
330             addresses = userKeysReactivationResult.addresses;
331             allReactivatedAddressKeysMap = userKeysReactivationResult.allReactivatedAddressKeysMap;
332         } catch (e: any) {
333             userRecord.keysToReactivate.forEach(({ id }) => onReactivation(id, e));
334             addressRecordsInV2Format.forEach(({ keysToReactivate }) => {
335                 keysToReactivate.forEach(({ id }) => onReactivation(id, e));
336             });
337         }
338     }
340     const primaryPrivateUserKey = getPrimaryKey(userKeys)?.privateKey;
342     for (const { address: oldAddress, keysToReactivate } of addressRecordsInLegacyFormatOrWithBackup) {
343         const keysLeftToReactivate: KeyReactivationData[] = [];
344         keysToReactivate.forEach((x) => {
345             // Even if this key was uploaded, it may have gotten reactivated from a user key earlier, and so it
346             // should not be attempted to be reactivated again
347             const alreadyReactivated = allReactivatedAddressKeysMap[x.Key.ID] === true;
348             if (alreadyReactivated) {
349                 onReactivation(x.id, 'ok');
350             } else {
351                 keysLeftToReactivate.push(x);
352             }
353         });
354         try {
355             const address = addresses.find(({ ID: otherID }) => oldAddress?.ID === otherID);
356             if (!address || !primaryPrivateUserKey) {
357                 throw new Error('Missing dependency');
358             }
360             const addressKeys = await getDecryptedAddressKeysHelper(address.Keys, user, userKeys, '');
361             const activeAddressKeys = await getActiveAddressKeys(
362                 address,
363                 address.SignedKeyList,
364                 address.Keys,
365                 addressKeys
366             );
368             await reactivateAddressKeysV2({
369                 api,
370                 address,
371                 activeKeys: activeAddressKeys,
372                 userKey: primaryPrivateUserKey,
373                 onReactivation,
374                 keysToReactivate: keysLeftToReactivate,
375                 keyTransparencyVerify,
376             });
377         } catch (e: any) {
378             keysLeftToReactivate.forEach(({ id }) => onReactivation(id, e));
379         }
380     }
383 export default reactivateKeysProcessV2;