1 import type { PublicKeyReference, SessionKey } from '@proton/crypto';
2 import { CryptoProxy, toPublicKeyReference } from '@proton/crypto';
3 import { getIsAllDay } from '@proton/shared/lib/calendar/veventHelper';
4 import { ACCENT_COLORS_MAP } from '@proton/shared/lib/colors';
5 import { omit } from '@proton/shared/lib/helpers/object';
6 import { disableRandomMock, initRandomMock } from '@proton/testing/lib/mockRandomValues';
10 CALENDAR_SHARE_BUSY_TIME_SLOTS,
11 EVENT_VERIFICATION_STATUS,
12 } from '../../lib/calendar/constants';
13 import { readCalendarEvent, readSessionKeys } from '../../lib/calendar/deserialize';
14 import { unwrap, wrap } from '../../lib/calendar/helper';
15 import { createCalendarEvent } from '../../lib/calendar/serialize';
16 import { setVcalProdId } from '../../lib/calendar/vcalConfig';
17 import { toCRLF } from '../../lib/helpers/string';
18 import type { RequireSome } from '../../lib/interfaces';
22 CreateOrUpdateCalendarEventData,
24 } from '../../lib/interfaces/calendar';
25 import { DecryptableKey, DecryptableKey2 } from '../keys/keys.data';
27 const getVevent = (hasColor = false): VcalVeventComponent => {
33 action: { value: 'DISPLAY' },
35 value: { weeks: 0, days: 0, hours: 15, minutes: 0, seconds: 0, isNegative: true },
39 uid: { value: '123' },
41 value: { year: 2019, month: 12, day: 11, hours: 12, minutes: 12, seconds: 12, isUTC: true },
44 value: { year: 2019, month: 12, day: 11, hours: 12, minutes: 12, seconds: 12, isUTC: true },
47 value: { year: 2019, month: 12, day: 12, hours: 12, minutes: 12, seconds: 12, isUTC: true },
49 summary: { value: 'my title' },
50 comment: [{ value: 'asdasd' }],
51 color: hasColor ? { value: ACCENT_COLORS_MAP.enzian.color } : undefined,
54 value: 'mailto:james@bond.co.uk',
57 role: 'REQ-PARTICIPANT',
59 partstat: 'NEEDS-ACTION',
61 cn: 'james@bond.co.uk',
65 value: 'mailto:dr.no@mi6.co.uk',
68 role: 'REQ-PARTICIPANT',
70 partstat: 'TENTATIVE',
76 value: 'mailto:moneypenny@mi6.co.uk',
79 role: 'NON-PARTICIPANT',
82 cn: 'Miss Moneypenny',
90 interface CreateCalendarEventData
92 Partial<Omit<CreateOrUpdateCalendarEventData, 'Permissions'>>,
93 | 'SharedEventContent'
94 | 'CalendarEventContent'
95 | 'AttendeesEventContent'
99 AddressKeyPacket: string | null;
102 const transformToExternal = (
103 data: CreateCalendarEventData,
104 publicAddressKey: PublicKeyReference,
106 sharedSessionKey?: SessionKey,
107 calendarSessionKey?: SessionKey
109 const withAuthor = (x: Omit<CalendarEventData, 'Author'>[], author: string): CalendarEventData[] => {
110 return x.map((y) => ({ ...y, Author: author }));
112 const withFullAttendee = (
113 x?: Omit<Attendee, 'UpdateTime' | 'ID'>[],
117 return (x || []).map((y, i) => ({ ...y, ID: `${ID}-${i}`, UpdateTime }));
122 SharedEvents: withAuthor(data.SharedEventContent, 'me'),
123 CalendarEvents: withAuthor(data.CalendarEventContent, 'me'),
124 AttendeesEvents: withAuthor(data.AttendeesEventContent, 'me'),
125 Attendees: withFullAttendee(data.Attendees),
126 Notifications: data.Notifications,
128 CalendarID: 'calendarID',
133 me: [publicAddressKey],
139 CalendarID: 'calendarID',
140 DefaultEventDuration: 30,
141 DefaultPartDayNotifications: [],
142 DefaultFullDayNotifications: [],
143 MakesUserBusy: CALENDAR_SHARE_BUSY_TIME_SLOTS.YES,
149 describe('calendar encryption', () => {
150 beforeAll(() => initRandomMock());
151 afterAll(() => disableRandomMock());
153 it('should encrypt and sign calendar events', async () => {
154 const dummyProdId = 'Proton Calendar';
155 setVcalProdId(dummyProdId);
156 const calendarKey = await CryptoProxy.importPrivateKey({
157 armoredKey: DecryptableKey.PrivateKey,
160 const addressKey = await CryptoProxy.importPrivateKey({ armoredKey: DecryptableKey2, passphrase: '123' });
162 // without default notifications
164 await createCalendarEvent({
165 eventComponent: getVevent(true),
166 privateKey: addressKey,
167 publicKey: calendarKey,
169 isSwitchCalendar: false,
170 hasDefaultNotifications: false,
174 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
175 SharedEventContent: [
179 'BEGIN:VEVENT\r\nUID:123\r\nDTSTAMP:20191211T121212Z\r\nDTSTART:20191211T121212Z\r\nDTEND:20191212T121212Z\r\nEND:VEVENT',
182 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/),
186 // the following check is just to ensure some stability in the process generating the signatures
187 // i.e. given the same input, we produce the same encrypted data
188 Data: jasmine.stringMatching(/0sADAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
189 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
193 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
194 CalendarEventContent: [
197 // the following check is just to ensure some stability in the process generating the signatures
198 // i.e. given the same input, we produce the same encrypted data
199 Data: jasmine.stringMatching(/0sABAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
200 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
203 Notifications: [{ Type: 1, Trigger: '-PT15H' }],
204 AttendeesEventContent: [
207 // the following check is just to ensure some stability in the process generating the signatures
208 // i.e. given the same input, we produce the same encrypted data
209 Data: jasmine.stringMatching(/0sE8AfKRArUuTJnXofqQYdEjeY\+U6lh.*/g),
210 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
214 { Token: 'abc', Status: ATTENDEE_STATUS_API.NEEDS_ACTION },
215 { Token: 'bcd', Status: ATTENDEE_STATUS_API.TENTATIVE },
216 { Token: 'cde', Status: ATTENDEE_STATUS_API.ACCEPTED },
218 Color: ACCENT_COLORS_MAP.enzian.color,
221 // with default notifications
223 await createCalendarEvent({
224 eventComponent: getVevent(false),
225 privateKey: addressKey,
226 publicKey: calendarKey,
228 isSwitchCalendar: false,
229 hasDefaultNotifications: true,
233 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
234 SharedEventContent: [
238 'BEGIN:VEVENT\r\nUID:123\r\nDTSTAMP:20191211T121212Z\r\nDTSTART:20191211T121212Z\r\nDTEND:20191212T121212Z\r\nEND:VEVENT',
241 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/),
245 // the following check is just to ensure some stability in the process generating the signatures
246 // i.e. given the same input, we produce the same encrypted data
247 Data: jasmine.stringMatching(/0sADAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
248 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
252 'wV4DatuD4HBmK9ESAQdAh5aMHBZCvQYA9q2Gm4j5LJYj0N/ETwHe/+Icmt09yl8w81ByP+wHwvShTNdKZNv7ziSuGkYloQ9Y2hReRQR0Vdacz4LtBa2T3H17aBbI/rBs',
253 CalendarEventContent: [
256 // the following check is just to ensure some stability in the process generating the signatures
257 // i.e. given the same input, we produce the same encrypted data
258 Data: jasmine.stringMatching(/0sABAfKRArUuTJnXofqQYdEjeY\+U6lg.*/g),
259 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
263 AttendeesEventContent: [
266 // the following check is just to ensure some stability in the process generating the signatures
267 // i.e. given the same input, we produce the same encrypted data
268 Data: jasmine.stringMatching(/0sE8AfKRArUuTJnXofqQYdEjeY\+U6lh.*/g),
269 Signature: jasmine.stringMatching(/-----BEGIN PGP SIGNATURE-----.*/g),
273 { Token: 'abc', Status: ATTENDEE_STATUS_API.NEEDS_ACTION },
274 { Token: 'bcd', Status: ATTENDEE_STATUS_API.TENTATIVE },
275 { Token: 'cde', Status: ATTENDEE_STATUS_API.ACCEPTED },
283 it('should roundtrip', async () => {
284 const veventComponent = getVevent(true);
285 const addressKey = await CryptoProxy.importPrivateKey({ armoredKey: DecryptableKey2, passphrase: '123' });
286 const calendarKey = await CryptoProxy.importPrivateKey({
287 armoredKey: DecryptableKey.PrivateKey,
290 const publicKey = await toPublicKeyReference(calendarKey);
291 const publicAddressKey = await toPublicKeyReference(addressKey);
293 const data = (await createCalendarEvent({
294 eventComponent: veventComponent,
295 privateKey: addressKey,
298 isSwitchCalendar: false,
299 hasDefaultNotifications: false,
300 })) as CreateCalendarEventData;
301 const [sharedSessionKey, calendarSessionKey] = await readSessionKeys({
303 privateKeys: calendarKey,
306 const { veventComponent: decryptedVeventComponent, verificationStatus } = await readCalendarEvent(
310 getIsAllDay(veventComponent),
316 expect(decryptedVeventComponent).toEqual(omit(veventComponent, ['color']));
317 expect(verificationStatus).toEqual(EVENT_VERIFICATION_STATUS.SUCCESSFUL);
321 describe('wrapping', () => {
322 it('should add wrapping', () => {
323 expect(wrap('asd')).toEqual(
324 toCRLF(`BEGIN:VCALENDAR
329 expect(wrap('asd', 'gfd')).toEqual(
330 toCRLF(`BEGIN:VCALENDAR
337 it('should remove wrapping', () => {
338 expect(unwrap(wrap('BEGIN:VEVENT asd END:VEVENT', 'gfd'))).toEqual('BEGIN:VEVENT asd END:VEVENT');