Merge branch 'INDA-330-pii-update' into 'main'
[ProtonMail-WebClient.git] / packages / account / addressKeys / listener.ts
blob32dd9b1a1a1ab79b0950e6bb135a5cc746a4ffc1
1 import { CryptoProxy } from '@proton/crypto';
2 import type { SharedStartListening } from '@proton/redux-shared-store-types';
3 import { CacheType } from '@proton/redux-utilities';
4 import isDeepEqual from '@proton/shared/lib/helpers/isDeepEqual';
5 import type { Address, DecryptedAddressKey, DecryptedKey, Key } from '@proton/shared/lib/interfaces';
6 import noop from '@proton/utils/noop';
8 import { selectAddresses } from '../addresses';
9 import { selectUserKeys } from '../userKeys';
10 import {
11     type AddressKeysState,
12     addressKeysThunk,
13     dispatchGetAllAddressesKeys,
14     getAllAddressKeysAction,
15     selectAddressKeys,
16 } from './index';
18 const addressKeyEqualityComparator = (a: Key[] | undefined = [], b: Key[] | undefined = []) => {
19     return (
20         a &&
21         b &&
22         a.length === b.length &&
23         a.every((value, index) => {
24             const otherKey = b[index];
25             return isDeepEqual(value, otherKey);
26         })
27     );
30 const getAddressesWithChangedKeys = (
31     currentAddressKeys: AddressKeysState['addressKeys'],
32     currentUserKeys: DecryptedKey[],
33     nextUserKeys: DecryptedKey[],
34     a: Address[],
35     b: Address[]
36 ) => {
37     const result = (() => {
38         // Any user keys changed, return all new addresses.
39         if (currentUserKeys !== nextUserKeys) {
40             return b;
41         }
42         const currentAddressesMap = a.reduce<{ [key: string]: Address }>((acc, address) => {
43             acc[address.ID] = address;
44             return acc;
45         }, {});
46         return b.filter((address) => {
47             return !addressKeyEqualityComparator(address.Keys, currentAddressesMap[address.ID]?.Keys);
48         });
49     })();
50     return result.filter((address) => currentAddressKeys[address.ID]);
53 const getChanges = (currentValue: AddressKeysState['addressKeys'], nextValue: AddressKeysState['addressKeys']) => {
54     return Object.entries(currentValue).filter(([addressID, currentValue]) => {
55         return currentValue?.value !== nextValue[addressID]?.value;
56     });
59 export const addressKeysListener = (startListening: SharedStartListening<AddressKeysState>) => {
60     startListening({
61         predicate: (action, currentState, previousState) => {
62             const currentUserKeys = selectUserKeys(currentState).value;
63             const currentAddresses = selectAddresses(currentState).value || [];
64             const previousUserKeys = selectUserKeys(previousState).value;
65             const previousAddresses = selectAddresses(previousState).value || [];
66             const previousAddressKeys = selectAddressKeys(previousState);
67             return (
68                 // Decrypting address keys depend on user keys, so if they change we assume it should get recomputed.
69                 (currentUserKeys !== previousUserKeys ||
70                     // Addresses changed and address keys have been computed.
71                     currentAddresses !== previousAddresses) &&
72                 Object.keys(previousAddressKeys).length > 0
73             );
74         },
75         effect: async (action, listenerApi) => {
76             const currentState = listenerApi.getOriginalState();
77             const nextState = listenerApi.getState();
78             const currentAddressKeys = selectAddressKeys(currentState);
79             const currentUserKeys = selectUserKeys(currentState)?.value || [];
80             const nextUserKeys = selectUserKeys(nextState)?.value || [];
81             const currentAddresses = selectAddresses(currentState)?.value || [];
82             const nextAddresses = selectAddresses(nextState)?.value || [];
83             const changedAddresses = getAddressesWithChangedKeys(
84                 currentAddressKeys,
85                 currentUserKeys,
86                 nextUserKeys,
87                 currentAddresses,
88                 nextAddresses
89             );
90             if (!changedAddresses.length) {
91                 return;
92             }
93             await Promise.all(
94                 changedAddresses.map((address) =>
95                     listenerApi.dispatch(addressKeysThunk({ addressID: address.ID, cache: CacheType.None }))
96                 )
97             );
98         },
99     });
101     startListening({
102         predicate: (action, currentState, previousState) => {
103             const newValue = selectAddressKeys(currentState);
104             const oldValue = selectAddressKeys(previousState);
105             const changes = getChanges(oldValue, newValue);
106             return changes.length > 0;
107         },
108         effect: (action, listenerApi) => {
109             const clear = (addressID: string, oldValue: DecryptedAddressKey[] | undefined) => {
110                 return Promise.all(
111                     (oldValue || []).map(async (cachedKey) => {
112                         if (cachedKey?.privateKey) {
113                             await CryptoProxy.clearKey({ key: cachedKey.privateKey }).catch(noop);
114                         }
115                         if (cachedKey?.publicKey) {
116                             await CryptoProxy.clearKey({ key: cachedKey.publicKey }).catch(noop);
117                         }
118                         return undefined;
119                     })
120                 );
121             };
122             const oldValue = selectAddressKeys(listenerApi.getOriginalState());
123             const newValue = selectAddressKeys(listenerApi.getState());
124             const changes = getChanges(oldValue, newValue);
125             return Promise.all(
126                 changes.map(async ([addressID, oldValue]) => {
127                     return clear(addressID, oldValue?.value);
128                 })
129             ).then(() => undefined);
130         },
131     });
133     startListening({
134         actionCreator: getAllAddressKeysAction,
135         effect: async (action, listenerApi) => {
136             await dispatchGetAllAddressesKeys(listenerApi.dispatch);
137         },
138     });