Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / shared / lib / keys / organizationKeys.ts
blobb0e6c9a708445bd71bbc811767e7ac3ef261e2db
1 import { c } from 'ttag';
3 import type { PrivateKeyReference, PublicKeyReference, SessionKey } from '@proton/crypto';
4 import { CryptoProxy, VERIFICATION_STATUS } from '@proton/crypto';
5 import { arrayToHexString } from '@proton/crypto/lib/utils';
6 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
7 import { getAndVerifyApiKeys } from '@proton/shared/lib/api/helpers/getAndVerifyApiKeys';
8 import { decryptKeyPacket, encryptAndSignKeyPacket } from '@proton/shared/lib/keys/keypacket';
9 import { computeKeyPassword, generateKeySalt } from '@proton/srp';
10 import isTruthy from '@proton/utils/isTruthy';
12 import type { UpdateOrganizationKeysPayloadLegacy, UpdateOrganizationKeysPayloadV2 } from '../api/organization';
13 import type {
14     Address,
15     Api,
16     KTUserContext,
17     KeyGenConfig,
18     KeyPair,
19     Member,
20     OrganizationKey,
21     PasswordlessOrganizationKey,
22     VerifyOutboundPublicKeys,
23 } from '../interfaces';
24 import { encryptAddressKeyToken, generateAddressKey, getAddressKeyToken } from './addressKeys';
25 import { getPrimaryKey } from './getPrimaryKey';
26 import { splitKeys } from './keys';
27 import { decryptMemberToken, encryptMemberToken, generateMemberToken } from './memberToken';
29 export const ORGANIZATION_SIGNATURE_CONTEXT = {
30     SHARE_ORGANIZATION_KEY_TOKEN: 'account.key-token.organization',
31     ORG_KEY_FINGERPRINT_SIGNATURE_CONTEXT: 'account.organization-fingerprint',
34 export const getBackupKeyData = async ({
35     backupPassword,
36     organizationKey,
37 }: {
38     backupPassword: string;
39     organizationKey: PrivateKeyReference;
40 }) => {
41     const backupKeySalt = generateKeySalt();
42     const backupKeyPassword = await computeKeyPassword(backupPassword, backupKeySalt);
43     const backupArmoredPrivateKey = await CryptoProxy.exportPrivateKey({
44         privateKey: organizationKey,
45         passphrase: backupKeyPassword,
46     });
48     return {
49         backupKeySalt,
50         backupArmoredPrivateKey,
51     };
54 export const ORGANIZATION_USERID = 'not_for_email_use@domain.tld';
56 interface GenerateOrganizationKeysArguments {
57     keyPassword: string;
58     backupPassword: string;
59     keyGenConfig: KeyGenConfig;
62 export const generateOrganizationKeys = async ({
63     keyPassword,
64     backupPassword,
65     keyGenConfig,
66 }: GenerateOrganizationKeysArguments) => {
67     const privateKey = await CryptoProxy.generateKey({
68         userIDs: [{ name: ORGANIZATION_USERID, email: ORGANIZATION_USERID }],
69         ...keyGenConfig,
70     });
71     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: keyPassword });
72     return {
73         privateKey,
74         privateKeyArmored,
75         ...(await getBackupKeyData({ backupPassword, organizationKey: privateKey })),
76     };
79 export const generateOrganizationKeyToken = async (userKey: PrivateKeyReference) => {
80     const randomBytes = crypto.getRandomValues(new Uint8Array(32));
81     const token = arrayToHexString(randomBytes);
82     return encryptAddressKeyToken({
83         token,
84         userKey,
85         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
86     });
89 export const generatePasswordlessOrganizationKey = async ({
90     userKey,
91     keyGenConfig,
92 }: {
93     userKey: PrivateKeyReference;
94     keyGenConfig: KeyGenConfig;
95 }) => {
96     if (!userKey) {
97         throw new Error('Missing primary user key');
98     }
99     const { token, encryptedToken, signature } = await generateOrganizationKeyToken(userKey);
100     const privateKey = await CryptoProxy.generateKey({
101         userIDs: [{ name: ORGANIZATION_USERID, email: ORGANIZATION_USERID }],
102         ...keyGenConfig,
103     });
104     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: token });
105     return {
106         privateKey,
107         privateKeyArmored,
108         encryptedToken,
109         signature,
110     };
113 export const reformatOrganizationKey = async (privateKey: PrivateKeyReference, passphrase: string) => {
114     const reformattedPrivateKey = await CryptoProxy.reformatKey({
115         userIDs: [{ name: ORGANIZATION_USERID, email: ORGANIZATION_USERID }],
116         privateKey: privateKey,
117     });
119     const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: reformattedPrivateKey, passphrase });
120     return { privateKey: reformattedPrivateKey, privateKeyArmored };
123 interface ReEncryptOrganizationTokens {
124     publicMembers: { member: Member; memberAddresses: Address[] }[];
125     oldOrganizationKey: KeyPair;
126     newOrganizationKey: KeyPair;
129 export const getReEncryptedPublicMemberTokensPayloadV2 = async ({
130     publicMembers = [],
131     oldOrganizationKey,
132     newOrganizationKey,
133 }: ReEncryptOrganizationTokens): Promise<UpdateOrganizationKeysPayloadV2['Members']> => {
134     const run = async ({ member, memberAddresses }: { member: Member; memberAddresses: Address[] }) => {
135         if (!member.Keys?.length) {
136             return;
137         }
139         const memberKeysAndReEncryptedTokens = await Promise.all(
140             member.Keys.map(async ({ Token, PrivateKey, ID }) => {
141                 if (!Token) {
142                     throw new Error('Missing token');
143                 }
144                 const memberKeyToken = await decryptMemberToken(
145                     Token,
146                     [oldOrganizationKey.privateKey],
147                     [oldOrganizationKey.publicKey]
148                 );
149                 const reEncryptedMemberKeyToken = await encryptMemberToken(
150                     memberKeyToken,
151                     newOrganizationKey.privateKey
152                 );
153                 const privateKey = await CryptoProxy.importPrivateKey({
154                     armoredKey: PrivateKey,
155                     passphrase: memberKeyToken,
156                 });
157                 const publicKey = await CryptoProxy.importPublicKey({
158                     armoredKey: await CryptoProxy.exportPublicKey({ key: privateKey }),
159                 });
160                 return {
161                     ID,
162                     privateKey,
163                     publicKey,
164                     token: reEncryptedMemberKeyToken,
165                 };
166             })
167         );
168         const memberUserKeys = splitKeys(memberKeysAndReEncryptedTokens);
170         const primaryMemberUserKey = getPrimaryKey(memberKeysAndReEncryptedTokens)?.privateKey;
171         if (!primaryMemberUserKey) {
172             throw new Error('Missing primary private user key');
173         }
175         const AddressKeyTokens = (
176             await Promise.all(
177                 memberAddresses.map(async (address) => {
178                     if (!address.Keys?.length) {
179                         return;
180                     }
181                     return Promise.all(
182                         address.Keys.map(async ({ ID, Token, Signature, PrivateKey }) => {
183                             if (!Token) {
184                                 throw new Error('Missing token');
185                             }
186                             const token = await getAddressKeyToken({
187                                 Token,
188                                 Signature,
189                                 organizationKey: oldOrganizationKey,
190                                 privateKeys: memberUserKeys.privateKeys,
191                                 publicKeys: memberUserKeys.publicKeys,
192                             });
193                             await CryptoProxy.clearKey({
194                                 // To ensure it can get decrypted with this token
195                                 key: await CryptoProxy.importPrivateKey({
196                                     armoredKey: PrivateKey,
197                                     passphrase: token,
198                                 }),
199                             });
200                             const result = await encryptAddressKeyToken({
201                                 token,
202                                 organizationKey: newOrganizationKey.privateKey,
203                                 userKey: primaryMemberUserKey,
204                             });
205                             return {
206                                 ID,
207                                 Token: result.encryptedToken,
208                                 Signature: result.signature,
209                                 OrgSignature: result.organizationSignature!,
210                             };
211                         })
212                     );
213                 })
214             )
215         )
216             .filter(isTruthy)
217             .flat();
219         return {
220             ID: member.ID,
221             UserKeyTokens: memberKeysAndReEncryptedTokens.map(({ ID, token }) => {
222                 return {
223                     ID,
224                     Token: token,
225                 };
226             }),
227             AddressKeyTokens,
228         };
229     };
231     const result = await Promise.all(publicMembers.map(run));
233     return result.filter(isTruthy);
236 export const getReEncryptedPublicMemberTokensPayloadLegacy = async ({
237     publicMembers = [],
238     oldOrganizationKey,
239     newOrganizationKey,
240 }: ReEncryptOrganizationTokens) => {
241     let result: UpdateOrganizationKeysPayloadLegacy['Tokens'] = [];
243     // Performed iteratively to not spam the API
244     for (const { member, memberAddresses } of publicMembers) {
245         if (!member.Keys?.length) {
246             continue;
247         }
248         const memberUserAndAddressKeys = memberAddresses.reduce((acc, { Keys: AddressKeys }) => {
249             return acc.concat(AddressKeys);
250         }, member.Keys);
252         const memberResult = await Promise.all(
253             memberUserAndAddressKeys.map(async ({ ID, Token }) => {
254                 if (!Token) {
255                     throw new Error('Missing Token, should never happen');
256                 }
257                 const memberKeyToken = await decryptMemberToken(
258                     Token,
259                     [oldOrganizationKey.privateKey],
260                     [oldOrganizationKey.publicKey]
261                 );
262                 const reEncryptedMemberKeyToken = await encryptMemberToken(
263                     memberKeyToken,
264                     newOrganizationKey.privateKey
265                 );
266                 return { ID, Token: reEncryptedMemberKeyToken };
267             })
268         );
270         result = result.concat(memberResult);
271     }
273     return result;
277  * Generate member address for non-private users.
278  * It requires that the user has been set up with a primary key first.
279  * @param address - The address to generate keys for.
280  * @param primaryKey - The primary key of the member.
281  * @param organizationKey - The organization key.
282  * @param keyGenConfig - The selected encryption config.
283  */
284 interface GenerateMemberAddressKeyArguments {
285     email: string;
286     primaryKey: PrivateKeyReference;
287     organizationKey: PrivateKeyReference;
288     keyGenConfig: KeyGenConfig;
291 export const generateMemberAddressKey = async ({
292     email,
293     primaryKey,
294     organizationKey,
295     keyGenConfig,
296 }: GenerateMemberAddressKeyArguments) => {
297     const memberKeyToken = generateMemberToken();
298     const orgKeyToken = generateMemberToken();
300     const { privateKey, privateKeyArmored } = await generateAddressKey({
301         email,
302         passphrase: memberKeyToken,
303         keyGenConfig,
304     });
306     const privateKeyArmoredOrganization = await CryptoProxy.exportPrivateKey({
307         privateKey,
308         passphrase: orgKeyToken,
309     });
311     const [activationToken, organizationToken] = await Promise.all([
312         encryptMemberToken(memberKeyToken, primaryKey),
313         encryptMemberToken(orgKeyToken, organizationKey),
314     ]);
316     return {
317         privateKey,
318         privateKeyArmored,
319         activationToken,
320         privateKeyArmoredOrganization,
321         organizationToken,
322     };
325 export const getIsPasswordless = (orgKey?: OrganizationKey): orgKey is PasswordlessOrganizationKey => {
326     return !!orgKey && (orgKey.Passwordless || (!!orgKey.Signature && !!orgKey.Token && !!orgKey.PrivateKey));
329 export const reencryptOrganizationToken = async ({
330     Token,
331     decryptionKeys,
332     encryptionKey,
333     signingKey,
334 }: {
335     Token: string;
336     decryptionKeys: PrivateKeyReference[];
337     encryptionKey: PublicKeyReference;
338     signingKey: PrivateKeyReference;
339 }) => {
340     const { sessionKey, message } = await decryptKeyPacket({ armoredMessage: Token, decryptionKeys });
341     return encryptAndSignKeyPacket({
342         sessionKey,
343         binaryData: message.data,
344         encryptionKey,
345         signingKey,
346         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
347     });
350 export const verifyOrganizationTokenSignature = async ({
351     armoredSignature,
352     binaryData,
353     verificationKeys,
354 }: {
355     armoredSignature: string;
356     binaryData: Uint8Array;
357     verificationKeys: PublicKeyReference[];
358 }) => {
359     const result = await CryptoProxy.verifyMessage({
360         armoredSignature,
361         binaryData,
362         verificationKeys,
363         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, required: true },
364     });
366     if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
367         const error = new Error(c('Error').t`Signature verification failed`);
368         error.name = 'SignatureError';
369         throw error;
370     }
373 export const acceptInvitation = async ({
374     Token,
375     Signature,
376     verificationKeys,
377     decryptionKeys,
378     encryptionKey,
379 }: {
380     Token: string;
381     Signature: string;
382     verificationKeys: PublicKeyReference[];
383     decryptionKeys: PrivateKeyReference[];
384     encryptionKey: PrivateKeyReference;
385 }) => {
386     const { sessionKey, message } = await decryptKeyPacket({
387         armoredMessage: Token,
388         decryptionKeys,
389     });
390     await verifyOrganizationTokenSignature({
391         armoredSignature: Signature,
392         binaryData: message.data,
393         verificationKeys,
394     });
395     return encryptAndSignKeyPacket({
396         sessionKey,
397         binaryData: message.data,
398         encryptionKey: encryptionKey,
399         signingKey: encryptionKey,
400         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
401     });
404 export const getVerifiedPublicKeys = async ({
405     api,
406     email,
407     verifyOutboundPublicKeys,
408     userContext,
409 }: {
410     email: string;
411     api: Api;
412     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
413     userContext: KTUserContext | undefined;
414 }) => {
415     if (!email) {
416         throw new Error('Missing email');
417     }
419     const { addressKeys } = await getAndVerifyApiKeys({
420         api,
421         email,
422         verifyOutboundPublicKeys,
423         userContext,
424         internalKeysOnly: false,
425         noCache: true,
426     });
428     return addressKeys;
431 export const generatePrivateMemberInvitation = async ({
432     signer,
433     data,
434     member,
435     publicKey,
436     addressID,
437 }: {
438     signer: {
439         privateKey: PrivateKeyReference;
440         addressID: string;
441     };
442     data: {
443         sessionKey: SessionKey;
444         binaryData: Uint8Array;
445     };
446     member: Member;
447     addressID: string;
448     publicKey: PublicKeyReference;
449 }) => {
450     const result = await encryptAndSignKeyPacket({
451         sessionKey: data.sessionKey,
452         binaryData: data.binaryData,
453         encryptionKey: publicKey,
454         signingKey: signer.privateKey,
455         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
456     });
457     return {
458         MemberID: member.ID,
459         TokenKeyPacket: result.keyPacket,
460         Signature: result.signature,
461         SignatureAddressID: signer.addressID,
462         EncryptionAddressID: addressID,
463     };
466 export const generatePublicMemberInvitation = async ({
467     member,
468     data,
469     privateKey,
470 }: {
471     member: Member;
472     privateKey: PrivateKeyReference;
473     data: {
474         sessionKey: SessionKey;
475         binaryData: Uint8Array;
476     };
477 }) => {
478     const result = await encryptAndSignKeyPacket({
479         sessionKey: data.sessionKey,
480         binaryData: data.binaryData,
481         encryptionKey: privateKey,
482         signingKey: privateKey,
483         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
484     });
485     return {
486         MemberID: member.ID,
487         TokenKeyPacket: result.keyPacket,
488         Signature: result.signature,
489     };
492 export const generateOrganizationKeySignature = async ({
493     signingKeys,
494     organizationKey,
495 }: {
496     signingKeys: PrivateKeyReference;
497     organizationKey: PrivateKeyReference;
498 }) => {
499     const [fingerprint] = await CryptoProxy.getSHA256Fingerprints({ key: organizationKey });
500     const signature = await CryptoProxy.signMessage({
501         signingKeys,
502         detached: true,
503         textData: fingerprint,
504         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.ORG_KEY_FINGERPRINT_SIGNATURE_CONTEXT, critical: true },
505     });
506     return signature;
509 export const validateOrganizationKeySignature = async ({
510     armoredSignature,
511     verificationKeys,
512     organizationKey,
513 }: {
514     armoredSignature: string;
515     verificationKeys: PublicKeyReference[];
516     organizationKey: PublicKeyReference;
517 }) => {
518     const [fingerprint] = await CryptoProxy.getSHA256Fingerprints({ key: organizationKey });
519     const result = await CryptoProxy.verifyMessage({
520         armoredSignature,
521         textData: fingerprint,
522         verificationKeys,
523         context: { value: ORGANIZATION_SIGNATURE_CONTEXT.ORG_KEY_FINGERPRINT_SIGNATURE_CONTEXT, required: true },
524     });
526     if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
527         const error = new Error(c('Error').t`Signature verification failed`);
528         error.name = 'SignatureError';
529         throw error;
530     }
533 export enum OrganizationSignatureState {
534     publicKeys,
535     valid,
536     error,
539 export const validateOrganizationSignatureHelper = async ({
540     email,
541     privateKey,
542     armoredSignature,
543     verifyOutboundPublicKeys,
544     api,
545 }: {
546     email: string;
547     privateKey: PrivateKeyReference;
548     armoredSignature: string;
549     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
550     api: Api;
551 }) => {
552     const silentApi = getSilentApi(api);
554     const adminEmailPublicKeys = (
555         await getVerifiedPublicKeys({
556             api: silentApi,
557             email,
558             verifyOutboundPublicKeys,
559             // In app context, can use default
560             userContext: undefined,
561         })
562     ).map(({ publicKey }) => publicKey);
564     if (!adminEmailPublicKeys.length) {
565         return {
566             state: OrganizationSignatureState.publicKeys,
567         };
568     }
570     try {
571         await validateOrganizationKeySignature({
572             verificationKeys: adminEmailPublicKeys,
573             organizationKey: privateKey,
574             armoredSignature,
575         });
576         return {
577             state: OrganizationSignatureState.valid,
578         };
579     } catch (e) {
580         return {
581             state: OrganizationSignatureState.error,
582         };
583     }