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