Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / pass / lib / cache / decrypt.ts
blob40f59dccf48f5184fb42ab6ac6bcf97f8b8e5cce
1 import { decryptData } from '@proton/pass/lib/crypto/utils/crypto-helpers';
2 import { PassCryptoError } from '@proton/pass/lib/crypto/utils/errors';
3 import { deobfuscateItem, obfuscateItem } from '@proton/pass/lib/items/item.obfuscation';
4 import { unwrapOptimisticState } from '@proton/pass/store/optimistic/utils/transformers';
5 import type { ItemsByShareId } from '@proton/pass/store/reducers';
6 import type { State } from '@proton/pass/store/types';
7 import type { Maybe, PassCryptoSnapshot, SerializedCryptoContext } from '@proton/pass/types';
8 import { PassEncryptionTag } from '@proton/pass/types';
9 import type { EncryptedPassCache, PassCache } from '@proton/pass/types/worker/cache';
10 import { logger } from '@proton/pass/utils/logger';
11 import { objectFilter } from '@proton/pass/utils/object/filter';
12 import { stringToUint8Array, uint8ArrayToString } from '@proton/shared/lib/helpers/encoding';
14 const decrypt = async <T extends object>(options: {
15     data: string;
16     key: CryptoKey;
17     useTextDecoder: boolean;
18 }): Promise<T | undefined> => {
19     if (!options.data) return;
21     try {
22         const encryptedData = stringToUint8Array(options.data);
23         const decryptedData = await decryptData(options.key, encryptedData, PassEncryptionTag.Cache);
25         const decoder = new TextDecoder();
26         const value = options.useTextDecoder ? decoder.decode(decryptedData) : uint8ArrayToString(decryptedData);
27         return JSON.parse(value) as T;
28     } catch (error) {
29         logger.warn(`[Cache::decrypt] Decryption failure`, error);
30     }
33 /** Ensures synchronization between the `PassCache` state and its snapshot by removing
34  * shares from the state that lack a corresponding share manager reference in the snapshot.
35  * If removal creates a discrepancy in the item shares, returns undefined to indicate that
36  * a refetch is necessary to reconcile the state and snapshot. */
37 export const sanitizeCache = (cache: Maybe<PassCache>): Maybe<PassCache> => {
38     if (!cache) return;
40     const state = { ...cache.state };
42     /* Filter out shares that have no corresponding share manager reference */
43     const shareManagers = Object.fromEntries(cache.snapshot.shareManagers);
44     state.shares = objectFilter(state.shares, (shareId) => shareId in shareManagers);
46     /* Check that all item shareIDs have a corresponding share in the sanitized list */
47     const itemShareIds = Object.keys(unwrapOptimisticState(state.items.byShareId));
48     const valid = itemShareIds.every((shareId) => shareId in state.shares);
50     return valid ? { state, snapshot: cache.snapshot } : undefined;
53 export const decryptCache = async (
54     cacheKey: CryptoKey,
55     { state: encryptedState, snapshot: encryptedSnapshot, salt }: Partial<EncryptedPassCache>
56 ): Promise<Maybe<PassCache>> => {
57     if (!encryptedState) logger.warn(`[Cache::decrypt] Cached state not found`);
58     if (!encryptedSnapshot) logger.warn(`[Cache::decrypt] Crypto snapshot not found`);
59     if (!salt) logger.warn(`[Cache::decrypt] Salt not found`);
61     if (encryptedState && encryptedSnapshot && salt) {
62         logger.info(`[Cache] Decrypting cache`);
64         const [state, snapshot] = await Promise.all([
65             decrypt<State>({ data: encryptedState, key: cacheKey, useTextDecoder: true }),
66             decrypt<SerializedCryptoContext<PassCryptoSnapshot>>({
67                 data: encryptedSnapshot,
68                 key: cacheKey,
69                 useTextDecoder: false,
70             }),
71         ]);
73         if (state !== undefined && snapshot !== undefined) {
74             /* reobfuscate each cached item with a new mask */
75             for (const [shareId, items] of Object.entries(state.items.byShareId as ItemsByShareId)) {
76                 for (const item of Object.values(items)) {
77                     if ('itemId' in item) {
78                         state.items.byShareId[shareId][item.itemId] = {
79                             ...item,
80                             data: obfuscateItem(deobfuscateItem(item.data)),
81                         };
82                     }
83                 }
84             }
86             return sanitizeCache({ state, snapshot });
87         } else throw new PassCryptoError('Cache decryption failure');
88     }