1 import { CryptoProxy } from '@proton/crypto';
3 import { CONTACT_CARD_TYPE } from '../constants';
4 import { generateProtonWebUID } from '../helpers/uid';
5 import type { KeyPair } from '../interfaces';
6 import type { Contact, ContactCard } from '../interfaces/contacts/Contact';
7 import type { VCardContact, VCardProperty } from '../interfaces/contacts/VCard';
8 import { CLEAR_FIELDS, SIGNED_FIELDS } from './constants';
9 import { createContactPropertyUid, getVCardProperties, hasCategories } from './properties';
10 import { getFallbackFNValue, prepareForSaving } from './surgery';
11 import { vCardPropertiesToICAL } from './vcard';
13 const { CLEAR_TEXT, ENCRYPTED_AND_SIGNED, SIGNED } = CONTACT_CARD_TYPE;
15 interface SplitVCardProperties {
16 toEncryptAndSign: VCardProperty[];
17 toSign: VCardProperty[];
18 toClearText: VCardProperty[];
22 * Split properties for contact cards
24 const splitVCardProperties = (properties: VCardProperty[]): SplitVCardProperties => {
25 // we should only create a clear text part if categories are present
26 const splitClearText = hasCategories(properties);
28 return properties.reduce<SplitVCardProperties>(
30 const { field } = property;
32 if (splitClearText && CLEAR_FIELDS.includes(field)) {
33 acc.toClearText.push(property);
34 // Notice CLEAR_FIELDS and SIGNED_FIELDS have some overlap.
35 // The repeated fields need to be in the clear-text and signed parts
36 if (SIGNED_FIELDS.includes(field)) {
37 acc.toSign.push(property);
42 if (SIGNED_FIELDS.includes(field)) {
43 acc.toSign.push(property);
47 acc.toEncryptAndSign.push(property);
58 export const prepareCardsFromVCard = (
59 vCardContact: VCardContact,
60 { privateKey, publicKey }: KeyPair
61 ): Promise<ContactCard[]> => {
63 const publicKeys = [publicKey];
64 const privateKeys = [privateKey];
65 const properties = getVCardProperties(vCardContact);
66 const { toEncryptAndSign = [], toSign = [], toClearText = [] } = splitVCardProperties(properties);
68 if (toEncryptAndSign.length > 0) {
69 const textData: string = vCardPropertiesToICAL(toEncryptAndSign).toString();
72 CryptoProxy.encryptMessage({
74 encryptionKeys: publicKeys,
75 signingKeys: privateKeys,
77 }).then(({ message: Data, signature: Signature }) => {
78 const card: ContactCard = {
79 Type: ENCRYPTED_AND_SIGNED,
88 // The FN field could be empty on contact creation, this is intentional but we need to compute it from first and last name field if that's the case
89 if (!vCardContact.fn) {
90 const givenName = vCardContact?.n?.value?.givenNames?.[0].trim() ?? '';
91 const familyName = vCardContact?.n?.value?.familyNames?.[0].trim() ?? '';
92 const computedFirstAndLastName = `${givenName} ${familyName}` || ''; // Fallback that should never happen since we should always have a first and last name
93 const fallbackEmail = vCardContact.email?.[0]?.value; // Fallback that should never happen since we should always have a first and last name
95 const computedFullName: VCardProperty = {
97 value: computedFirstAndLastName || fallbackEmail || '',
98 uid: createContactPropertyUid(),
100 toSign.push(computedFullName);
103 if (toSign.length > 0) {
104 const hasUID = toSign.some((property) => property.field === 'uid');
105 const hasFN = toSign.some((property) => property.field === 'fn');
108 const defaultUID = generateProtonWebUID();
109 toSign.push({ field: 'uid', value: defaultUID, uid: createContactPropertyUid() });
113 const fallbackFN = getFallbackFNValue();
114 toSign.push({ field: 'fn', value: fallbackFN, uid: createContactPropertyUid() });
117 const textData: string = vCardPropertiesToICAL(toSign).toString();
120 CryptoProxy.signMessage({
122 stripTrailingSpaces: true,
123 signingKeys: privateKeys,
125 }).then((Signature) => {
126 const card: ContactCard = {
136 if (toClearText.length > 0) {
137 const Data = vCardPropertiesToICAL(toClearText).toString();
146 return Promise.all(promises);
149 export const prepareVCardContact = async (
150 vCardContact: VCardContact,
151 { privateKey, publicKey }: KeyPair
152 ): Promise<Pick<Contact, 'Cards'>> => {
153 const prepared = prepareForSaving(vCardContact);
154 const Cards = await prepareCardsFromVCard(prepared, { privateKey, publicKey });
158 export const prepareVCardContacts = async (
159 vCardContacts: VCardContact[],
160 { privateKey, publicKey }: KeyPair
161 ): Promise<Pick<Contact, 'Cards'>[]> => {
162 if (!privateKey || !publicKey) {
163 return Promise.resolve([]);
166 return Promise.all(vCardContacts.map((contact) => prepareVCardContact(contact, { privateKey, publicKey })));