Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / crypto / keys / calendarKeys.ts
blob5b46d60b73d7f1e1b83ab20f11f203c1e31aaecb
1 import { c } from 'ttag';
3 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
4 import { CryptoProxy, VERIFICATION_STATUS, toPublicKeyReference } from '@proton/crypto';
5 import { SIGNATURE_CONTEXT } from '@proton/shared/lib/calendar/crypto/constants';
6 import isTruthy from '@proton/utils/isTruthy';
8 import { KEYGEN_CONFIGS, KEYGEN_TYPES } from '../../../constants';
9 import { uint8ArrayToBase64String } from '../../../helpers/encoding';
10 import type { KeyGenConfig, SimpleMap } from '../../../interfaces';
11 import type {
12     CreateOrResetCalendarPayload,
13     DecryptedCalendarKey,
14     CalendarKey as tsKey,
15 } from '../../../interfaces/calendar';
16 import { getEncryptedSessionKey } from '../encrypt';
18 export const generatePassphrase = () => {
19     const value = crypto.getRandomValues(new Uint8Array(32));
20     return uint8ArrayToBase64String(value);
23 /**
24  * The calendar key is generated with less user info to not confuse if the key is exported.
25  */
26 export const generateCalendarKey = async ({
27     passphrase,
28     keyGenConfig,
29 }: {
30     passphrase: string;
31     keyGenConfig: KeyGenConfig;
32 }) => {
33     const privateKey = await CryptoProxy.generateKey({
34         userIDs: [{ name: 'Calendar key' }],
35         ...keyGenConfig,
36     });
38     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase });
40     return { privateKey, privateKeyArmored };
43 export const signPassphrase = ({ passphrase, privateKey }: { passphrase: string; privateKey: PrivateKeyReference }) => {
44     return CryptoProxy.signMessage({ textData: passphrase, signingKeys: privateKey, detached: true });
47 export const encryptPassphrase = async ({
48     passphrase,
49     privateKey,
50     publicKey,
51 }: {
52     passphrase: string;
53     privateKey: PrivateKeyReference;
54     publicKey: PublicKeyReference;
55 }): Promise<{
56     keyPacket: string;
57     dataPacket: string;
58     signature: string;
59 }> => {
60     const sessionKey = await CryptoProxy.generateSessionKey({ recipientKeys: publicKey });
61     // we encrypt using `sessionKey` directly instead of `encryptionKeys` so that returned message only includes
62     // symmetrically encrypted data
63     const { message: encryptedData, signature: binarySignature } = await CryptoProxy.encryptMessage({
64         textData: passphrase, // stripTrailingSpaces: false
65         sessionKey,
66         signingKeys: privateKey,
67         detached: true,
68         format: 'binary',
69     });
71     // encrypt to the public key separately to get a separate serialized session key
72     const encryptedSessionKey = await CryptoProxy.encryptSessionKey({
73         ...sessionKey,
74         encryptionKeys: publicKey,
75         format: 'binary',
76     });
78     return {
79         keyPacket: uint8ArrayToBase64String(encryptedSessionKey),
80         dataPacket: uint8ArrayToBase64String(encryptedData),
81         signature: await CryptoProxy.getArmoredSignature({ binarySignature }),
82     };
85 export function encryptPassphraseSessionKey({
86     sessionKey,
87     publicKey,
88     signingKey,
89 }: {
90     sessionKey: SessionKey;
91     publicKey: PublicKeyReference;
92     signingKey: PrivateKeyReference;
93 }): Promise<{ encryptedSessionKey: string; armoredSignature: string }>;
94 export function encryptPassphraseSessionKey({
95     sessionKey,
96     memberPublicKeys,
97     signingKey,
98 }: {
99     sessionKey: SessionKey;
100     memberPublicKeys: SimpleMap<PublicKeyReference>;
101     signingKey: PrivateKeyReference;
102 }): Promise<{ encryptedSessionKeyMap: SimpleMap<string>; armoredSignature: string }>;
103 export function encryptPassphraseSessionKey({
104     sessionKey,
105     memberPublicKeys,
106     signingKey,
107 }: {
108     sessionKey: SessionKey;
109     memberPublicKeys: SimpleMap<PublicKeyReference>;
110     signingKey: PrivateKeyReference;
111 }): Promise<{ encryptedSessionKey?: string; encryptedSessionKeyMap?: SimpleMap<string>; armoredSignature: string }>;
113 export async function encryptPassphraseSessionKey({
114     sessionKey,
115     publicKey,
116     memberPublicKeys,
117     signingKey,
118 }: {
119     sessionKey: SessionKey;
120     publicKey?: PublicKeyReference;
121     memberPublicKeys?: SimpleMap<PublicKeyReference>;
122     signingKey: PrivateKeyReference;
123 }) {
124     const armoredSignaturePromise = CryptoProxy.signMessage({
125         binaryData: sessionKey.data,
126         signingKeys: signingKey,
127         detached: true,
128         format: 'armored',
129         context: { critical: true, value: SIGNATURE_CONTEXT.SHARE_CALENDAR_INVITE },
130     });
132     if (publicKey) {
133         const [armoredSignature, encryptedSessionKey] = await Promise.all([
134             armoredSignaturePromise,
135             uint8ArrayToBase64String(await getEncryptedSessionKey(sessionKey, publicKey)),
136         ]);
138         return {
139             encryptedSessionKey,
140             armoredSignature,
141         };
142     } else if (memberPublicKeys) {
143         const encryptedSessionKeysPromise = Promise.all(
144             Object.entries(memberPublicKeys).map(async ([id, publicKey]) => {
145                 if (!publicKey) {
146                     throw new Error('Missing public key for member');
147                 }
148                 const keyPacket = uint8ArrayToBase64String(await getEncryptedSessionKey(sessionKey, publicKey));
150                 return [id, keyPacket];
151             })
152         );
154         const [armoredSignature, encryptedSessionKeys] = await Promise.all([
155             armoredSignaturePromise,
156             encryptedSessionKeysPromise,
157         ]);
159         return {
160             encryptedSessionKeyMap: Object.fromEntries(encryptedSessionKeys),
161             armoredSignature,
162         };
163     }
165     throw new Error('Missing parameters to encrypt session key');
169  * Decrypts a calendar passphrase with either some private keys or a session key
170  */
171 export const decryptPassphrase = async ({
172     armoredPassphrase,
173     sessionKey,
174     armoredSignature,
175     privateKeys,
176     publicKeys,
177 }: {
178     armoredPassphrase: string;
179     sessionKey?: SessionKey;
180     armoredSignature?: string;
181     privateKeys?: PrivateKeyReference[];
182     publicKeys?: PublicKeyReference[];
183 }) => {
184     const { data: decryptedPassphrase, verified } = await CryptoProxy.decryptMessage({
185         armoredMessage: armoredPassphrase,
186         armoredSignature,
187         decryptionKeys: privateKeys,
188         verificationKeys: publicKeys,
189         sessionKeys: sessionKey,
190     });
192     if (publicKeys?.length && verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
193         const error = new Error(c('Error').t`Signature verification failed`);
194         error.name = 'SignatureError';
195         throw error;
196     }
198     return decryptedPassphrase;
202  * Retrieves the decrypted session key that encrypts a calendar passphrase with a private key
203  */
204 export const decryptPassphraseSessionKey = ({
205     armoredPassphrase,
206     privateKeys,
207 }: {
208     armoredPassphrase: string;
209     privateKeys: PrivateKeyReference[];
210 }) => {
211     return CryptoProxy.decryptSessionKey({
212         armoredMessage: armoredPassphrase,
213         decryptionKeys: privateKeys,
214     });
218  * Decrypt the calendar keys.
219  * @param Keys - the calendar keys as coming from the API
220  * @param passphrasesMap - The decrypted passphrases map
221  */
222 export const getDecryptedCalendarKeys = async (
223     Keys: tsKey[],
224     passphrasesMap: { [key: string]: string | undefined } = {}
225 ): Promise<DecryptedCalendarKey[]> => {
226     const process = async (Key: tsKey) => {
227         try {
228             const { PrivateKey, PassphraseID } = Key;
229             const passphrase = passphrasesMap[PassphraseID] || '';
230             const privateKey = await CryptoProxy.importPrivateKey({ armoredKey: PrivateKey, passphrase });
231             const publicKey = await toPublicKeyReference(privateKey);
232             return {
233                 Key,
234                 privateKey,
235                 publicKey,
236             };
237         } catch (e: any) {
238             return undefined;
239         }
240     };
241     return Promise.all(Keys.map(process)).then((result) => {
242         return result.filter(isTruthy);
243     });
246 export const generateCalendarKeyPayload = async ({
247     addressID,
248     privateKey,
249     publicKey,
250 }: {
251     addressID: string;
252     privateKey: PrivateKeyReference;
253     publicKey: PublicKeyReference;
254 }): Promise<CreateOrResetCalendarPayload> => {
255     const passphrase = generatePassphrase();
256     const keyGenConfig = KEYGEN_CONFIGS[KEYGEN_TYPES.CURVE25519];
257     const [{ privateKeyArmored: PrivateKey }, { dataPacket: DataPacket, keyPacket: KeyPacket, signature: Signature }] =
258         await Promise.all([
259             generateCalendarKey({ passphrase, keyGenConfig }),
260             encryptPassphrase({ passphrase, privateKey, publicKey }),
261         ]);
263     return {
264         AddressID: addressID,
265         Signature,
266         PrivateKey,
267         Passphrase: {
268             DataPacket,
269             KeyPacket,
270         },
271     };