1 import { useCallback } from 'react';
3 import { useGetAddressKeys } from '@proton/account/addressKeys/hooks';
4 import { useGetAddresses } from '@proton/account/addresses/hooks';
5 import { useGetUser } from '@proton/account/user/hooks';
6 import { useGetUserKeys } from '@proton/account/userKeys/hooks';
7 import useApi from '@proton/components/hooks/useApi';
8 import useConfig from '@proton/components/hooks/useConfig';
9 import useEventManager from '@proton/components/hooks/useEventManager';
10 import { CryptoProxy, serverTime } from '@proton/crypto';
11 import type { SelfAuditResult } from '@proton/key-transparency/lib';
18 } from '@proton/key-transparency/lib';
19 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
20 import { INTERVAL_EVENT_TIMER, MINUTE } from '@proton/shared/lib/constants';
21 import { KEY_TRANSPARENCY_REMINDER_UPDATE } from '@proton/shared/lib/drawer/interfaces';
22 import { wait } from '@proton/shared/lib/helpers/promise';
23 import type { DecryptedAddressKey, KeyPair, SelfAuditState } from '@proton/shared/lib/interfaces';
25 import useGetLatestEpoch from './useGetLatestEpoch';
26 import useReportSelfAuditErrors from './useReportSelfAuditErrors';
28 const SELF_AUDIT_MAX_TRIALS = 6;
30 const ignoreError = (error: any): boolean => {
31 return error instanceof StaleEpochError;
34 const reportError = (error: any, tooManyRetries: boolean) => {
35 if (tooManyRetries || !ignoreError(error)) {
36 ktSentryReportError(error, { context: 'runSelfAudit' });
40 const useRunSelfAudit = () => {
41 const getUser = useGetUser();
42 const getAddresses = useGetAddresses();
43 const getUserKeys = useGetUserKeys();
44 const getLatestEpoch = useGetLatestEpoch();
45 const normalApi = useApi();
46 const { APP_NAME: appName } = useConfig();
47 const reportSelfAuditErrors = useReportSelfAuditErrors();
48 const getAddressKeys = useGetAddressKeys();
49 const { subscribe } = useEventManager();
51 const createSelfAuditStateUserKeys = useCallback(async (): Promise<KeyPair[]> => {
52 const userKeys = await getUserKeys();
53 const exportedUserKeys = await Promise.all(
55 CryptoProxy.exportPrivateKey({
56 privateKey: key.privateKey,
63 const selfAuditKeyReferences = await Promise.all(
64 exportedUserKeys.map(async (key) => {
65 const privateKey = await CryptoProxy.importPrivateKey({
70 privateKey: privateKey,
71 publicKey: privateKey,
75 return selfAuditKeyReferences;
77 exportedUserKeys.forEach((privateKey) => privateKey.fill(0));
81 const createSelfAuditStateAddressKeys = useCallback(async () => {
83 * Wait for the event loop to return an event with no address change
84 * At this point self audit can be sure to have the latest version of addresses.
86 const waitForAddressUpdates = async () => {
87 let resolve: () => void;
88 const eventPromise = new Promise<void>((_resolve) => (resolve = _resolve));
89 const unsubscribe = subscribe((data) => {
90 if (!data.Addresses) {
94 await Promise.any([eventPromise, wait(5 * INTERVAL_EVENT_TIMER)]);
98 await waitForAddressUpdates();
99 const addressesWithoutKeys = await getAddresses();
100 const addressesKeys = await Promise.all(addressesWithoutKeys.map((address) => getAddressKeys(address.ID)));
101 const exportedAddressesKeys = await Promise.all(
102 addressesKeys.map(async (keys) =>
104 keys.map(async (key) => {
105 const privateKey = await CryptoProxy.exportPrivateKey({
106 privateKey: key.privateKey,
113 Primary: key.Primary,
121 const selfAuditAddressesKeys = await Promise.all<Promise<DecryptedAddressKey[]>>(
122 exportedAddressesKeys.map(async (keys) =>
124 keys.map(async (key) => {
125 const privateKey = await CryptoProxy.importPrivateKey({
126 binaryKey: key.privateKey,
132 Primary: key.Primary,
133 privateKey: privateKey,
134 publicKey: privateKey,
140 const addresses = addressesWithoutKeys.map((address, index) => {
143 addressKeys: selfAuditAddressesKeys[index],
146 return { addresses };
148 exportedAddressesKeys.forEach((keys) => keys.forEach(({ privateKey }) => privateKey.fill(0)));
152 const createSelfAuditState = useCallback(
153 async (lastSelfAudit: SelfAuditResult | undefined): Promise<SelfAuditState> => {
154 const [userKeys, addressKeys] = await Promise.all([
155 createSelfAuditStateUserKeys(),
156 createSelfAuditStateAddressKeys(),
167 const clearSelfAuditState = useCallback(async (state: SelfAuditState) => {
168 const clearUserKeysPromise = Promise.all(
169 state.userKeys.map(async ({ privateKey }) => CryptoProxy.clearKey({ key: privateKey }))
171 const clearAddressKeysPromise = Promise.all(
172 state.addresses.map(async ({ addressKeys }) => {
173 await Promise.all(addressKeys.map(async ({ privateKey }) => CryptoProxy.clearKey({ key: privateKey })));
176 await Promise.all([clearUserKeysPromise, clearAddressKeysPromise]);
179 const runSelfAuditWithState = useCallback(async (state: SelfAuditState) => {
180 const ktLSAPIPromise = getKTLocalStorage(appName);
181 const ktLSAPI = await ktLSAPIPromise;
182 if (state.userKeys.length === 0) {
183 throw new Error('User has no user keys');
186 const selfAuditResult = await selfAudit({
194 api: getSilentApi(normalApi),
199 // Update local storage value
200 document.dispatchEvent(
201 new CustomEvent(KEY_TRANSPARENCY_REMINDER_UPDATE, {
208 await reportSelfAuditErrors(selfAuditResult);
209 return { selfAuditResult };
210 } catch (error: any) {
211 const failedTrials = (state.lastSelfAudit?.error?.failedTrials ?? 0) + 1;
212 const tooManyRetries = failedTrials >= SELF_AUDIT_MAX_TRIALS;
213 const currentTime = +serverTime();
214 const selfAuditBaseInterval = getSelfAuditInterval();
215 const nextSelfAuditInterval = tooManyRetries
216 ? selfAuditBaseInterval
217 : Math.min(Math.pow(2, failedTrials) * MINUTE, selfAuditBaseInterval);
218 const selfAuditResult: SelfAuditResult = {
219 auditTime: currentTime,
220 nextAuditTime: currentTime + nextSelfAuditInterval,
221 addressAuditResults: [],
222 localStorageAuditResultsOtherAddress: [],
223 localStorageAuditResultsOwnAddress: [],
224 error: { failedTrials: tooManyRetries ? 0 : failedTrials, tooManyRetries },
226 reportError(error, tooManyRetries);
227 await reportSelfAuditErrors(selfAuditResult);
228 return { selfAuditResult };
232 const runSelfAudit = useCallback(async (lastSelfAudit: SelfAuditResult | undefined) => {
233 const state = await createSelfAuditState(lastSelfAudit);
235 return await runSelfAuditWithState(state);
237 await clearSelfAuditState(state);
244 export default useRunSelfAudit;