1 import { c } from 'ttag';
3 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto';
4 import { CryptoProxy } from '@proton/crypto';
5 import isTruthy from '@proton/utils/isTruthy';
7 import { CONTACT_CARD_TYPE } from '../constants';
8 import { CANONICALIZE_SCHEME, canonicalizeEmail } from '../helpers/email';
9 import { generateProtonWebUID } from '../helpers/uid';
10 import type { ContactCard } from '../interfaces/contacts';
11 import type { VCardProperty } from '../interfaces/contacts/VCard';
12 import { CRYPTO_PROCESSING_TYPES } from './constants';
13 import { readSigned } from './decrypt';
14 import { toKeyProperty } from './keyProperties';
15 import { createContactPropertyUid, getVCardProperties } from './properties';
16 import { parseToVCard, vCardPropertiesToICAL } from './vcard';
19 * Pin a public key in a contact. Give to it the highest preference
20 * Public keys need to be passed to check signature validity of signed contact cards
21 * Private keys (typically only the primary one) need to be passed to sign the new contact card with the new pinned key
23 interface ParamsUpdate {
24 contactCards: ContactCard[];
27 bePinnedPublicKey: PublicKeyReference;
28 publicKeys: PublicKeyReference[];
29 privateKeys: PrivateKeyReference[];
31 export const pinKeyUpdateContact = async ({
38 }: ParamsUpdate): Promise<ContactCard[]> => {
39 // get the signed card of the contact that contains the key properties. Throw if there are errors
40 const [signedCard, ...otherCards] = contactCards.reduce<ContactCard[]>((acc, card) => {
41 if (card.Type === CONTACT_CARD_TYPE.SIGNED) {
48 const readSignedCard = await readSigned(signedCard, { publicKeys });
49 if (readSignedCard.type !== CRYPTO_PROCESSING_TYPES.SUCCESS) {
50 if (readSignedCard.error) {
51 throw readSignedCard.error;
53 throw new Error('Unknown error');
55 const signedVcard = readSignedCard.data;
57 // get the key properties that correspond to the email address
58 const signedVCard = parseToVCard(signedVcard);
59 const signedProperties = getVCardProperties(signedVCard);
60 const emailProperty = signedProperties.find(({ field, value }) => {
61 const scheme = isInternal ? CANONICALIZE_SCHEME.PROTON : CANONICALIZE_SCHEME.DEFAULT;
63 field === 'email' && canonicalizeEmail(value as string, scheme) === canonicalizeEmail(emailAddress, scheme)
66 const emailGroup = emailProperty?.group as string;
67 const keyProperties = emailGroup
68 ? signedProperties.filter((prop) => {
69 return prop.field === 'key' && prop.group === emailGroup;
73 throw new Error(c('Error').t`The key properties for ${emailAddress} could not be extracted`);
76 // add the new key as the preferred one
77 const shiftedPrefKeyProperties = keyProperties.map((property) => ({
81 pref: String(Number(property.params?.pref) + 1),
84 const newKeyProperties = [
85 await toKeyProperty({ publicKey: bePinnedPublicKey, group: emailGroup, index: 0 }),
86 ...shiftedPrefKeyProperties,
88 const untouchedSignedProperties = signedProperties.filter(
89 ({ field, group }) => field !== 'key' || group !== emailGroup
91 const newSignedProperties = [...untouchedSignedProperties, ...newKeyProperties];
93 // sign the new properties
94 const toSignVcard: string = vCardPropertiesToICAL(newSignedProperties).toString();
95 const signature = await CryptoProxy.signMessage({
96 textData: toSignVcard,
97 stripTrailingSpaces: true,
98 signingKeys: privateKeys,
101 const newSignedCard = {
102 Type: CONTACT_CARD_TYPE.SIGNED,
104 Signature: signature,
106 return [newSignedCard, ...otherCards];
110 * Create a contact with a pinned key. Set encrypt flag to true
111 * Private keys (typically only the primary one) need to be passed to sign the new contact card with the new pinned key
113 interface ParamsCreate {
114 emailAddress: string;
117 bePinnedPublicKey: PublicKeyReference;
118 privateKeys: PrivateKeyReference[];
120 export const pinKeyCreateContact = async ({
126 }: ParamsCreate): Promise<ContactCard[]> => {
127 const properties: VCardProperty[] = [
128 { field: 'fn', value: name || emailAddress, uid: createContactPropertyUid() },
129 { field: 'uid', value: generateProtonWebUID(), uid: createContactPropertyUid() },
130 { field: 'email', value: emailAddress, group: 'item1', uid: createContactPropertyUid() },
131 !isInternal && { field: 'x-pm-encrypt', value: 'true', group: 'item1', uid: createContactPropertyUid() },
132 !isInternal && { field: 'x-pm-sign', value: 'true', group: 'item1', uid: createContactPropertyUid() },
133 await toKeyProperty({ publicKey: bePinnedPublicKey, group: 'item1', index: 0 }),
135 // sign the properties
136 const toSignVcard: string = vCardPropertiesToICAL(properties).toString();
137 const signature = await CryptoProxy.signMessage({
138 textData: toSignVcard,
139 stripTrailingSpaces: true,
140 signingKeys: privateKeys,
143 const newSignedCard = {
144 Type: CONTACT_CARD_TYPE.SIGNED,
146 Signature: signature,
148 return [newSignedCard];