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;
16 * Check if some API key data belongs to an internal user
18 export const getIsInternalUser = ({ RecipientType }: ApiKeysConfig): boolean => RecipientType === TYPE_INTERNAL;
21 * Test if no key is enabled
23 export const isDisabledUser = (config: ApiKeysConfig): boolean =>
24 getIsInternalUser(config) && config.publicKeys.every(({ flags }) => !getKeyHasFlagsToEncrypt(flags));
26 export const getEmailMismatchWarning = (
27 publicKey: PublicKeyReference,
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
41 const canonicalKeyEmails = keyEmails.map((email) =>
42 isInternal ? canonicalizeInternalEmail(email) : canonicalizeEmailByGuess(email)
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})`];
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
55 export const sortApiKeys = ({
58 compromisedFingerprints,
61 keys: PublicKeyReference[];
62 obsoleteFingerprints: Set<string>;
63 compromisedFingerprints: Set<string>;
64 trustedFingerprints: Set<string>;
65 }): PublicKeyReference[] =>
67 .reduce<PublicKeyReference[][]>(
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),
79 Array.from({ length: 8 }).map(() => [])
84 * Sort list of pinned keys retrieved from the API. Keys that can be used for sending take preference
86 export const sortPinnedKeys = ({
89 compromisedFingerprints,
90 encryptionCapableFingerprints,
92 keys: PublicKeyReference[];
93 obsoleteFingerprints: Set<string>;
94 compromisedFingerprints: Set<string>;
95 encryptionCapableFingerprints: Set<string>;
96 }): PublicKeyReference[] =>
98 .reduce<PublicKeyReference[][]>(
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),
107 acc[index].push(key);
110 Array.from({ length: 8 }).map(() => [])
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.
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
127 export const getIsValidForSending = (fingerprint: string, publicKeyModel: PublicKeyModel | ContactPublicKeyModel) => {
128 const { compromisedFingerprints, obsoleteFingerprints, encryptionCapableFingerprints } = publicKeyModel;
130 !compromisedFingerprints.has(fingerprint) &&
131 !obsoleteFingerprints.has(fingerprint) &&
132 encryptionCapableFingerprints.has(fingerprint)
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.
148 export const getContactPublicKeyModel = async ({
152 }: Omit<PublicKeyConfigs, 'mailSettings'>): Promise<ContactPublicKeyModel> => {
159 mimeType: vcardMimeType,
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);
175 processedApiKeys.map(async ({ publicKey, flags }) => {
176 const fingerprint = publicKey.getFingerprint();
177 const canEncrypt = await getKeyEncryptionCapableStatus(publicKey);
179 encryptionCapableFingerprints.add(fingerprint);
181 if (!hasBit(flags, KEY_FLAG.FLAG_NOT_COMPROMISED)) {
182 compromisedFingerprints.add(fingerprint);
184 if (!hasBit(flags, KEY_FLAG.FLAG_NOT_OBSOLETE)) {
185 obsoleteFingerprints.add(fingerprint);
190 // prepare keys retrieved from the vCard
192 pinnedKeys.map(async (publicKey) => {
193 const fingerprint = publicKey.getFingerprint();
194 const canEncrypt = await getKeyEncryptionCapableStatus(publicKey);
195 trustedFingerprints.add(fingerprint);
197 encryptionCapableFingerprints.add(fingerprint);
201 const orderedPinnedKeys = sortPinnedKeys({
203 obsoleteFingerprints,
204 compromisedFingerprints,
205 encryptionCapableFingerprints,
208 const orderedApiKeys = sortApiKeys({
211 obsoleteFingerprints,
212 compromisedFingerprints,
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;
228 scheme: vcardScheme || PGP_SCHEMES_MORE.GLOBAL_DEFAULT,
229 mimeType: vcardMimeType || MIME_TYPES_MORE.AUTOMATIC,
232 apiKeys: orderedApiKeys,
233 pinnedKeys: orderedPinnedKeys,
234 verifyingPinnedKeys: getVerifyingKeys(orderedPinnedKeys, compromisedFingerprints),
236 isInternalWithDisabledE2EEForMail: !!apiKeysConfig.isInternalWithDisabledE2EEForMail,
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),
247 isContactSignatureVerified,
248 contactSignatureTimestamp,
249 emailAddressWarnings: apiKeysConfig.Warnings,
250 emailAddressErrors: apiKeysConfig.Errors,
251 ktVerificationResult: apiKeysConfig.ktVerificationResult,