Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / sharing / shareUrl / shareUrl.ts
blob2ac8e3a6c5e030433a859b55ff7ff45cf3876b12
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';
11 import type {
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 ({
35     encryptedPurpose,
36     privateKeys,
37 }: {
38     encryptedPurpose: string;
39     privateKeys: PrivateKeyReference[];
40 }) =>
41     (
42         await CryptoProxy.decryptMessage({
43             armoredMessage: encryptedPurpose,
44             decryptionKeys: privateKeys,
45         })
46     ).data;
48 export const generateEncryptedPurpose = async ({
49     purpose,
50     publicKey,
51 }: {
52     purpose: string;
53     publicKey: PublicKeyReference;
54 }) => {
55     return (
56         await CryptoProxy.encryptMessage({ textData: purpose, stripTrailingSpaces: true, encryptionKeys: publicKey })
57     ).message;
59 export const generateEncryptedPassphrase = ({
60     passphraseKey,
61     passphrase,
62 }: {
63     passphraseKey: Uint8Array;
64     passphrase: string;
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 ({
75     cacheKey,
76     publicKeys,
77 }: {
78     cacheKey: string;
79     publicKeys: PublicKeyReference[];
80 }) =>
81     (
82         await CryptoProxy.encryptMessage({
83             textData: cacheKey, // stripTrailingSpaces: false
84             encryptionKeys: publicKeys,
85         })
86     ).message;
88 export const decryptCacheKey = async ({
89     encryptedCacheKey,
90     privateKeys,
91 }: {
92     encryptedCacheKey: string;
93     privateKeys: PrivateKeyReference[];
94 }) =>
95     (
96         await CryptoProxy.decryptMessage({
97             armoredMessage: encryptedCacheKey,
98             decryptionKeys: privateKeys,
99         })
100     ).data;
102 export const getPassphraseKey = ({
103     encryptedPassphrase,
104     calendarPassphrase,
105 }: {
106     encryptedPassphrase: Nullable<string>;
107     calendarPassphrase: string;
108 }) => {
109     if (!encryptedPassphrase) {
110         return null;
111     }
113     return stringToUint8Array(
114         xorEncryptDecrypt({ key: decodeBase64(calendarPassphrase), data: decodeBase64(encryptedPassphrase) })
115     );
118 export const buildLink = ({
119     urlID,
120     accessLevel,
121     passphraseKey,
122     cacheKey,
123 }: {
124     urlID: string;
125     accessLevel: ACCESS_LEVEL;
126     passphraseKey: Nullable<Uint8Array>;
127     cacheKey: string;
128 }) => {
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}`;
137     }
139     return `${baseURL}?CacheKey=${encodedCacheKey}`;
142 export const getCreatePublicLinkPayload = async ({
143     accessLevel,
144     publicKeys,
145     passphrase,
146     passphraseID,
147     encryptedPurpose = null,
148 }: {
149     accessLevel: ACCESS_LEVEL;
150     publicKeys: PublicKeyReference[];
151     passphrase: string;
152     passphraseID: string;
153     encryptedPurpose?: Nullable<string>;
154 }) => {
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 });
164     return {
165         payload: {
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,
173         },
174         passphraseKey,
175         cacheKey,
176     };
179 export const transformLinkFromAPI = async ({
180     calendarUrl,
181     privateKeys,
182     calendarPassphrase,
183     onError,
184 }: {
185     calendarUrl: CalendarUrl;
186     privateKeys: PrivateKeyReference[];
187     calendarPassphrase: string;
188     onError: (e: Error) => void;
189 }): Promise<CalendarLink> => {
190     const {
191         EncryptedPurpose: encryptedPurpose,
192         CalendarUrlID,
193         AccessLevel,
194         EncryptedCacheKey,
195         EncryptedPassphrase,
196     } = calendarUrl;
197     let purpose = null;
198     let link = '';
200     if (encryptedPurpose) {
201         try {
202             purpose = await decryptPurpose({
203                 encryptedPurpose,
204                 privateKeys,
205             });
206         } catch (e: any) {
207             onError(e);
208             purpose = encryptedPurpose;
209         }
210     }
212     try {
213         const cacheKey = await decryptCacheKey({ encryptedCacheKey: EncryptedCacheKey, privateKeys });
214         const passphraseKey = getPassphraseKey({ encryptedPassphrase: EncryptedPassphrase, calendarPassphrase });
216         link = buildLink({
217             urlID: CalendarUrlID,
218             accessLevel: AccessLevel,
219             passphraseKey,
220             cacheKey,
221         });
222     } catch (e: any) {
223         onError(e);
224         link = `Error building link: ${e.message}`;
225     }
227     return {
228         ...calendarUrl,
229         purpose,
230         link,
231     };
234 export const transformLinksFromAPI = async ({
235     calendarUrls,
236     privateKeys,
237     calendarPassphrase,
238     onError,
239 }: {
240     calendarUrls: CalendarUrl[];
241     privateKeys: PrivateKeyReference[];
242     calendarPassphrase: string;
243     onError: (e: Error) => void;
244 }) => {
245     return Promise.all(
246         calendarUrls.map((calendarUrl) =>
247             transformLinkFromAPI({
248                 calendarUrl,
249                 privateKeys,
250                 calendarPassphrase,
251                 onError,
252             })
253         )
254     );