1 import { useCallback } from 'react';
3 import { useGetAddressKeys } from '@proton/account/addressKeys/hooks';
4 import { useGetAddresses } from '@proton/account/addresses/hooks';
5 import { useGetUserKeys } from '@proton/account/userKeys/hooks';
6 import type { PublicKeyReference } from '@proton/crypto';
7 import { useGetMailSettings } from '@proton/mail/mailSettings/hooks';
8 import getPublicKeysVcardHelper from '@proton/shared/lib/api/helpers/getPublicKeysVcardHelper';
9 import { ADDRESS_STATUS, KEY_FLAG, MINUTE, RECIPIENT_TYPES } from '@proton/shared/lib/constants';
10 import { hasBit } from '@proton/shared/lib/helpers/bitset';
11 import { canonicalizeEmail, canonicalizeInternalEmail } from '@proton/shared/lib/helpers/email';
12 import type { ApiKeysConfig } from '@proton/shared/lib/interfaces';
13 import { KT_VERIFICATION_STATUS } from '@proton/shared/lib/interfaces';
14 import type { GetVerificationPreferences } from '@proton/shared/lib/interfaces/hooks/GetVerificationPreferences';
15 import { splitKeys } from '@proton/shared/lib/keys';
16 import { getActiveKeys } from '@proton/shared/lib/keys/getActiveKeys';
17 import { getVerifyingKeys } from '@proton/shared/lib/keys/publicKeys';
19 import useApi from './useApi';
20 import useCache from './useCache';
21 import { getPromiseValue } from './useCachedModelResult';
22 import useGetPublicKeysForInbox from './useGetPublicKeysForInbox';
24 export const CACHE_KEY = 'VERIFICATION_PREFERENCES';
26 const DEFAULT_LIFETIME = 5 * MINUTE;
29 * Given an email address and the user mail settings, return the verification preferences for verifying messages
30 * from that email address.
32 const useGetVerificationPreferences = () => {
34 const cache = useCache();
35 const getAddresses = useGetAddresses();
36 const getUserKeys = useGetUserKeys();
37 const getAddressKeys = useGetAddressKeys();
38 const getPublicKeysForInbox = useGetPublicKeysForInbox();
39 const getMailSettings = useGetMailSettings();
41 const getVerificationPreferences = useCallback<GetVerificationPreferences>(
42 async ({ email, lifetime, contactEmailsMap }) => {
43 const addresses = await getAddresses();
44 const canonicalEmail = canonicalizeInternalEmail(email);
45 // Disabled addresses might now belong to other users (internal or external), who reused them.
46 // Since own keys take precedence on verification, but the message might be signed by a new/different address owner,
47 // we want to avoid considering disabled addresses as own addresses, in order to successfully verify messages sent by
49 // As a result, however, verification will fail for messages who were indeed sent from the disabled address.
50 // This downside has been deemed acceptable, since it aligns with the more general issue (to be tackled separately)
51 // of needing to preserve the message verification status at the time when a message is first read, regardless of
52 // any subsequent public key changes (for non-pinned keys).
53 const selfAddress = addresses
54 .filter(({ Status }) => Status === ADDRESS_STATUS.STATUS_ENABLED)
55 .find(({ Email }) => canonicalizeInternalEmail(Email) === canonicalEmail);
57 const selfAddressKeys = await getAddressKeys(selfAddress.ID);
58 const activeAddressKeys = await getActiveKeys(
60 selfAddress.SignedKeyList,
64 const activePublicKeys = activeAddressKeys.map(({ publicKey }) => publicKey);
65 const compromisedFingerprints = new Set(
67 .filter(({ flags }) => !hasBit(flags, KEY_FLAG.FLAG_NOT_COMPROMISED))
68 .map(({ fingerprint }) => fingerprint)
70 const verifyingKeys = getVerifyingKeys(activePublicKeys, compromisedFingerprints);
74 apiKeys: activePublicKeys,
76 compromisedFingerprints,
77 ktVerificationResult: { status: KT_VERIFICATION_STATUS.VERIFIED_KEYS },
85 }: ApiKeysConfig = await getPublicKeysForInbox({
88 // messages from internal senders with e2ee disabled are still signed, thus we need to fetch the corresponding verification keys
89 includeInternalKeysWithE2EEDisabledForMail: true,
90 // untrusted WKD keys are not used for verification, and requesting WKD keys leaks to the sender's domain owner that the message has been read.
91 internalKeysOnly: true,
93 const isInternal = RecipientType === RECIPIENT_TYPES.TYPE_INTERNAL;
94 const { publicKeys } = splitKeys(await getUserKeys());
95 const { pinnedKeys, isContactSignatureVerified: pinnedKeysVerified } = await getPublicKeysVcardHelper(
102 const compromisedKeysFingerprints = new Set(
104 .filter(({ flags }) => !hasBit(flags, KEY_FLAG.FLAG_NOT_COMPROMISED))
105 .map(({ publicKey }) => publicKey!.getFingerprint())
107 const pinnedKeysFingerprints = new Set(pinnedKeys.map((key) => key.getFingerprint()));
108 const apiPublicKeys = apiKeys.filter(({ publicKey }) => !!publicKey).map(({ publicKey }) => publicKey!);
109 let verifyingKeys: PublicKeyReference[] = [];
110 if (pinnedKeys.length) {
111 verifyingKeys = getVerifyingKeys(pinnedKeys, compromisedKeysFingerprints);
112 } else if (isInternal && ktVerificationResult?.status === KT_VERIFICATION_STATUS.VERIFIED_KEYS) {
113 verifyingKeys = getVerifyingKeys(apiPublicKeys, compromisedKeysFingerprints);
119 apiKeys: apiPublicKeys,
120 ktVerificationResult,
121 pinnedKeysFingerprints,
122 compromisedKeysFingerprints,
124 apiKeysErrors: Errors,
127 [api, getAddressKeys, getAddresses, getPublicKeysForInbox, getMailSettings]
130 return useCallback<GetVerificationPreferences>(
131 ({ email, lifetime = DEFAULT_LIFETIME, contactEmailsMap }) => {
132 if (!cache.has(CACHE_KEY)) {
133 cache.set(CACHE_KEY, new Map());
135 const subCache = cache.get(CACHE_KEY);
136 // By normalizing email here, we consider that it could not exists different encryption preferences
137 // For 2 addresses identical but for the cases.
138 // If a provider does different one day, this would have to evolve.
139 const canonicalEmail = canonicalizeEmail(email);
140 const miss = () => getVerificationPreferences({ email: canonicalEmail, lifetime, contactEmailsMap });
141 return getPromiseValue(subCache, canonicalEmail, miss, lifetime);
143 [cache, getVerificationPreferences]
147 export default useGetVerificationPreferences;