Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / contacts / encrypt.ts
blob51e40f689779028733a79046d8ee2d5d986a179c
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[];
21 /**
22  * Split properties for contact cards
23  */
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>(
29         (acc, property) => {
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);
38                 }
39                 return acc;
40             }
42             if (SIGNED_FIELDS.includes(field)) {
43                 acc.toSign.push(property);
44                 return acc;
45             }
47             acc.toEncryptAndSign.push(property);
48             return acc;
49         },
50         {
51             toEncryptAndSign: [],
52             toSign: [],
53             toClearText: [],
54         }
55     );
58 export const prepareCardsFromVCard = (
59     vCardContact: VCardContact,
60     { privateKey, publicKey }: KeyPair
61 ): Promise<ContactCard[]> => {
62     const promises = [];
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();
71         promises.push(
72             CryptoProxy.encryptMessage({
73                 textData,
74                 encryptionKeys: publicKeys,
75                 signingKeys: privateKeys,
76                 detached: true,
77             }).then(({ message: Data, signature: Signature }) => {
78                 const card: ContactCard = {
79                     Type: ENCRYPTED_AND_SIGNED,
80                     Data,
81                     Signature,
82                 };
83                 return card;
84             })
85         );
86     }
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 = {
96             field: 'fn',
97             value: computedFirstAndLastName || fallbackEmail || '',
98             uid: createContactPropertyUid(),
99         };
100         toSign.push(computedFullName);
101     }
103     if (toSign.length > 0) {
104         const hasUID = toSign.some((property) => property.field === 'uid');
105         const hasFN = toSign.some((property) => property.field === 'fn');
107         if (!hasUID) {
108             const defaultUID = generateProtonWebUID();
109             toSign.push({ field: 'uid', value: defaultUID, uid: createContactPropertyUid() });
110         }
112         if (!hasFN) {
113             const fallbackFN = getFallbackFNValue();
114             toSign.push({ field: 'fn', value: fallbackFN, uid: createContactPropertyUid() });
115         }
117         const textData: string = vCardPropertiesToICAL(toSign).toString();
119         promises.push(
120             CryptoProxy.signMessage({
121                 textData,
122                 stripTrailingSpaces: true,
123                 signingKeys: privateKeys,
124                 detached: true,
125             }).then((Signature) => {
126                 const card: ContactCard = {
127                     Type: SIGNED,
128                     Data: textData,
129                     Signature,
130                 };
131                 return card;
132             })
133         );
134     }
136     if (toClearText.length > 0) {
137         const Data = vCardPropertiesToICAL(toClearText).toString();
139         promises.push({
140             Type: CLEAR_TEXT,
141             Data,
142             Signature: null,
143         });
144     }
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 });
155     return { Cards };
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([]);
164     }
166     return Promise.all(vCardContacts.map((contact) => prepareVCardContact(contact, { privateKey, publicKey })));