1 import { c } from 'ttag';
3 import type { PublicKeyReference } from '@proton/crypto';
5 import { extractDraftMIMEType, extractScheme, extractSign } from '../api/helpers/mailSettings';
6 import type { CONTACT_MIME_TYPES, PGP_SCHEMES } from '../constants';
7 import { BRAND_NAME } from '../constants';
10 KeyTransparencyVerificationResult,
14 } from '../interfaces';
15 import { getEmailMismatchWarning, getIsValidForSending } from '../keys/publicKeys';
17 export enum ENCRYPTION_PREFERENCES_ERROR_TYPES {
19 INTERNAL_USER_DISABLED,
20 INTERNAL_USER_NO_API_KEY,
23 EXTERNAL_USER_NO_VALID_EXTERNALLY_FETCHED_KEY,
24 EXTERNAL_USER_NO_VALID_PINNED_KEY,
25 CONTACT_SIGNATURE_NOT_VERIFIED,
28 export class EncryptionPreferencesError extends Error {
29 type: ENCRYPTION_PREFERENCES_ERROR_TYPES;
31 constructor(type: ENCRYPTION_PREFERENCES_ERROR_TYPES, message: string) {
34 Object.setPrototypeOf(this, EncryptionPreferencesError.prototype);
38 export interface EncryptionPreferences {
42 mimeType: CONTACT_MIME_TYPES;
43 isInternalWithDisabledE2EEForMail: boolean; // both `encrypt` and `isInternalWithDisabledE2EEForMail` might be true at this stage
44 sendKey?: PublicKeyReference;
45 isSendKeyPinned?: boolean;
46 apiKeys: PublicKeyReference[];
47 pinnedKeys: PublicKeyReference[];
48 verifyingPinnedKeys: PublicKeyReference[];
51 hasPinnedKeys: boolean;
53 isContactSignatureVerified?: boolean;
54 contactSignatureTimestamp?: Date;
56 error?: EncryptionPreferencesError;
57 emailAddressWarnings?: string[];
58 ktVerificationResult?: KeyTransparencyVerificationResult;
61 const extractEncryptionPreferencesOwnAddress = (
62 publicKeyModel: PublicKeyModel,
64 ): EncryptionPreferences => {
66 publicKeys: { apiKeys },
70 isInternalWithDisabledE2EEForMail,
72 isContactSignatureVerified,
73 contactSignatureTimestamp,
78 const { address, publicKey, canSend } = selfSend;
79 const hasApiKeys = !!address.HasKeys;
80 const canAddressReceive = !!address.Receive;
89 verifyingPinnedKeys: [],
92 isInternalWithDisabledE2EEForMail: isInternalWithDisabledE2EEForMail, // this may be true if own addresses are disabled
94 isContactSignatureVerified,
95 contactSignatureTimestamp,
99 if (emailAddressErrors?.length) {
100 const errorString = emailAddressErrors[0];
103 error: new EncryptionPreferencesError(ENCRYPTION_PREFERENCES_ERROR_TYPES.EMAIL_ADDRESS_ERROR, errorString),
106 if (!canAddressReceive) {
109 error: new EncryptionPreferencesError(
110 ENCRYPTION_PREFERENCES_ERROR_TYPES.INTERNAL_USER_DISABLED,
111 c('Error').t`Email address disabled`
118 error: new EncryptionPreferencesError(
119 ENCRYPTION_PREFERENCES_ERROR_TYPES.INTERNAL_USER_NO_API_KEY,
120 // Proton users should always have keys, so this case should never happen in practice. Therefore we don't translate the error message
121 `No key was found for the ${BRAND_NAME} user`
125 if (!publicKey || !canSend) {
128 error: new EncryptionPreferencesError(
129 ENCRYPTION_PREFERENCES_ERROR_TYPES.PRIMARY_CANNOT_SEND,
130 c('Error').t`Primary key is not valid for sending`
134 const warnings = getEmailMismatchWarning(publicKey, emailAddress, true);
136 return { ...result, sendKey: publicKey, isSendKeyPinned: false, warnings };
139 const extractEncryptionPreferencesInternal = (publicKeyModel: PublicKeyModel): EncryptionPreferences => {
142 publicKeys: { apiKeys, pinnedKeys, verifyingPinnedKeys },
147 isContactSignatureVerified,
148 contactSignatureTimestamp,
149 emailAddressWarnings,
151 ktVerificationResult,
153 const hasApiKeys = !!apiKeys.length;
154 const hasPinnedKeys = !!pinnedKeys.length;
166 isInternalWithDisabledE2EEForMail: false,
168 isContactSignatureVerified,
169 contactSignatureTimestamp,
170 emailAddressWarnings,
171 ktVerificationResult,
173 if (emailAddressErrors?.length) {
174 const errorString = emailAddressErrors[0];
177 error: new EncryptionPreferencesError(ENCRYPTION_PREFERENCES_ERROR_TYPES.EMAIL_ADDRESS_ERROR, errorString),
180 if (isContact && isContactSignatureVerified === false) {
183 error: new EncryptionPreferencesError(
184 ENCRYPTION_PREFERENCES_ERROR_TYPES.CONTACT_SIGNATURE_NOT_VERIFIED,
185 c('Error').t`Contact signature could not be verified`
192 error: new EncryptionPreferencesError(
193 ENCRYPTION_PREFERENCES_ERROR_TYPES.INTERNAL_USER_NO_API_KEY,
194 // Proton users should always have keys, so this case should never happen in practice. Therefore we don't translate the error message
195 `No key was found for the ${BRAND_NAME} user`
199 // API keys are ordered in terms of user preference. The primary key (first in the list) will be used for sending
200 const [primaryKey] = apiKeys;
201 const primaryKeyFingerprint = primaryKey.getFingerprint();
202 if (!getIsValidForSending(primaryKeyFingerprint, publicKeyModel)) {
205 error: new EncryptionPreferencesError(
206 ENCRYPTION_PREFERENCES_ERROR_TYPES.PRIMARY_CANNOT_SEND,
207 c('Error').t`Primary key retrieved for ${BRAND_NAME} user is not valid for sending`
211 if (!hasPinnedKeys) {
212 const warnings = getEmailMismatchWarning(primaryKey, emailAddress, true);
213 return { ...result, sendKey: primaryKey, isSendKeyPinned: false, warnings };
215 // if there are pinned keys, make sure the primary API key is trusted and valid for sending
216 const isPrimaryTrustedAndValid =
217 trustedFingerprints.has(primaryKeyFingerprint) && getIsValidForSending(primaryKeyFingerprint, publicKeyModel);
218 const sendKey = pinnedKeys.find((key) => key.getFingerprint() === primaryKeyFingerprint);
219 if (!isPrimaryTrustedAndValid || !sendKey) {
223 error: new EncryptionPreferencesError(
224 ENCRYPTION_PREFERENCES_ERROR_TYPES.PRIMARY_NOT_PINNED,
225 c('Error').t`Trusted keys are not valid for sending`
229 const warnings = getEmailMismatchWarning(sendKey, emailAddress, true);
231 // return the pinned key, not the API one
232 return { ...result, sendKey, isSendKeyPinned: true, warnings };
235 const extractEncryptionPreferencesExternalWithExternallyFetchedKeys = (
236 publicKeyModel: PublicKeyModel
237 ): EncryptionPreferences => {
242 publicKeys: { apiKeys, pinnedKeys, verifyingPinnedKeys },
247 isContactSignatureVerified,
248 contactSignatureTimestamp,
249 emailAddressWarnings,
251 ktVerificationResult,
253 const hasApiKeys = true;
254 const hasPinnedKeys = !!pinnedKeys.length;
266 isInternalWithDisabledE2EEForMail: false,
268 isContactSignatureVerified,
269 contactSignatureTimestamp,
270 emailAddressWarnings,
271 ktVerificationResult,
273 if (emailAddressErrors?.length) {
274 const errorString = emailAddressErrors[0];
277 error: new EncryptionPreferencesError(ENCRYPTION_PREFERENCES_ERROR_TYPES.EMAIL_ADDRESS_ERROR, errorString),
280 if (isContact && isContactSignatureVerified === false) {
283 error: new EncryptionPreferencesError(
284 ENCRYPTION_PREFERENCES_ERROR_TYPES.CONTACT_SIGNATURE_NOT_VERIFIED,
285 c('Error').t`Contact signature could not be verified`
289 // WKD and KOO keys are ordered in terms of user preference. The primary key (first in the list) will be used for sending
290 const [primaryKey] = apiKeys;
291 const primaryKeyFingerprint = primaryKey.getFingerprint();
292 const validApiSendKey = apiKeys.find((key) => getIsValidForSending(key.getFingerprint(), publicKeyModel));
293 if (!validApiSendKey) {
296 error: new EncryptionPreferencesError(
297 ENCRYPTION_PREFERENCES_ERROR_TYPES.EXTERNAL_USER_NO_VALID_EXTERNALLY_FETCHED_KEY,
298 c('Error').t`No external key retrieved for user is valid for sending`
302 if (!hasPinnedKeys) {
303 const warnings = getEmailMismatchWarning(primaryKey, emailAddress, false);
304 return { ...result, sendKey: primaryKey, isSendKeyPinned: false, warnings };
306 // if there are pinned keys, make sure the primary API key is trusted and valid for sending
307 const isPrimaryTrustedAndValid =
308 trustedFingerprints.has(primaryKeyFingerprint) && getIsValidForSending(primaryKeyFingerprint, publicKeyModel);
309 const sendKey = pinnedKeys.find((key) => key.getFingerprint() === primaryKeyFingerprint);
310 if (!isPrimaryTrustedAndValid || !sendKey) {
313 sendKey: validApiSendKey,
314 error: new EncryptionPreferencesError(
315 ENCRYPTION_PREFERENCES_ERROR_TYPES.PRIMARY_NOT_PINNED,
316 c('Error').t`Trusted keys are not valid for sending`
320 const warnings = getEmailMismatchWarning(sendKey, emailAddress, false);
322 // return the pinned key, not the API one
323 return { ...result, sendKey, isSendKeyPinned: true, warnings };
326 const extractEncryptionPreferencesExternalWithoutExternallyFetchedKeys = (
327 publicKeyModel: PublicKeyModel
328 ): EncryptionPreferences => {
331 publicKeys: { apiKeys, pinnedKeys, verifyingPinnedKeys },
336 isInternalWithDisabledE2EEForMail,
338 isContactSignatureVerified,
339 contactSignatureTimestamp,
340 emailAddressWarnings,
342 ktVerificationResult,
344 const hasPinnedKeys = !!pinnedKeys.length;
356 isInternalWithDisabledE2EEForMail: isInternalWithDisabledE2EEForMail,
358 isContactSignatureVerified,
359 contactSignatureTimestamp,
360 emailAddressWarnings,
361 ktVerificationResult,
363 if (emailAddressErrors?.length) {
364 const errorString = emailAddressErrors[0];
367 error: new EncryptionPreferencesError(ENCRYPTION_PREFERENCES_ERROR_TYPES.EMAIL_ADDRESS_ERROR, errorString),
370 if (isContact && isContactSignatureVerified === false) {
373 error: new EncryptionPreferencesError(
374 ENCRYPTION_PREFERENCES_ERROR_TYPES.CONTACT_SIGNATURE_NOT_VERIFIED,
375 c('Error').t`Contact signature could not be verified`
379 if (!hasPinnedKeys || !encrypt) {
382 // Pinned keys are ordered in terms of preference. Make sure the first is valid
383 const [sendKey] = pinnedKeys;
384 if (!getIsValidForSending(sendKey.getFingerprint(), publicKeyModel)) {
387 error: new EncryptionPreferencesError(
388 ENCRYPTION_PREFERENCES_ERROR_TYPES.EXTERNAL_USER_NO_VALID_PINNED_KEY,
389 c('Error').t`The sending key is not valid`
393 const warnings = getEmailMismatchWarning(sendKey, emailAddress, false);
395 return { ...result, sendKey, isSendKeyPinned: true, warnings };
399 * Extract the encryption preferences from a public-key model corresponding to a certain email address
401 const extractEncryptionPreferences = (
402 model: ContactPublicKeyModel,
403 mailSettings: MailSettings,
405 ): EncryptionPreferences => {
406 // Determine encrypt and sign flags, plus PGP scheme and MIME type.
407 // Take mail settings into account if they are present
408 const encrypt = !!model.encrypt;
409 const sign = extractSign(model, mailSettings);
410 const scheme = extractScheme(model, mailSettings);
411 const mimeType = extractDraftMIMEType(model, mailSettings);
413 const publicKeyModel = {
416 sign: encrypt || sign,
420 // case of own address
422 return extractEncryptionPreferencesOwnAddress(publicKeyModel, selfSend);
424 // case of internal user
425 if (model.isPGPInternal) {
426 return extractEncryptionPreferencesInternal(publicKeyModel);
428 // case of external user with WKD keys
429 if (model.isPGPExternalWithExternallyFetchedKeys) {
430 return extractEncryptionPreferencesExternalWithExternallyFetchedKeys(publicKeyModel);
432 // case of external user without WKD keys
433 return extractEncryptionPreferencesExternalWithoutExternallyFetchedKeys(publicKeyModel);
436 export default extractEncryptionPreferences;