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';
21 } from '../interfaces';
22 import { getDecryptedAddressKeysHelper, getDecryptedUserKeysHelper, reactivateKeysProcess } from '../keys';
24 getAllKeysReactivationRequests,
27 getLikelyHasKeysToReactivate,
28 } from '../keys/getInactiveKeys';
29 import type { KeyReactivationRequestStateData } from '../keys/reactivation/interface';
31 generateRecoveryFileMessage,
32 generateRecoverySecret,
33 getIsRecoveryFileAvailable,
34 getKeyWithRecoverySecret,
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) {
65 const armouredKeys = await parseRecoveryFiles([recoveryMessage], recoverySecrets);
68 armouredKeys.map(({ armoredKey }) => CryptoProxy.importPrivateKey({ armoredKey, passphrase: null }))
72 export const attemptDeviceRecovery = async ({
80 addresses: Address[] | undefined;
83 preAuthKTVerify: PreAuthKTVerify;
85 const privateUser = Boolean(user.Private);
86 if (!addresses || !privateUser) {
90 const hasKeysToReactivate = getLikelyHasKeysToReactivate(user, addresses);
91 if (!hasKeysToReactivate) {
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);
102 inactiveKeys: await getInactiveKeys(address.Keys, keys),
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;
114 const allKeysToReactivate = getAllKeysReactivationRequests({
119 const initialStates = getInitialStates(allKeysToReactivate);
120 const keys = await getKeysFromDeviceRecovery(user);
126 const mapToUploadedPrivateKey = ({ id, Key, fingerprint }: KeyReactivationRequestStateData) => {
127 const uploadedPrivateKey = keys.find((decryptedBackupKey) => {
128 return fingerprint === decryptedBackupKey.getFingerprint();
130 if (!uploadedPrivateKey) {
136 privateKey: uploadedPrivateKey,
140 const keyReactivationRecords = initialStates
141 .map((keyReactivationRecordState) => {
142 const uploadedKeysToReactivate = keyReactivationRecordState.keysToReactivate
143 .map(mapToUploadedPrivateKey)
146 if (!uploadedKeysToReactivate.length) {
151 ...keyReactivationRecordState,
152 keysToReactivate: uploadedKeysToReactivate,
157 const keyTransparencyVerify = preAuthKTVerify(userKeys);
159 let numberOfReactivatedKeys = 0;
160 await reactivateKeysProcess({
166 keyReactivationRecords,
168 onReactivation: (_, result) => {
169 if (result === 'ok') {
170 numberOfReactivatedKeys++;
173 keyTransparencyVerify,
176 return numberOfReactivatedKeys;
179 const storeRecoveryMessage = async ({
186 recoverySecret: string;
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 ({
205 userKeys: DecryptedKey[];
207 const privateUser = Boolean(user.Private);
212 const primaryUserKey = userKeys?.[0];
213 if (!primaryUserKey) {
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 });
223 setNewRecoverySecret({
224 RecoverySecret: recoverySecret,
225 Signature: signature,
229 await storeRecoveryMessage({ user, userKeys, recoverySecret });
233 const valid = await validateRecoverySecret(primaryRecoverySecret, primaryUserKey.publicKey).catch(noop);
238 await storeRecoveryMessage({
241 recoverySecret: primaryRecoverySecret.RecoverySecret,
245 export const getIsDeviceRecoveryAvailable = getIsRecoveryFileAvailable;
247 export const getIsDeviceRecoveryEnabled = (userSettings: UserSettings, authentication: AuthenticationStore) => {
248 return userSettings.DeviceRecovery && authentication.getTrusted();
251 export const syncDeviceRecovery = async ({
263 userKeys: DecryptedKey[];
264 userSettings: UserSettings;
266 addresses: Address[];
267 signal?: AbortSignal;
268 authentication: AuthenticationStore;
270 const hasRecoveryMessage = getHasRecoveryMessage(user.ID);
271 const isDeviceRecoveryEnabled = getIsDeviceRecoveryEnabled(userSettings, authentication);
273 const shouldRemoveDeviceRecovery = hasRecoveryMessage && !isDeviceRecoveryEnabled;
274 if (shouldRemoveDeviceRecovery) {
275 removeDeviceRecovery(user.ID);
279 const isRecoveryFileAvailable = getIsRecoveryFileAvailable({
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) {
294 const storedKeys = (await getKeysFromDeviceRecovery(user)) || [];
295 if (signal?.aborted) {
298 const storedKeyFingerprints = storedKeys.map((key) => key.getFingerprint());
299 const userKeysHaveUpdated = !arraysContainSameElements(storedKeyFingerprints, privateKeyFingerPrints);
301 if (!userKeysHaveUpdated) {
305 await storeDeviceRecovery({ api, user, userKeys });