Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / keys / publicKeys.ts
blobc06a0548fe89185e4775c85f38c9c9a8b51492c7
1 import { c } from 'ttag';
3 import type { PublicKeyReference } from '@proton/crypto';
4 import { CryptoProxy, serverTime } from '@proton/crypto';
6 import { KEY_FLAG, MIME_TYPES_MORE, PGP_SCHEMES_MORE, RECIPIENT_TYPES } from '../constants';
7 import { hasBit } from '../helpers/bitset';
8 import { canonicalizeEmailByGuess, canonicalizeInternalEmail, extractEmailFromUserID } from '../helpers/email';
9 import { toBitMap } from '../helpers/object';
10 import type { ApiKeysConfig, ContactPublicKeyModel, PublicKeyConfigs, PublicKeyModel } from '../interfaces';
11 import { getKeyHasFlagsToEncrypt } from './keyFlags';
13 const { TYPE_INTERNAL } = RECIPIENT_TYPES;
15 /**
16  * Check if some API key data belongs to an internal user
17  */
18 export const getIsInternalUser = ({ RecipientType }: ApiKeysConfig): boolean => RecipientType === TYPE_INTERNAL;
20 /**
21  * Test if no key is enabled
22  */
23 export const isDisabledUser = (config: ApiKeysConfig): boolean =>
24     getIsInternalUser(config) && config.publicKeys.every(({ flags }) => !getKeyHasFlagsToEncrypt(flags));
26 export const getEmailMismatchWarning = (
27     publicKey: PublicKeyReference,
28     emailAddress: string,
29     isInternal: boolean
30 ): string[] => {
31     const canonicalEmail = isInternal
32         ? canonicalizeInternalEmail(emailAddress)
33         : canonicalizeEmailByGuess(emailAddress);
34     const userIDs = publicKey.getUserIDs();
35     const keyEmails = userIDs.reduce<string[]>((acc, userID) => {
36         const email = extractEmailFromUserID(userID) || userID;
37         // normalize the email
38         acc.push(email);
39         return acc;
40     }, []);
41     const canonicalKeyEmails = keyEmails.map((email) =>
42         isInternal ? canonicalizeInternalEmail(email) : canonicalizeEmailByGuess(email)
43     );
44     if (!canonicalKeyEmails.includes(canonicalEmail)) {
45         const keyUserIds = keyEmails.join(', ');
46         return [c('PGP key warning').t`Email address not found among user ids defined in sending key (${keyUserIds})`];
47     }
48     return [];
51 /**
52  * Sort list of keys retrieved from the API. Trusted keys take preference.
53  * For two keys such that both are either trusted or not, non-verify-only keys take preference
54  */
55 export const sortApiKeys = ({
56     keys = [],
57     obsoleteFingerprints,
58     compromisedFingerprints,
59     trustedFingerprints,
60 }: {
61     keys: PublicKeyReference[];
62     obsoleteFingerprints: Set<string>;
63     compromisedFingerprints: Set<string>;
64     trustedFingerprints: Set<string>;
65 }): PublicKeyReference[] =>
66     keys
67         .reduce<PublicKeyReference[][]>(
68             (acc, key) => {
69                 const fingerprint = key.getFingerprint();
70                 // calculate order through a bitmap
71                 const index = toBitMap({
72                     isObsolete: obsoleteFingerprints.has(fingerprint),
73                     isCompromised: compromisedFingerprints.has(fingerprint),
74                     isNotTrusted: !trustedFingerprints.has(fingerprint),
75                 });
76                 acc[index].push(key);
77                 return acc;
78             },
79             Array.from({ length: 8 }).map(() => [])
80         )
81         .flat();
83 /**
84  * Sort list of pinned keys retrieved from the API. Keys that can be used for sending take preference
85  */
86 export const sortPinnedKeys = ({
87     keys = [],
88     obsoleteFingerprints,
89     compromisedFingerprints,
90     encryptionCapableFingerprints,
91 }: {
92     keys: PublicKeyReference[];
93     obsoleteFingerprints: Set<string>;
94     compromisedFingerprints: Set<string>;
95     encryptionCapableFingerprints: Set<string>;
96 }): PublicKeyReference[] =>
97     keys
98         .reduce<PublicKeyReference[][]>(
99             (acc, key) => {
100                 const fingerprint = key.getFingerprint();
101                 // calculate order through a bitmap
102                 const index = toBitMap({
103                     isObsolete: obsoleteFingerprints.has(fingerprint),
104                     isCompromised: compromisedFingerprints.has(fingerprint),
105                     cannotSend: !encryptionCapableFingerprints.has(fingerprint),
106                 });
107                 acc[index].push(key);
108                 return acc;
109             },
110             Array.from({ length: 8 }).map(() => [])
111         )
112         .flat();
115  * Given a public key, return true if it is capable of encrypting messages.
116  * This includes checking that the key is neither expired nor revoked.
117  */
118 export const getKeyEncryptionCapableStatus = async (publicKey: PublicKeyReference, timestamp?: number) => {
119     const now = timestamp || +serverTime();
120     return CryptoProxy.canKeyEncrypt({ key: publicKey, date: new Date(now) });
124  * Check if a public key is valid for sending according to the information stored in a public key model
125  * We rely only on the fingerprint of the key to do this check
126  */
127 export const getIsValidForSending = (fingerprint: string, publicKeyModel: PublicKeyModel | ContactPublicKeyModel) => {
128     const { compromisedFingerprints, obsoleteFingerprints, encryptionCapableFingerprints } = publicKeyModel;
129     return (
130         !compromisedFingerprints.has(fingerprint) &&
131         !obsoleteFingerprints.has(fingerprint) &&
132         encryptionCapableFingerprints.has(fingerprint)
133     );
136 const getIsValidForVerifying = (fingerprint: string, compromisedFingerprints: Set<string>) => {
137     return !compromisedFingerprints.has(fingerprint);
140 export const getVerifyingKeys = (keys: PublicKeyReference[], compromisedFingerprints: Set<string>) => {
141     return keys.filter((key) => getIsValidForVerifying(key.getFingerprint(), compromisedFingerprints));
145  * For a given email address and its corresponding public keys (retrieved from the API and/or the corresponding vCard),
146  * construct the contact public key model, which reflects the content of the vCard.
147  */
148 export const getContactPublicKeyModel = async ({
149     emailAddress,
150     apiKeysConfig,
151     pinnedKeysConfig,
152 }: Omit<PublicKeyConfigs, 'mailSettings'>): Promise<ContactPublicKeyModel> => {
153     const {
154         pinnedKeys = [],
155         encryptToPinned,
156         encryptToUntrusted,
157         sign,
158         scheme: vcardScheme,
159         mimeType: vcardMimeType,
160         isContact,
161         isContactSignatureVerified,
162         contactSignatureTimestamp,
163     } = pinnedKeysConfig;
164     const trustedFingerprints = new Set<string>();
165     const encryptionCapableFingerprints = new Set<string>();
166     const obsoleteFingerprints = new Set<string>();
167     const compromisedFingerprints = new Set<string>();
169     // prepare keys retrieved from the API
170     const isInternalUser = getIsInternalUser(apiKeysConfig);
171     const isExternalUser = !isInternalUser;
172     const processedApiKeys = apiKeysConfig.publicKeys;
173     const apiKeys = processedApiKeys.map(({ publicKey }) => publicKey);
174     await Promise.all(
175         processedApiKeys.map(async ({ publicKey, flags }) => {
176             const fingerprint = publicKey.getFingerprint();
177             const canEncrypt = await getKeyEncryptionCapableStatus(publicKey);
178             if (canEncrypt) {
179                 encryptionCapableFingerprints.add(fingerprint);
180             }
181             if (!hasBit(flags, KEY_FLAG.FLAG_NOT_COMPROMISED)) {
182                 compromisedFingerprints.add(fingerprint);
183             }
184             if (!hasBit(flags, KEY_FLAG.FLAG_NOT_OBSOLETE)) {
185                 obsoleteFingerprints.add(fingerprint);
186             }
187         })
188     );
190     // prepare keys retrieved from the vCard
191     await Promise.all(
192         pinnedKeys.map(async (publicKey) => {
193             const fingerprint = publicKey.getFingerprint();
194             const canEncrypt = await getKeyEncryptionCapableStatus(publicKey);
195             trustedFingerprints.add(fingerprint);
196             if (canEncrypt) {
197                 encryptionCapableFingerprints.add(fingerprint);
198             }
199         })
200     );
201     const orderedPinnedKeys = sortPinnedKeys({
202         keys: pinnedKeys,
203         obsoleteFingerprints,
204         compromisedFingerprints,
205         encryptionCapableFingerprints,
206     });
208     const orderedApiKeys = sortApiKeys({
209         keys: apiKeys,
210         trustedFingerprints,
211         obsoleteFingerprints,
212         compromisedFingerprints,
213     });
215     let encrypt: boolean | undefined = undefined;
216     if (pinnedKeys.length > 0) {
217         // Some old contacts with pinned WKD keys did not store the `x-pm-encrypt` flag,
218         // since encryption was always enabled, so we treat an 'undefined' flag as 'true'.
219         encrypt = encryptToPinned !== false;
220     } else if (isExternalUser && apiKeys.length > 0) {
221         // Enable encryption by default for contacts with no `x-pm-encrypt-untrusted` flag.
222         encrypt = encryptToUntrusted !== false;
223     }
225     return {
226         encrypt,
227         sign,
228         scheme: vcardScheme || PGP_SCHEMES_MORE.GLOBAL_DEFAULT,
229         mimeType: vcardMimeType || MIME_TYPES_MORE.AUTOMATIC,
230         emailAddress,
231         publicKeys: {
232             apiKeys: orderedApiKeys,
233             pinnedKeys: orderedPinnedKeys,
234             verifyingPinnedKeys: getVerifyingKeys(orderedPinnedKeys, compromisedFingerprints),
235         },
236         isInternalWithDisabledE2EEForMail: !!apiKeysConfig.isInternalWithDisabledE2EEForMail,
237         trustedFingerprints,
238         obsoleteFingerprints,
239         compromisedFingerprints,
240         encryptionCapableFingerprints,
241         isPGPExternal: isExternalUser,
242         isPGPInternal: isInternalUser,
243         isPGPExternalWithExternallyFetchedKeys: isExternalUser && !!apiKeys.length,
244         isPGPExternalWithoutExternallyFetchedKeys: isExternalUser && !apiKeys.length,
245         pgpAddressDisabled: isDisabledUser(apiKeysConfig),
246         isContact,
247         isContactSignatureVerified,
248         contactSignatureTimestamp,
249         emailAddressWarnings: apiKeysConfig.Warnings,
250         emailAddressErrors: apiKeysConfig.Errors,
251         ktVerificationResult: apiKeysConfig.ktVerificationResult,
252     };