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';
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 ({
38 backupPassword: string;
39 organizationKey: PrivateKeyReference;
41 const backupKeySalt = generateKeySalt();
42 const backupKeyPassword = await computeKeyPassword(backupPassword, backupKeySalt);
43 const backupArmoredPrivateKey = await CryptoProxy.exportPrivateKey({
44 privateKey: organizationKey,
45 passphrase: backupKeyPassword,
50 backupArmoredPrivateKey,
54 export const ORGANIZATION_USERID = 'not_for_email_use@domain.tld';
56 interface GenerateOrganizationKeysArguments {
58 backupPassword: string;
59 keyGenConfig: KeyGenConfig;
62 export const generateOrganizationKeys = async ({
66 }: GenerateOrganizationKeysArguments) => {
67 const privateKey = await CryptoProxy.generateKey({
68 userIDs: [{ name: ORGANIZATION_USERID, email: ORGANIZATION_USERID }],
71 const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: keyPassword });
75 ...(await getBackupKeyData({ backupPassword, organizationKey: privateKey })),
79 export const generateOrganizationKeyToken = async (userKey: PrivateKeyReference) => {
80 const randomBytes = crypto.getRandomValues(new Uint8Array(32));
81 const token = arrayToHexString(randomBytes);
82 return encryptAddressKeyToken({
85 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
89 export const generatePasswordlessOrganizationKey = async ({
93 userKey: PrivateKeyReference;
94 keyGenConfig: KeyGenConfig;
97 throw new Error('Missing primary user key');
99 const { token, encryptedToken, signature } = await generateOrganizationKeyToken(userKey);
100 const privateKey = await CryptoProxy.generateKey({
101 userIDs: [{ name: ORGANIZATION_USERID, email: ORGANIZATION_USERID }],
104 const privateKeyArmored = await CryptoProxy.exportPrivateKey({ privateKey: privateKey, passphrase: token });
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,
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 ({
133 }: ReEncryptOrganizationTokens): Promise<UpdateOrganizationKeysPayloadV2['Members']> => {
134 const run = async ({ member, memberAddresses }: { member: Member; memberAddresses: Address[] }) => {
135 if (!member.Keys?.length) {
139 const memberKeysAndReEncryptedTokens = await Promise.all(
140 member.Keys.map(async ({ Token, PrivateKey, ID }) => {
142 throw new Error('Missing token');
144 const memberKeyToken = await decryptMemberToken(
146 [oldOrganizationKey.privateKey],
147 [oldOrganizationKey.publicKey]
149 const reEncryptedMemberKeyToken = await encryptMemberToken(
151 newOrganizationKey.privateKey
153 const privateKey = await CryptoProxy.importPrivateKey({
154 armoredKey: PrivateKey,
155 passphrase: memberKeyToken,
157 const publicKey = await CryptoProxy.importPublicKey({
158 armoredKey: await CryptoProxy.exportPublicKey({ key: privateKey }),
164 token: reEncryptedMemberKeyToken,
168 const memberUserKeys = splitKeys(memberKeysAndReEncryptedTokens);
170 const primaryMemberUserKey = getPrimaryKey(memberKeysAndReEncryptedTokens)?.privateKey;
171 if (!primaryMemberUserKey) {
172 throw new Error('Missing primary private user key');
175 const AddressKeyTokens = (
177 memberAddresses.map(async (address) => {
178 if (!address.Keys?.length) {
182 address.Keys.map(async ({ ID, Token, Signature, PrivateKey }) => {
184 throw new Error('Missing token');
186 const token = await getAddressKeyToken({
189 organizationKey: oldOrganizationKey,
190 privateKeys: memberUserKeys.privateKeys,
191 publicKeys: memberUserKeys.publicKeys,
193 await CryptoProxy.clearKey({
194 // To ensure it can get decrypted with this token
195 key: await CryptoProxy.importPrivateKey({
196 armoredKey: PrivateKey,
200 const result = await encryptAddressKeyToken({
202 organizationKey: newOrganizationKey.privateKey,
203 userKey: primaryMemberUserKey,
207 Token: result.encryptedToken,
208 Signature: result.signature,
209 OrgSignature: result.organizationSignature!,
221 UserKeyTokens: memberKeysAndReEncryptedTokens.map(({ ID, token }) => {
231 const result = await Promise.all(publicMembers.map(run));
233 return result.filter(isTruthy);
236 export const getReEncryptedPublicMemberTokensPayloadLegacy = async ({
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) {
248 const memberUserAndAddressKeys = memberAddresses.reduce((acc, { Keys: AddressKeys }) => {
249 return acc.concat(AddressKeys);
252 const memberResult = await Promise.all(
253 memberUserAndAddressKeys.map(async ({ ID, Token }) => {
255 throw new Error('Missing Token, should never happen');
257 const memberKeyToken = await decryptMemberToken(
259 [oldOrganizationKey.privateKey],
260 [oldOrganizationKey.publicKey]
262 const reEncryptedMemberKeyToken = await encryptMemberToken(
264 newOrganizationKey.privateKey
266 return { ID, Token: reEncryptedMemberKeyToken };
270 result = result.concat(memberResult);
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.
284 interface GenerateMemberAddressKeyArguments {
286 primaryKey: PrivateKeyReference;
287 organizationKey: PrivateKeyReference;
288 keyGenConfig: KeyGenConfig;
291 export const generateMemberAddressKey = async ({
296 }: GenerateMemberAddressKeyArguments) => {
297 const memberKeyToken = generateMemberToken();
298 const orgKeyToken = generateMemberToken();
300 const { privateKey, privateKeyArmored } = await generateAddressKey({
302 passphrase: memberKeyToken,
306 const privateKeyArmoredOrganization = await CryptoProxy.exportPrivateKey({
308 passphrase: orgKeyToken,
311 const [activationToken, organizationToken] = await Promise.all([
312 encryptMemberToken(memberKeyToken, primaryKey),
313 encryptMemberToken(orgKeyToken, organizationKey),
320 privateKeyArmoredOrganization,
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 ({
336 decryptionKeys: PrivateKeyReference[];
337 encryptionKey: PublicKeyReference;
338 signingKey: PrivateKeyReference;
340 const { sessionKey, message } = await decryptKeyPacket({ armoredMessage: Token, decryptionKeys });
341 return encryptAndSignKeyPacket({
343 binaryData: message.data,
346 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
350 export const verifyOrganizationTokenSignature = async ({
355 armoredSignature: string;
356 binaryData: Uint8Array;
357 verificationKeys: PublicKeyReference[];
359 const result = await CryptoProxy.verifyMessage({
363 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, required: true },
366 if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
367 const error = new Error(c('Error').t`Signature verification failed`);
368 error.name = 'SignatureError';
373 export const acceptInvitation = async ({
382 verificationKeys: PublicKeyReference[];
383 decryptionKeys: PrivateKeyReference[];
384 encryptionKey: PrivateKeyReference;
386 const { sessionKey, message } = await decryptKeyPacket({
387 armoredMessage: Token,
390 await verifyOrganizationTokenSignature({
391 armoredSignature: Signature,
392 binaryData: message.data,
395 return encryptAndSignKeyPacket({
397 binaryData: message.data,
398 encryptionKey: encryptionKey,
399 signingKey: encryptionKey,
400 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.SHARE_ORGANIZATION_KEY_TOKEN, critical: true },
404 export const getVerifiedPublicKeys = async ({
407 verifyOutboundPublicKeys,
412 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
413 userContext: KTUserContext | undefined;
416 throw new Error('Missing email');
419 const { addressKeys } = await getAndVerifyApiKeys({
422 verifyOutboundPublicKeys,
424 internalKeysOnly: false,
431 export const generatePrivateMemberInvitation = async ({
439 privateKey: PrivateKeyReference;
443 sessionKey: SessionKey;
444 binaryData: Uint8Array;
448 publicKey: PublicKeyReference;
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 },
459 TokenKeyPacket: result.keyPacket,
460 Signature: result.signature,
461 SignatureAddressID: signer.addressID,
462 EncryptionAddressID: addressID,
466 export const generatePublicMemberInvitation = async ({
472 privateKey: PrivateKeyReference;
474 sessionKey: SessionKey;
475 binaryData: Uint8Array;
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 },
487 TokenKeyPacket: result.keyPacket,
488 Signature: result.signature,
492 export const generateOrganizationKeySignature = async ({
496 signingKeys: PrivateKeyReference;
497 organizationKey: PrivateKeyReference;
499 const [fingerprint] = await CryptoProxy.getSHA256Fingerprints({ key: organizationKey });
500 const signature = await CryptoProxy.signMessage({
503 textData: fingerprint,
504 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.ORG_KEY_FINGERPRINT_SIGNATURE_CONTEXT, critical: true },
509 export const validateOrganizationKeySignature = async ({
514 armoredSignature: string;
515 verificationKeys: PublicKeyReference[];
516 organizationKey: PublicKeyReference;
518 const [fingerprint] = await CryptoProxy.getSHA256Fingerprints({ key: organizationKey });
519 const result = await CryptoProxy.verifyMessage({
521 textData: fingerprint,
523 context: { value: ORGANIZATION_SIGNATURE_CONTEXT.ORG_KEY_FINGERPRINT_SIGNATURE_CONTEXT, required: true },
526 if (result.verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
527 const error = new Error(c('Error').t`Signature verification failed`);
528 error.name = 'SignatureError';
533 export enum OrganizationSignatureState {
539 export const validateOrganizationSignatureHelper = async ({
543 verifyOutboundPublicKeys,
547 privateKey: PrivateKeyReference;
548 armoredSignature: string;
549 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
552 const silentApi = getSilentApi(api);
554 const adminEmailPublicKeys = (
555 await getVerifiedPublicKeys({
558 verifyOutboundPublicKeys,
559 // In app context, can use default
560 userContext: undefined,
562 ).map(({ publicKey }) => publicKey);
564 if (!adminEmailPublicKeys.length) {
566 state: OrganizationSignatureState.publicKeys,
571 await validateOrganizationKeySignature({
572 verificationKeys: adminEmailPublicKeys,
573 organizationKey: privateKey,
577 state: OrganizationSignatureState.valid,
581 state: OrganizationSignatureState.error,