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: {
17 useTextDecoder: boolean;
18 }): Promise<T | undefined> => {
19 if (!options.data) return;
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;
29 logger.warn(`[Cache::decrypt] Decryption failure`, error);
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> => {
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 (
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,
69 useTextDecoder: false,
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] = {
80 data: obfuscateItem(deobfuscateItem(item.data)),
86 return sanitizeCache({ state, snapshot });
87 } else throw new PassCryptoError('Cache decryption failure');