Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / deserialize.ts
blobcabb04a03a3b71f3ae9cca76cba323e7e3cdc314
1 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
3 import { getIsAddressActive, getIsAddressExternal } from '../helpers/address';
4 import { canonicalizeInternalEmail } from '../helpers/email';
5 import { base64StringToUint8Array } from '../helpers/encoding';
6 import type { Address, Nullable } from '../interfaces';
7 import type {
8     CalendarEvent,
9     CalendarNotificationSettings,
10     CalendarSettings,
11     VcalAttendeeProperty,
12     VcalOrganizerProperty,
13     VcalVeventComponent,
14 } from '../interfaces/calendar';
15 import type { SimpleMap } from '../interfaces/utils';
16 import { toSessionKey } from '../keys/sessionKey';
17 import { modelToValarmComponent } from './alarms/modelToValarm';
18 import { apiNotificationsToModel } from './alarms/notificationsToModel';
19 import { getAttendeeEmail, toInternalAttendee } from './attendees';
20 import { ICAL_ATTENDEE_STATUS } from './constants';
21 import {
22     decryptAndVerifyCalendarEvent,
23     getAggregatedEventVerificationStatus,
24     getDecryptedSessionKey,
25 } from './crypto/decrypt';
26 import { unwrap } from './helper';
27 import { parseWithFoldingRecovery } from './icsSurgery/ics';
28 import { getAttendeePartstat, getIsEventComponent } from './vcalHelper';
30 export const readSessionKey = (
31     KeyPacket?: Nullable<string>,
32     privateKeys?: PrivateKeyReference | PrivateKeyReference[]
33 ) => {
34     if (!KeyPacket || !privateKeys) {
35         return;
36     }
37     return getDecryptedSessionKey(base64StringToUint8Array(KeyPacket), privateKeys);
40 /**
41  * Read the session keys.
42  */
43 export const readSessionKeys = async ({
44     calendarEvent,
45     decryptedSharedKeyPacket,
46     privateKeys,
47 }: {
48     calendarEvent: Pick<CalendarEvent, 'SharedKeyPacket' | 'AddressKeyPacket' | 'CalendarKeyPacket'>;
49     decryptedSharedKeyPacket?: string;
50     privateKeys?: PrivateKeyReference | PrivateKeyReference[];
51 }) => {
52     const sharedsessionKeyPromise = decryptedSharedKeyPacket
53         ? Promise.resolve(toSessionKey(decryptedSharedKeyPacket))
54         : readSessionKey(calendarEvent.AddressKeyPacket || calendarEvent.SharedKeyPacket, privateKeys);
55     const calendarSessionKeyPromise = readSessionKey(calendarEvent.CalendarKeyPacket, privateKeys);
56     return Promise.all([sharedsessionKeyPromise, calendarSessionKeyPromise]);
59 const fromApiNotifications = ({
60     notifications: apiNotifications,
61     isAllDay,
62     calendarSettings,
63 }: {
64     notifications: Nullable<CalendarNotificationSettings[]>;
65     isAllDay: boolean;
66     calendarSettings: CalendarSettings;
67 }) => {
68     const modelAlarms = apiNotificationsToModel({ notifications: apiNotifications, isAllDay, calendarSettings });
70     return modelAlarms.map((alarm) => modelToValarmComponent(alarm));
73 export const getSelfAddressData = ({
74     organizer,
75     attendees = [],
76     addresses = [],
77 }: {
78     organizer?: VcalOrganizerProperty;
79     attendees?: VcalAttendeeProperty[];
80     addresses?: Address[];
81 }) => {
82     if (!organizer) {
83         // it is not an invitation
84         return {
85             isOrganizer: false,
86             isAttendee: false,
87         };
88     }
89     const internalAddresses = addresses.filter((address) => !getIsAddressExternal(address));
90     const ownCanonicalizedEmailsMap = internalAddresses.reduce<SimpleMap<string>>((acc, { Email }) => {
91         acc[Email] = canonicalizeInternalEmail(Email);
92         return acc;
93     }, {});
95     const organizerEmail = canonicalizeInternalEmail(getAttendeeEmail(organizer));
96     const organizerAddress = internalAddresses.find(({ Email }) => ownCanonicalizedEmailsMap[Email] === organizerEmail);
98     if (organizerAddress) {
99         return {
100             isOrganizer: true,
101             isAttendee: false,
102             selfAddress: organizerAddress,
103         };
104     }
106     const canonicalAttendeeEmails = attendees.map((attendee) => canonicalizeInternalEmail(getAttendeeEmail(attendee)));
108     // split active and inactive addresses
109     const { activeAddresses, inactiveAddresses } = internalAddresses.reduce<{
110         activeAddresses: Address[];
111         inactiveAddresses: Address[];
112     }>(
113         (acc, address) => {
114             const addresses = getIsAddressActive(address) ? acc.activeAddresses : acc.inactiveAddresses;
115             addresses.push(address);
117             return acc;
118         },
119         { activeAddresses: [], inactiveAddresses: [] }
120     );
122     // start checking active addresses
123     const { selfActiveAttendee, selfActiveAddress, selfActiveAttendeeIndex } = activeAddresses.reduce<{
124         selfActiveAttendee?: VcalAttendeeProperty;
125         selfActiveAttendeeIndex?: number;
126         selfActiveAddress?: Address;
127         answeredAttendeeFound: boolean;
128     }>(
129         (acc, address) => {
130             if (acc.answeredAttendeeFound) {
131                 return acc;
132             }
133             const canonicalSelfEmail = ownCanonicalizedEmailsMap[address.Email];
134             const index = canonicalAttendeeEmails.findIndex((email) => email === canonicalSelfEmail);
135             if (index === -1) {
136                 return acc;
137             }
138             const attendee = attendees[index];
139             const partstat = getAttendeePartstat(attendee);
140             const answeredAttendeeFound = partstat !== ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
141             if (answeredAttendeeFound || !(acc.selfActiveAttendee && acc.selfActiveAddress)) {
142                 return {
143                     selfActiveAttendee: attendee,
144                     selfActiveAddress: address,
145                     selfActiveAttendeeIndex: index,
146                     answeredAttendeeFound,
147                 };
148             }
149             return acc;
150         },
151         { answeredAttendeeFound: false }
152     );
153     if (selfActiveAttendee && selfActiveAddress) {
154         return {
155             isOrganizer: false,
156             isAttendee: true,
157             selfAttendee: selfActiveAttendee,
158             selfAddress: selfActiveAddress,
159             selfAttendeeIndex: selfActiveAttendeeIndex,
160         };
161     }
162     // check inactive addresses
163     const { selfInactiveAttendee, selfInactiveAddress, selfInactiveAttendeeIndex } = inactiveAddresses.reduce<{
164         selfInactiveAttendee?: VcalAttendeeProperty;
165         selfInactiveAttendeeIndex?: number;
166         selfInactiveAddress?: Address;
167         answeredAttendeeFound: boolean;
168     }>(
169         (acc, address) => {
170             if (acc.answeredAttendeeFound) {
171                 return acc;
172             }
173             const canonicalSelfEmail = ownCanonicalizedEmailsMap[address.Email];
174             const index = canonicalAttendeeEmails.findIndex((email) => email === canonicalSelfEmail);
175             if (index === -1) {
176                 return acc;
177             }
178             const attendee = attendees[index];
179             const partstat = getAttendeePartstat(attendee);
180             const answeredAttendeeFound = partstat !== ICAL_ATTENDEE_STATUS.NEEDS_ACTION;
181             if (answeredAttendeeFound || !(acc.selfInactiveAttendee && acc.selfInactiveAddress)) {
182                 return {
183                     selfInactiveAttendee: attendee,
184                     selfInactiveAttendeeIndex: index,
185                     selfInactiveAddress: address,
186                     answeredAttendeeFound,
187                 };
188             }
189             return acc;
190         },
191         { answeredAttendeeFound: false }
192     );
193     return {
194         isOrganizer: false,
195         isAttendee: !!selfInactiveAttendee,
196         selfAttendee: selfInactiveAttendee,
197         selfAddress: selfInactiveAddress,
198         selfAttendeeIndex: selfInactiveAttendeeIndex,
199     };
202 const readCalendarAlarms = (
203     { Notifications, FullDay }: Pick<CalendarEvent, 'Notifications' | 'FullDay'>,
204     calendarSettings: CalendarSettings
205 ) => {
206     return {
207         valarmComponents: fromApiNotifications({
208             notifications: Notifications || null,
209             isAllDay: !!FullDay,
210             calendarSettings,
211         }),
212         hasDefaultNotifications: !Notifications,
213     };
217  * Read the parts of a calendar event into an internal vcal component.
218  */
219 interface ReadCalendarEventArguments {
220     event: Pick<
221         CalendarEvent,
222         | 'SharedEvents'
223         | 'CalendarEvents'
224         | 'AttendeesEvents'
225         | 'Attendees'
226         | 'Notifications'
227         | 'FullDay'
228         | 'CalendarID'
229         | 'ID'
230         | 'Color'
231     >;
232     publicKeysMap?: SimpleMap<PublicKeyReference | PublicKeyReference[]>;
233     sharedSessionKey?: SessionKey;
234     calendarSessionKey?: SessionKey;
235     calendarSettings: CalendarSettings;
236     addresses: Address[];
237     encryptingAddressID?: string;
240 export const readCalendarEvent = async ({
241     event: {
242         SharedEvents = [],
243         CalendarEvents = [],
244         AttendeesEvents = [],
245         Attendees = [],
246         Notifications,
247         FullDay,
248         CalendarID: calendarID,
249         ID: eventID,
250         Color,
251     },
252     publicKeysMap = {},
253     addresses,
254     sharedSessionKey,
255     calendarSessionKey,
256     calendarSettings,
257     encryptingAddressID,
258 }: ReadCalendarEventArguments) => {
259     const decryptedEventsResults = await Promise.all([
260         Promise.all(SharedEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, sharedSessionKey))),
261         Promise.all(CalendarEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, calendarSessionKey))),
262         Promise.all(AttendeesEvents.map((e) => decryptAndVerifyCalendarEvent(e, publicKeysMap, sharedSessionKey))),
263     ]);
264     const [decryptedSharedEvents, decryptedCalendarEvents, decryptedAttendeesEvents] = decryptedEventsResults.map(
265         (decryptedEvents) => decryptedEvents.map(({ data }) => data)
266     );
267     const verificationStatusArray = decryptedEventsResults
268         .map((decryptedEvents) => decryptedEvents.map(({ verificationStatus }) => verificationStatus))
269         .flat();
270     const verificationStatus = getAggregatedEventVerificationStatus(verificationStatusArray);
272     const vevent = [...decryptedSharedEvents, ...decryptedCalendarEvents].reduce<VcalVeventComponent>((acc, event) => {
273         if (!event) {
274             return acc;
275         }
276         const parsedComponent = parseWithFoldingRecovery(unwrap(event), { calendarID, eventID });
277         if (!getIsEventComponent(parsedComponent)) {
278             return acc;
279         }
280         return { ...acc, ...parsedComponent };
281     }, {} as VcalVeventComponent);
283     const { valarmComponents, hasDefaultNotifications } = readCalendarAlarms(
284         { Notifications, FullDay },
285         calendarSettings
286     );
288     const veventAttendees = decryptedAttendeesEvents.reduce<VcalAttendeeProperty[]>((acc, event) => {
289         if (!event) {
290             return acc;
291         }
292         const parsedComponent = parseWithFoldingRecovery(unwrap(event), { calendarID, eventID });
293         if (!getIsEventComponent(parsedComponent)) {
294             return acc;
295         }
296         return acc.concat(toInternalAttendee(parsedComponent, Attendees));
297     }, []);
299     if (valarmComponents.length) {
300         vevent.components = valarmComponents;
301     }
303     if (veventAttendees.length) {
304         vevent.attendee = veventAttendees;
305     }
307     if (Color) {
308         vevent.color = { value: Color };
309     }
311     const selfAddressData = getSelfAddressData({
312         organizer: vevent.organizer,
313         attendees: veventAttendees,
314         addresses,
315     });
316     const encryptionData = {
317         encryptingAddressID,
318         sharedSessionKey,
319         calendarSessionKey,
320     };
322     return { veventComponent: vevent, hasDefaultNotifications, verificationStatus, selfAddressData, encryptionData };