Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / shared / lib / keys / addressKeys.ts
blob46b9783c66e087550ad9dddd664e8f151cc7a1ab
1 import { c } from 'ttag';
3 import type { PrivateKeyReference, PublicKeyReference } from '@proton/crypto';
4 import { CryptoProxy, VERIFICATION_STATUS, serverTime } from '@proton/crypto';
5 import { arrayToHexString } from '@proton/crypto/lib/utils';
6 import { getPrimaryKey } from '@proton/shared/lib/keys/getPrimaryKey';
7 import { splitKeys } from '@proton/shared/lib/keys/keys';
8 import isTruthy from '@proton/utils/isTruthy';
10 import { DEFAULT_KEYGEN_TYPE, KEYGEN_CONFIGS } from '../constants';
11 import type {
12     Address,
13     AddressKey,
14     CachedOrganizationKey,
15     DecryptedAddressKey,
16     DecryptedKey,
17     KeyGenConfig,
18     KeyGenConfigV6,
19     KeyPair,
20     KeysPair,
21     AddressKey as tsAddressKey,
22 } from '../interfaces';
23 import { decryptKeyPacket, encryptAndSignKeyPacket } from './keypacket';
24 import { decryptMemberToken } from './memberToken';
26 interface EncryptAddressKeyTokenArguments {
27     token: string;
28     userKey: PrivateKeyReference;
29     organizationKey?: PrivateKeyReference;
30     context?: Parameters<typeof CryptoProxy.signMessage<any>>[0]['context'];
33 export const encryptAddressKeyToken = async ({
34     token,
35     userKey,
36     organizationKey,
37     context,
38 }: EncryptAddressKeyTokenArguments) => {
39     const date = serverTime(); // ensure the signed message and the encrypted one have the same creation time, otherwise verification will fail
40     const textData = token;
41     const [userSignatureResult, organizationSignatureResult] = await Promise.all([
42         CryptoProxy.signMessage({
43             textData, // stripTrailingSpaces: false,
44             date,
45             signingKeys: [userKey],
46             detached: true,
47             context,
48         }),
49         organizationKey
50             ? CryptoProxy.signMessage({
51                   textData, // stripTrailingSpaces: false,
52                   date,
53                   signingKeys: [organizationKey],
54                   detached: true,
55                   context,
56               })
57             : undefined,
58     ]);
60     const { message: encryptedToken } = await CryptoProxy.encryptMessage({
61         textData,
62         date,
63         encryptionKeys: organizationKey ? [userKey, organizationKey] : [userKey],
64     });
66     return {
67         token,
68         encryptedToken,
69         signature: userSignatureResult,
70         ...(organizationSignatureResult && { organizationSignature: organizationSignatureResult }),
71     };
74 interface EncryptAddressKeyUsingOrgKeyTokenArguments {
75     token: string;
76     organizationKey: PrivateKeyReference;
77     context?: Parameters<typeof CryptoProxy.signMessage<any>>[0]['context'];
80 export const encryptAddressKeyUsingOrgKeyToken = async ({
81     token,
82     organizationKey,
83     context,
84 }: EncryptAddressKeyUsingOrgKeyTokenArguments) => {
85     const date = serverTime(); // ensure the signed message and the encrypted one have the same creation time, otherwise verification will fail
86     const textData = token;
88     const organizationSignatureResult = await CryptoProxy.signMessage({
89         textData,
90         date,
91         signingKeys: [organizationKey],
92         detached: true,
93         context,
94     });
96     const { message: encryptedToken } = await CryptoProxy.encryptMessage({
97         textData,
98         date,
99         encryptionKeys: [organizationKey],
100     });
102     return {
103         token,
104         encryptedToken,
105         signature: organizationSignatureResult,
106     };
109 interface DecryptAddressKeyTokenArguments {
110     Token: string;
111     Signature: string;
112     privateKeys: PrivateKeyReference | PrivateKeyReference[];
113     publicKeys: PublicKeyReference | PublicKeyReference[];
114     context?: Parameters<typeof CryptoProxy.decryptMessage<any>>[0]['context'];
117 export const decryptAddressKeyToken = async ({
118     Token,
119     Signature,
120     privateKeys,
121     publicKeys,
122     context,
123 }: DecryptAddressKeyTokenArguments) => {
124     const { data: decryptedToken, verified } = await CryptoProxy.decryptMessage({
125         armoredMessage: Token,
126         armoredSignature: Signature,
127         decryptionKeys: privateKeys,
128         verificationKeys: publicKeys,
129         context,
130     });
132     if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
133         const error = new Error(c('Error').t`Signature verification failed`);
134         error.name = 'SignatureError';
135         throw error;
136     }
138     return decryptedToken;
141 interface DectyptAddressKeyUsingOrgKeyTokenArguments {
142     Token: string;
143     organizationKey: PrivateKeyReference;
144     Signature: string;
147 export const decryptAddressKeyUsingOrgKeyToken = async ({
148     Token,
149     organizationKey,
150     Signature,
151 }: DectyptAddressKeyUsingOrgKeyTokenArguments) => {
152     const { data: decryptedToken, verified } = await CryptoProxy.decryptMessage({
153         armoredMessage: Token,
154         armoredSignature: Signature,
155         decryptionKeys: [organizationKey],
156         verificationKeys: [organizationKey],
157     });
159     if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
160         const error = new Error(c('Error').t`Signature verification failed`);
161         error.name = 'SignatureError';
162         throw error;
163     }
165     return Promise.resolve(decryptedToken);
168 interface AddressKeyTokenResult {
169     token: string;
170     encryptedToken: string;
171     signature: string;
172     organizationSignature?: string;
175 interface AddressKeyOrgTokenResult extends AddressKeyTokenResult {
176     organizationSignature: string;
179 export function generateAddressKeyTokens(
180     userKey: PrivateKeyReference,
181     organizationKey: PrivateKeyReference
182 ): Promise<AddressKeyOrgTokenResult>;
183 export function generateAddressKeyTokens(
184     userKey: PrivateKeyReference,
185     organizationKey?: PrivateKeyReference
186 ): Promise<AddressKeyTokenResult>;
188 export async function generateAddressKeyTokens(userKey: PrivateKeyReference, organizationKey?: PrivateKeyReference) {
189     const randomBytes = crypto.getRandomValues(new Uint8Array(32));
190     const token = arrayToHexString(randomBytes);
191     return encryptAddressKeyToken({ token, organizationKey, userKey });
194 export async function generateAddressKeyTokensUsingOrgKey(organizationKey: PrivateKeyReference) {
195     const randomBytes = crypto.getRandomValues(new Uint8Array(32));
196     const token = arrayToHexString(randomBytes);
197     return encryptAddressKeyUsingOrgKeyToken({ token, organizationKey });
200 interface GetAddressKeyTokenArguments {
201     Token: string;
202     Signature: string;
203     organizationKey?: KeyPair;
204     privateKeys: PrivateKeyReference | PrivateKeyReference[];
205     publicKeys: PublicKeyReference | PublicKeyReference[];
208 export const getAddressKeyToken = ({
209     Token,
210     Signature,
211     organizationKey,
212     privateKeys,
213     publicKeys,
214 }: GetAddressKeyTokenArguments) => {
215     // New address key format
216     if (Signature) {
217         return decryptAddressKeyToken({
218             Token,
219             Signature,
220             privateKeys,
221             publicKeys,
222         });
223     }
224     if (!organizationKey) {
225         throw new Error('Missing organization key');
226     }
227     // Old address key format for an admin signed into a non-private user
228     return decryptMemberToken(Token, [organizationKey.privateKey], [organizationKey.publicKey]);
231 export const getAddressKeyPassword = (
232     { Activation, Token, Signature }: tsAddressKey,
233     userKeys: KeysPair,
234     keyPassword: string,
235     organizationKey?: KeyPair
236 ) => {
237     // If not decrypting the non-private member keys with the organization key, and
238     // because the activation process is asynchronous in the background, allow the
239     // private key to get decrypted already here so that it can be used
240     if (!organizationKey && Activation) {
241         return decryptMemberToken(Activation, userKeys.privateKeys, userKeys.publicKeys);
242     }
244     if (Token) {
245         return getAddressKeyToken({
246             Token,
247             Signature,
248             organizationKey,
249             privateKeys: userKeys.privateKeys,
250             publicKeys: userKeys.publicKeys,
251         });
252     }
254     return Promise.resolve(keyPassword);
257 export const getDecryptedAddressKey = async (
258     { ID, PrivateKey, Flags, Primary }: tsAddressKey,
259     addressKeyPassword: string
260 ): Promise<DecryptedAddressKey> => {
261     const privateKey = await CryptoProxy.importPrivateKey({ armoredKey: PrivateKey, passphrase: addressKeyPassword });
262     const publicKey = await CryptoProxy.importPublicKey({
263         binaryKey: await CryptoProxy.exportPublicKey({ key: privateKey, format: 'binary' }),
264     });
265     return {
266         ID,
267         Flags,
268         privateKey,
269         publicKey,
270         Primary,
271     };
274 export interface ReformatAddressKeyArguments {
275     email: string;
276     name?: string;
277     passphrase: string;
278     privateKey: PrivateKeyReference;
281 export const reformatAddressKey = async ({
282     email,
283     name = email,
284     passphrase,
285     privateKey: originalKey,
286 }: ReformatAddressKeyArguments) => {
287     const privateKey = await CryptoProxy.reformatKey({
288         userIDs: [{ name, email }],
289         privateKey: originalKey,
290     });
292     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase });
294     return { privateKey, privateKeyArmored };
297 export const getEncryptedArmoredAddressKey = async (
298     privateKey: PrivateKeyReference,
299     email: string,
300     newKeyPassword: string
301 ) => {
302     return CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: newKeyPassword });
305 export interface GenerateAddressKeyArguments<C extends KeyGenConfig | KeyGenConfigV6> {
306     email: string;
307     name?: string;
308     passphrase: string;
309     keyGenConfig?: C;
312 // TODOOOOO instead of type inference, create separate generateAddressKeyV6 helper?
313 export const generateAddressKey = async <C extends KeyGenConfig | KeyGenConfigV6>({
314     email,
315     name = email,
316     passphrase,
317     keyGenConfig = KEYGEN_CONFIGS[DEFAULT_KEYGEN_TYPE] as C,
318 }: GenerateAddressKeyArguments<C>) => {
319     const privateKey = await CryptoProxy.generateKey<C['config']>({
320         userIDs: [{ name, email }],
321         ...keyGenConfig,
322     });
324     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase });
326     return { privateKey, privateKeyArmored };
329 export const getIsTokenEncryptedToKeys = async ({
330     addressKey,
331     decryptionKeys,
332 }: {
333     addressKey: AddressKey;
334     decryptionKeys: PrivateKeyReference[];
335 }) => {
336     try {
337         const sessionKey = await CryptoProxy.decryptSessionKey({
338             armoredMessage: addressKey.Token,
339             decryptionKeys,
340         });
341         return !!sessionKey;
342     } catch (e) {
343         return false;
344     }
347 interface ReplaceAddressTokens {
348     privateKey: PrivateKeyReference;
349     privateKeys: PrivateKeyReference[];
350     addresses: Address[];
353 export const getReplacedAddressKeyTokens = async ({ addresses, privateKeys, privateKey }: ReplaceAddressTokens) => {
354     const reEncryptedTokens = await Promise.all(
355         addresses.map(async (address) => {
356             const result = await Promise.all(
357                 address.Keys.map(async (addressKey) => {
358                     try {
359                         // If the address key token is already encrypted to the new key, let's ignore this action.
360                         if (await getIsTokenEncryptedToKeys({ addressKey, decryptionKeys: [privateKey] })) {
361                             return undefined;
362                         }
363                         const { sessionKey, message } = await decryptKeyPacket({
364                             armoredMessage: addressKey.Token,
365                             decryptionKeys: privateKeys,
366                         });
367                         const result = await encryptAndSignKeyPacket({
368                             sessionKey,
369                             binaryData: message.data,
370                             encryptionKey: privateKey,
371                             signingKey: privateKey,
372                         });
373                         if (!result) {
374                             return undefined;
375                         }
376                         return {
377                             AddressKeyID: addressKey.ID,
378                             KeyPacket: result.keyPacket,
379                             Signature: result.signature,
380                         };
381                     } catch (e) {
382                         return undefined;
383                     }
384                 })
385             );
386             return result.filter(isTruthy);
387         })
388     );
390     return {
391         AddressKeyTokens: reEncryptedTokens.flat(),
392     };
395 interface RenameAddressKeysArguments {
396     userKeys: DecryptedKey[];
397     addressKeys: AddressKey[];
398     organizationKey?: KeyPair;
399     email: string;
402 export const getRenamedAddressKeys = async ({
403     userKeys,
404     addressKeys,
405     organizationKey,
406     email,
407 }: RenameAddressKeysArguments) => {
408     const splittedUserKeys = splitKeys(userKeys);
410     const cb = async (addressKey: AddressKey) => {
411         try {
412             const addressKeyPassword = await getAddressKeyPassword(
413                 addressKey,
414                 splittedUserKeys,
415                 '', // Not using a key password since this function only works for address key migrated users
416                 organizationKey
417             );
418             const { privateKey } = await getDecryptedAddressKey(addressKey, addressKeyPassword);
419             const changedPrivateKey = await CryptoProxy.cloneKeyAndChangeUserIDs({
420                 privateKey,
421                 userIDs: [{ name: email, email }],
422             });
423             const privateKeyArmored = await CryptoProxy.exportPrivateKey({
424                 privateKey: changedPrivateKey,
425                 passphrase: addressKeyPassword,
426             });
427             await CryptoProxy.clearKey({ key: privateKey });
428             return {
429                 PrivateKey: privateKeyArmored,
430                 ID: addressKey.ID,
431             };
432         } catch (e) {
433             return;
434         }
435     };
437     const result = await Promise.all(addressKeys.map(cb));
438     return result.filter(isTruthy);
441 export const getPreviousAddressKeyToken = async ({
442     addressKey,
443     privateKeys,
444     publicKeys,
445 }: {
446     addressKey: AddressKey;
447     privateKeys: PrivateKeyReference | PrivateKeyReference[];
448     publicKeys: PublicKeyReference | PublicKeyReference[];
449 }) => {
450     const encryptedToken = addressKey.Token;
451     if (!encryptedToken) {
452         throw new Error('Missing token');
453     }
454     const signature = addressKey.Signature;
455     const token = await getAddressKeyToken({
456         Token: encryptedToken,
457         Signature: signature,
458         privateKeys,
459         publicKeys,
460     });
461     return { signature, token, encryptedToken };
464 export const getNewAddressKeyToken = async ({ address, userKeys }: { address: Address; userKeys: KeyPair[] }) => {
465     const userPrimaryKey = getPrimaryKey(userKeys);
466     const userKey = userPrimaryKey?.privateKey;
467     const splitUserKeys = splitKeys(userKeys);
468     if (!userKey) {
469         throw new Error('Missing primary user key');
470     }
471     if (!address.Keys?.length) {
472         return generateAddressKeyTokens(userKey);
473     }
474     // When a primary key is available, we prefer re-using the existing token instead of generating a new one.
475     // For non-private members we can generate a new key without requiring access to the org key
476     // For future shared addresses, reusing the token will ensure other users can decrypt the new key as well
477     const [primaryAddressKey] = address.Keys;
478     try {
479         return await getPreviousAddressKeyToken({
480             addressKey: primaryAddressKey,
481             privateKeys: splitUserKeys.privateKeys,
482             publicKeys: splitUserKeys.publicKeys,
483         });
484     } catch {
485         return generateAddressKeyTokens(userKey);
486     }
489 export const getNewAddressKeyTokenFromOrgKey = async ({
490     address,
491     organizationKey,
492 }: {
493     address: Address;
494     organizationKey: CachedOrganizationKey;
495 }) => {
496     const { privateKey: decryptedOrganizationKey, publicKey } = organizationKey;
497     if (!decryptedOrganizationKey) {
498         throw new Error('Missing organization primary key');
499     }
500     if (!address.Keys?.length) {
501         return generateAddressKeyTokensUsingOrgKey(decryptedOrganizationKey);
502     }
503     // When a primary key is available, we prefer re-using the existing token instead of generating a new one.
504     // For non-private members we can generate a new key without requiring access to the org key
505     // For future shared addresses, reusing the token will ensure other users can decrypt the new key as well
506     const [primaryAddressKey] = address.Keys;
507     try {
508         return await getPreviousAddressKeyToken({
509             addressKey: primaryAddressKey,
510             privateKeys: decryptedOrganizationKey,
511             publicKeys: publicKey,
512         });
513     } catch {
514         return generateAddressKeyTokensUsingOrgKey(decryptedOrganizationKey);
515     }