1 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto';
2 import { CryptoProxy } from '@proton/crypto';
3 import { arrayToBinaryString, decodeBase64, encodeBase64 } from '@proton/crypto/lib/utils';
5 import { AES256, EVENT_ACTIONS } from '../../../constants';
6 import { generateRandomBytes, getSHA256Base64String, xorEncryptDecrypt } from '../../../helpers/crypto';
7 import { stringToUint8Array, uint8ArrayToPaddedBase64URLString, uint8ArrayToString } from '../../../helpers/encoding';
8 import type { Nullable } from '../../../interfaces';
9 import type { CalendarLink, CalendarUrl } from '../../../interfaces/calendar';
10 import { ACCESS_LEVEL } from '../../../interfaces/calendar';
12 CalendarUrlEventManager,
13 CalendarUrlEventManagerCreate,
14 CalendarUrlEventManagerDelete,
15 CalendarUrlEventManagerUpdate,
16 } from '../../../interfaces/calendar/EventManager';
18 export const getIsCalendarUrlEventManagerDelete = (
19 event: CalendarUrlEventManager
20 ): event is CalendarUrlEventManagerDelete => {
21 return event.Action === EVENT_ACTIONS.DELETE;
23 export const getIsCalendarUrlEventManagerCreate = (
24 event: CalendarUrlEventManager
25 ): event is CalendarUrlEventManagerCreate => {
26 return event.Action === EVENT_ACTIONS.CREATE;
28 export const getIsCalendarUrlEventManagerUpdate = (
29 event: CalendarUrlEventManager
30 ): event is CalendarUrlEventManagerUpdate => {
31 return event.Action === EVENT_ACTIONS.UPDATE;
34 export const decryptPurpose = async ({
38 encryptedPurpose: string;
39 privateKeys: PrivateKeyReference[];
42 await CryptoProxy.decryptMessage({
43 armoredMessage: encryptedPurpose,
44 decryptionKeys: privateKeys,
48 export const generateEncryptedPurpose = async ({
53 publicKey: PublicKeyReference;
56 await CryptoProxy.encryptMessage({ textData: purpose, stripTrailingSpaces: true, encryptionKeys: publicKey })
59 export const generateEncryptedPassphrase = ({
63 passphraseKey: Uint8Array;
65 }) => encodeBase64(xorEncryptDecrypt({ key: uint8ArrayToString(passphraseKey), data: decodeBase64(passphrase) }));
67 export const generateCacheKey = () => uint8ArrayToPaddedBase64URLString(generateRandomBytes(16));
69 export const generateCacheKeySalt = () => encodeBase64(arrayToBinaryString(generateRandomBytes(8)));
71 export const getCacheKeyHash = ({ cacheKey, cacheKeySalt }: { cacheKey: string; cacheKeySalt: string }) =>
72 getSHA256Base64String(`${cacheKeySalt}${cacheKey}`);
74 export const generateEncryptedCacheKey = async ({
79 publicKeys: PublicKeyReference[];
82 await CryptoProxy.encryptMessage({
83 textData: cacheKey, // stripTrailingSpaces: false
84 encryptionKeys: publicKeys,
88 export const decryptCacheKey = async ({
92 encryptedCacheKey: string;
93 privateKeys: PrivateKeyReference[];
96 await CryptoProxy.decryptMessage({
97 armoredMessage: encryptedCacheKey,
98 decryptionKeys: privateKeys,
102 export const getPassphraseKey = ({
106 encryptedPassphrase: Nullable<string>;
107 calendarPassphrase: string;
109 if (!encryptedPassphrase) {
113 return stringToUint8Array(
114 xorEncryptDecrypt({ key: decodeBase64(calendarPassphrase), data: decodeBase64(encryptedPassphrase) })
118 export const buildLink = ({
125 accessLevel: ACCESS_LEVEL;
126 passphraseKey: Nullable<Uint8Array>;
129 // calendar.proton.me must be hardcoded here as using getAppHref would produce links that wouldn't work
130 const baseURL = `https://calendar.proton.me/api/calendar/v1/url/${urlID}/calendar.ics`;
131 const encodedCacheKey = encodeURIComponent(cacheKey);
133 if (accessLevel === ACCESS_LEVEL.FULL && passphraseKey) {
134 const encodedPassphraseKey = encodeURIComponent(uint8ArrayToPaddedBase64URLString(passphraseKey));
136 return `${baseURL}?CacheKey=${encodedCacheKey}&PassphraseKey=${encodedPassphraseKey}`;
139 return `${baseURL}?CacheKey=${encodedCacheKey}`;
142 export const getCreatePublicLinkPayload = async ({
147 encryptedPurpose = null,
149 accessLevel: ACCESS_LEVEL;
150 publicKeys: PublicKeyReference[];
152 passphraseID: string;
153 encryptedPurpose?: Nullable<string>;
155 const passphraseKey = await CryptoProxy.generateSessionKeyForAlgorithm(AES256);
156 const encryptedPassphrase =
157 accessLevel === ACCESS_LEVEL.FULL ? generateEncryptedPassphrase({ passphraseKey, passphrase }) : null;
159 const cacheKeySalt = generateCacheKeySalt();
160 const cacheKey = generateCacheKey();
161 const cacheKeyHash = await getCacheKeyHash({ cacheKey, cacheKeySalt });
162 const encryptedCacheKey = await generateEncryptedCacheKey({ cacheKey, publicKeys });
166 AccessLevel: accessLevel,
167 CacheKeySalt: cacheKeySalt,
168 CacheKeyHash: cacheKeyHash,
169 EncryptedPassphrase: encryptedPassphrase,
170 EncryptedPurpose: encryptedPurpose,
171 EncryptedCacheKey: encryptedCacheKey,
172 PassphraseID: accessLevel === ACCESS_LEVEL.FULL ? passphraseID : null,
179 export const transformLinkFromAPI = async ({
185 calendarUrl: CalendarUrl;
186 privateKeys: PrivateKeyReference[];
187 calendarPassphrase: string;
188 onError: (e: Error) => void;
189 }): Promise<CalendarLink> => {
191 EncryptedPurpose: encryptedPurpose,
200 if (encryptedPurpose) {
202 purpose = await decryptPurpose({
208 purpose = encryptedPurpose;
213 const cacheKey = await decryptCacheKey({ encryptedCacheKey: EncryptedCacheKey, privateKeys });
214 const passphraseKey = getPassphraseKey({ encryptedPassphrase: EncryptedPassphrase, calendarPassphrase });
217 urlID: CalendarUrlID,
218 accessLevel: AccessLevel,
224 link = `Error building link: ${e.message}`;
234 export const transformLinksFromAPI = async ({
240 calendarUrls: CalendarUrl[];
241 privateKeys: PrivateKeyReference[];
242 calendarPassphrase: string;
243 onError: (e: Error) => void;
246 calendarUrls.map((calendarUrl) =>
247 transformLinkFromAPI({