DRVDOC-1129: Clicking on editor margins should focus editor
[ProtonMail-WebClient.git] / packages / account / organizationKey / actions.ts
blob5824fdb49d56af3976222eb74a53a20f8a97b0ab
1 import type { ThunkAction, UnknownAction } from '@reduxjs/toolkit';
2 import { c } from 'ttag';
4 import { CryptoProxy, type PrivateKeyReference, type PublicKeyReference } from '@proton/crypto';
5 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
6 import { CacheType } from '@proton/redux-utilities';
7 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
8 import { activatePasswordlessKey, updateRolePasswordless } from '@proton/shared/lib/api/members';
9 import {
10     createPasswordlessOrganizationKeys as createPasswordlessOrganizationKeysConfig,
11     migratePasswordlessOrganizationKey,
12     updateOrganizationKeysLegacy,
13     updateOrganizationKeysV2,
14     updatePasswordlessOrganizationKeys as updatePasswordlessOrganizationKeysConfig,
15     uploadOrganizationKeySignature,
16 } from '@proton/shared/lib/api/organization';
17 import { KEYGEN_CONFIGS, KEYGEN_TYPES, MEMBER_PRIVATE, MEMBER_ROLE } from '@proton/shared/lib/constants';
18 import { getIsAddressConfirmed, getIsAddressEnabled } from '@proton/shared/lib/helpers/address';
19 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
20 import type {
21     Address,
22     Api,
23     CachedOrganizationKey,
24     EnhancedMember,
25     Member,
26     VerifyOutboundPublicKeys,
27 } from '@proton/shared/lib/interfaces';
28 import { MEMBER_ORG_KEY_STATE } from '@proton/shared/lib/interfaces';
29 import {
30     acceptInvitation,
31     generateOrganizationKeySignature,
32     generateOrganizationKeyToken,
33     generateOrganizationKeys,
34     generatePasswordlessOrganizationKey,
35     generatePrivateMemberInvitation,
36     generatePublicMemberInvitation,
37     getDecryptedUserKeys,
38     getHasMigratedAddressKeys,
39     getIsPasswordless,
40     getOrganizationKeyToken,
41     getPrimaryKey,
42     getReEncryptedPublicMemberTokensPayloadLegacy,
43     getReEncryptedPublicMemberTokensPayloadV2,
44     getSentryError,
45     getVerifiedPublicKeys,
46     splitKeys,
47 } from '@proton/shared/lib/keys';
48 import { decryptKeyPacket } from '@proton/shared/lib/keys/keypacket';
49 import isTruthy from '@proton/utils/isTruthy';
51 import { addressKeysThunk } from '../addressKeys';
52 import { addressesThunk } from '../addresses';
53 import { getMemberAddresses, membersThunk } from '../members';
54 import { userKeysThunk } from '../userKeys';
55 import { type OrganizationKeyState, organizationKeyThunk } from './index';
57 const keyGenConfig = KEYGEN_CONFIGS[KEYGEN_TYPES.CURVE25519];
59 export const getPrivateAdminError = () => {
60     return c('passwordless').t`Private users can be promoted to admin when they've signed in for the first time`;
63 export const getPrivateText = () => {
64     return c('unprivatization').t`Admins can't access the data of private users and can't reset their password.`;
67 export const getPublicAdminError = () => {
68     return c('passwordless').t`Non-private users can be promoted to admin when they have setup keys`;
71 export const getPrivatizeError = () => {
72     return c('passwordless').t`You must privatize all users before generating a new organization key`;
75 export const getOrganizationTokenThunk = (): ThunkAction<
76     Promise<string>,
77     OrganizationKeyState,
78     ProtonThunkArguments,
79     UnknownAction
80 > => {
81     return async (dispatch, _, extra) => {
82         const key = await dispatch(organizationKeyThunk());
83         const userKeys = await dispatch(userKeysThunk());
84         const keyPassword = extra.authentication.getPassword();
85         return getOrganizationKeyToken({ userKeys, keyPassword, Key: key?.Key });
86     };
89 export interface PublicMemberKeyPayload {
90     type: 0;
91     member: Member;
92     email: string;
93     address: Address | undefined;
94     privateKey: PrivateKeyReference;
97 export interface PrivateMemberKeyPayload {
98     type: 1;
99     member: Member;
100     email: string;
101     address: Address;
102     publicKey: PublicKeyReference;
105 export type MemberKeyPayload = PrivateMemberKeyPayload | PublicMemberKeyPayload;
107 export type PublicMembersReEncryptPayload = {
108     member: Member;
109     memberAddresses: Address[];
110 }[];
112 export interface OrganizationKeyRotationPayload {
113     publicMembersToReEncryptPayload: PublicMembersReEncryptPayload;
114     memberKeyPayloads: MemberKeyPayload[];
117 // Error that can be ignored when e.g. rotating org keys
118 class ConstraintError extends Error {}
120 export const getMemberKeyPayload = async ({
121     organizationKey,
122     api,
123     mode,
124     member,
125     memberAddresses,
126 }: {
127     organizationKey: CachedOrganizationKey;
128     mode:
129         | {
130               type: 'email';
131               verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
132           }
133         | {
134               type: 'org-key';
135               publicKey: PublicKeyReference;
136           };
137     api: Api;
138     member: Member;
139     memberAddresses: Address[];
140 }): Promise<MemberKeyPayload> => {
141     const address: Address | undefined = memberAddresses.filter(
142         (address) => getIsAddressEnabled(address) && address.HasKeys
143     )[0];
145     if (member.Private === MEMBER_PRIVATE.READABLE) {
146         if (!member.Keys?.length) {
147             throw new ConstraintError(getPublicAdminError());
148         }
149         // Only needed to decrypt the user keys here.
150         if (!organizationKey?.privateKey) {
151             throw new Error(c('passwordless').t`Organization key must be activated to give admin privileges`);
152         }
153         const memberUserKeys = await getDecryptedUserKeys(member.Keys, '', organizationKey);
154         const privateKey = getPrimaryKey(memberUserKeys)?.privateKey;
155         if (!privateKey) {
156             throw new Error('Unable to decrypt non-private user keys');
157         }
158         return {
159             type: 0,
160             member,
161             address,
162             email: address?.Email || member.Name,
163             privateKey,
164         };
165     }
167     if (mode.type === 'org-key') {
168         return {
169             type: 1,
170             member,
171             address: {} as any, // Unused
172             email: 'unused',
173             publicKey: mode.publicKey,
174         };
175     }
177     if (!address) {
178         throw new ConstraintError(getPrivateAdminError());
179     }
180     const email = address.Email;
181     const memberPublicKey = (
182         await getVerifiedPublicKeys({
183             api,
184             verifyOutboundPublicKeys: mode.verifyOutboundPublicKeys,
185             email,
186             // In app context, can use default
187             userContext: undefined,
188         })
189     )[0]?.publicKey;
190     if (!memberPublicKey) {
191         throw new Error(getPrivateAdminError());
192     }
193     return {
194         type: 1,
195         member,
196         address,
197         email,
198         publicKey: memberPublicKey,
199     };
202 const getReEncryptedAdminTokens = async ({
203     armoredMessage,
204     decryptionKeys,
205     address,
206     memberKeyPayloads,
207 }: {
208     armoredMessage: string;
209     decryptionKeys: PrivateKeyReference[];
210     address: { ID: string; privateKey: PrivateKeyReference };
211     memberKeyPayloads: MemberKeyPayload[];
212 }) => {
213     const { sessionKey, message } = await decryptKeyPacket({
214         armoredMessage,
215         decryptionKeys,
216     });
217     const data = {
218         sessionKey,
219         binaryData: message.data,
220     };
221     const signer = {
222         addressID: address.ID,
223         privateKey: address.privateKey,
224     };
226     const { privateAdminPromises, publicAdminPromises } = memberKeyPayloads.reduce<{
227         privateAdminPromises: ReturnType<typeof generatePrivateMemberInvitation>[];
228         publicAdminPromises: ReturnType<typeof generatePublicMemberInvitation>[];
229     }>(
230         (acc, memberPayload) => {
231             if (memberPayload.type === 1) {
232                 const { member, publicKey, address } = memberPayload;
233                 acc.privateAdminPromises.push(
234                     generatePrivateMemberInvitation({
235                         member,
236                         publicKey,
237                         addressID: address.ID,
238                         signer,
239                         data,
240                     })
241                 );
242             } else {
243                 const { member, privateKey } = memberPayload;
244                 acc.publicAdminPromises.push(
245                     generatePublicMemberInvitation({
246                         member,
247                         privateKey,
248                         data,
249                     })
250                 );
251             }
253             return acc;
254         },
255         { privateAdminPromises: [], publicAdminPromises: [] }
256     );
258     const [privateAdminInvitations, publicAdminActivations] = await Promise.all([
259         Promise.all(privateAdminPromises),
260         Promise.all(publicAdminPromises),
261     ]);
263     return { privateAdminInvitations, publicAdminActivations };
266 const getReEncryptedMemberTokens = async ({
267     publicMembersToReEncryptPayload,
268     newOrganizationKey,
269     oldOrganizationKey,
270 }: {
271     publicMembersToReEncryptPayload: PublicMembersReEncryptPayload;
272     oldOrganizationKey: CachedOrganizationKey;
273     newOrganizationKey: { privateKey: PrivateKeyReference; privateKeyArmored: string };
274 }): Promise<Parameters<typeof updatePasswordlessOrganizationKeysConfig>[0]['Members']> => {
275     if (!publicMembersToReEncryptPayload.length) {
276         return [];
277     }
278     if (!oldOrganizationKey?.privateKey) {
279         throw new Error('Public members received without an existing organization key.');
280     }
281     const publicKey = await CryptoProxy.importPublicKey({ armoredKey: newOrganizationKey.privateKeyArmored });
282     return getReEncryptedPublicMemberTokensPayloadV2({
283         publicMembers: publicMembersToReEncryptPayload,
284         oldOrganizationKey: oldOrganizationKey,
285         newOrganizationKey: { privateKey: newOrganizationKey.privateKey, publicKey },
286     });
289 type ConfirmPromotionMemberAction = {
290     type: 'confirm-promote';
291     payload: MemberKeyPayload;
292     prompt: boolean;
294 type ConfirmDemotionMemberAction = {
295     type: 'confirm-demote';
296     payload: null;
298 export type MemberPromptAction = ConfirmPromotionMemberAction | ConfirmDemotionMemberAction;
300 export const getMemberEditPayload = ({
301     verifyOutboundPublicKeys,
302     member,
303     memberDiff,
304     api,
305 }: {
306     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
307     member: EnhancedMember;
308     memberDiff: Partial<{
309         role: MEMBER_ROLE;
310     }>;
311     api: Api;
312 }): ThunkAction<Promise<MemberPromptAction | null>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
313     return async (dispatch) => {
314         const organizationKey = await dispatch(organizationKeyThunk());
315         const passwordlessMode = getIsPasswordless(organizationKey?.Key);
317         if (memberDiff.role === MEMBER_ROLE.ORGANIZATION_MEMBER) {
318             return {
319                 type: 'confirm-demote',
320                 payload: null,
321             };
322         }
324         if (memberDiff.role === MEMBER_ROLE.ORGANIZATION_ADMIN && passwordlessMode) {
325             const payload = await getMemberKeyPayload({
326                 organizationKey,
327                 mode: {
328                     type: 'email',
329                     verifyOutboundPublicKeys,
330                 },
331                 api,
332                 member,
333                 memberAddresses: await dispatch(getMemberAddresses({ member, retry: true })),
334             });
336             if (member.Private === MEMBER_PRIVATE.UNREADABLE) {
337                 return {
338                     type: 'confirm-promote',
339                     payload,
340                     prompt: true,
341                 };
342             }
344             return {
345                 type: 'confirm-promote',
346                 payload,
347                 prompt: false,
348             };
349         }
351         return null;
352     };
355 export const getMemberKeyPayloads = ({
356     mode,
357     members,
358     api,
359     ignorePasswordlessValidation,
360     ignoreErrors,
361 }: {
362     mode: Parameters<typeof getMemberKeyPayload>[0]['mode'];
363     members: EnhancedMember[];
364     api: Api;
365     ignorePasswordlessValidation?: boolean;
366     ignoreErrors?: boolean;
367 }): ThunkAction<Promise<MemberKeyPayload[]>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
368     return async (dispatch) => {
369         const organizationKey = await dispatch(organizationKeyThunk());
370         if (!organizationKey) {
371             throw new Error('Org key loading');
372         }
373         if (!getIsPasswordless(organizationKey?.Key) && !ignorePasswordlessValidation) {
374             throw new Error('Only used on passwordless organizations');
375         }
376         return (
377             await Promise.all(
378                 members.map(async (member) => {
379                     try {
380                         return await getMemberKeyPayload({
381                             member,
382                             memberAddresses: await dispatch(getMemberAddresses({ member, retry: true })),
383                             mode,
384                             api,
385                             organizationKey,
386                         });
387                     } catch (e) {
388                         if (e instanceof ConstraintError) {
389                             return undefined;
390                         }
391                         if (ignoreErrors) {
392                             return undefined;
393                         }
394                         throw e;
395                     }
396                 })
397             )
398         ).filter(isTruthy);
399     };
402 export const getPublicMembersToReEncryptPayload = (): ThunkAction<
403     Promise<PublicMembersReEncryptPayload>,
404     OrganizationKeyState,
405     ProtonThunkArguments,
406     UnknownAction
407 > => {
408     return async (dispatch) => {
409         const organizationKey = await dispatch(organizationKeyThunk());
410         const members = await dispatch(membersThunk());
412         const publicMembers = members.filter((member) => member.Private === MEMBER_PRIVATE.READABLE);
414         if (publicMembers.length >= 1) {
415             if (!organizationKey?.privateKey) {
416                 throw new Error(getPrivatizeError());
417             }
418             const publicMembersToReEncrypt = await Promise.all(
419                 publicMembers.map(async (member) => {
420                     if (!member.Keys?.length) {
421                         return null;
422                     }
423                     if (!organizationKey?.privateKey) {
424                         throw new Error(getPrivatizeError());
425                     }
426                     const memberUserKeys = await getDecryptedUserKeys(member.Keys, '', organizationKey);
427                     const privateKey = getPrimaryKey(memberUserKeys)?.privateKey;
428                     if (!privateKey) {
429                         throw new Error('Missing private key');
430                     }
431                     const memberAddresses = await dispatch(getMemberAddresses({ member, retry: true }));
432                     return {
433                         member,
434                         memberUserKeys,
435                         memberAddresses,
436                     };
437                 })
438             );
439             return publicMembersToReEncrypt.filter(isTruthy);
440         }
442         return [];
443     };
446 export const getKeyRotationPayload = ({
447     verifyOutboundPublicKeys,
448     api,
449     ignorePasswordlessValidation,
450 }: {
451     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
452     api: Api;
453     ignorePasswordlessValidation?: boolean;
454 }): ThunkAction<Promise<OrganizationKeyRotationPayload>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
455     return async (dispatch) => {
456         const userKeys = await dispatch(userKeysThunk());
457         const organizationKey = await dispatch(organizationKeyThunk());
458         if (!getIsPasswordless(organizationKey?.Key) && !ignorePasswordlessValidation) {
459             throw new Error('Only used on passwordless organizations');
460         }
461         const userKey = userKeys[0]?.privateKey;
462         if (!userKey) {
463             throw new Error('Missing primary user key');
464         }
465         const [primaryAddress] = await dispatch(addressesThunk());
466         if (!primaryAddress) {
467             throw new Error('Missing primary address');
468         }
469         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
470         if (!primaryAddressKey) {
471             throw new Error('Missing primary address key');
472         }
473         const members = await dispatch(membersThunk());
474         const otherAdminMembers = members.filter((member) => {
475             return member.Role === MEMBER_ROLE.ORGANIZATION_ADMIN && !member.Self;
476         });
478         const [memberKeyPayloads, publicMembersToReEncryptPayload] = await Promise.all([
479             dispatch(
480                 getMemberKeyPayloads({
481                     api,
482                     mode: {
483                         type: 'email',
484                         verifyOutboundPublicKeys,
485                     },
486                     members: otherAdminMembers,
487                     ignorePasswordlessValidation,
488                 })
489             ),
490             dispatch(getPublicMembersToReEncryptPayload()),
491         ]);
493         return {
494             memberKeyPayloads,
495             publicMembersToReEncryptPayload,
496         };
497     };
499 export const setAdminRoles = ({
500     memberKeyPayloads,
501     api,
502 }: { memberKeyPayloads: MemberKeyPayload[] } & {
503     api: Api;
504 }): ThunkAction<Promise<void>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
505     return async (dispatch) => {
506         const userKeys = await dispatch(userKeysThunk());
507         const organizationKey = await dispatch(organizationKeyThunk());
508         if (!getIsPasswordless(organizationKey?.Key)) {
509             throw new Error('Only used on passwordless organizations');
510         }
511         if (!organizationKey?.privateKey) {
512             throw new Error('Organization key must be activated to set admin role');
513         }
514         const userKey = userKeys[0]?.privateKey;
515         if (!userKey) {
516             throw new Error('Missing primary user key');
517         }
518         const [primaryAddress] = await dispatch(addressesThunk());
519         if (!primaryAddress) {
520             throw new Error('Missing primary address');
521         }
522         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
523         if (!primaryAddressKey) {
524             throw new Error('Missing primary address key');
525         }
527         const { privateAdminInvitations, publicAdminActivations } = await getReEncryptedAdminTokens({
528             armoredMessage: organizationKey.Key.Token,
529             decryptionKeys: userKeys.map(({ privateKey }) => privateKey),
530             address: { ID: primaryAddress.ID, privateKey: primaryAddressKey.privateKey },
531             memberKeyPayloads,
532         });
534         const privatePromise = Promise.all(
535             privateAdminInvitations.map((invitation) => {
536                 return api(
537                     updateRolePasswordless({
538                         memberID: invitation.MemberID,
539                         Role: MEMBER_ROLE.ORGANIZATION_ADMIN,
540                         OrganizationKeyInvitation: {
541                             TokenKeyPacket: invitation.TokenKeyPacket,
542                             Signature: invitation.Signature,
543                             SignatureAddressID: invitation.SignatureAddressID,
544                             EncryptionAddressID: invitation.EncryptionAddressID,
545                         },
546                     })
547                 );
548             })
549         );
551         const publicPromise = Promise.all(
552             publicAdminActivations.map((activation) => {
553                 return api(
554                     updateRolePasswordless({
555                         memberID: activation.MemberID,
556                         Role: MEMBER_ROLE.ORGANIZATION_ADMIN,
557                         OrganizationKeyActivation: {
558                             TokenKeyPacket: activation.TokenKeyPacket,
559                             Signature: activation.Signature,
560                         },
561                     })
562                 );
563             })
564         );
566         await Promise.all([privatePromise, publicPromise]);
567     };
570 export const rotateOrganizationKeys = ({
571     password: newPassword,
572 }: {
573     password: string;
574 }): ThunkAction<
575     Promise<ReturnType<typeof updateOrganizationKeysV2> | ReturnType<typeof updateOrganizationKeysLegacy>>,
576     OrganizationKeyState,
577     ProtonThunkArguments,
578     UnknownAction
579 > => {
580     return async (dispatch, getState, extra) => {
581         const organizationKey = await dispatch(organizationKeyThunk());
583         const keyPassword = extra.authentication.getPassword();
584         const addresses = await dispatch(addressesThunk());
586         const publicMembersToReEncrypt = await dispatch(getPublicMembersToReEncryptPayload());
588         const { privateKey, privateKeyArmored, backupKeySalt, backupArmoredPrivateKey } =
589             await generateOrganizationKeys({
590                 keyPassword,
591                 backupPassword: newPassword,
592                 keyGenConfig,
593             });
595         const publicKey = await CryptoProxy.importPublicKey({ armoredKey: privateKeyArmored });
597         if (getHasMigratedAddressKeys(addresses)) {
598             let members: Parameters<typeof updateOrganizationKeysV2>[0]['Members'] = [];
599             if (publicMembersToReEncrypt.length >= 1) {
600                 if (!organizationKey?.privateKey) {
601                     throw new Error(getPrivatizeError());
602                 }
603                 members = await getReEncryptedPublicMemberTokensPayloadV2({
604                     publicMembers: publicMembersToReEncrypt,
605                     oldOrganizationKey: organizationKey,
606                     newOrganizationKey: { privateKey, publicKey },
607                 });
608             }
609             return updateOrganizationKeysV2({
610                 PrivateKey: privateKeyArmored,
611                 BackupPrivateKey: backupArmoredPrivateKey,
612                 BackupKeySalt: backupKeySalt,
613                 Members: members,
614             });
615         }
617         let tokens: Parameters<typeof updateOrganizationKeysLegacy>[0]['Tokens'] = [];
618         if (publicMembersToReEncrypt.length >= 1) {
619             if (!organizationKey?.privateKey) {
620                 throw new Error(getPrivatizeError());
621             }
622             tokens = await getReEncryptedPublicMemberTokensPayloadLegacy({
623                 publicMembers: publicMembersToReEncrypt,
624                 oldOrganizationKey: organizationKey,
625                 newOrganizationKey: { privateKey, publicKey },
626             });
627         }
628         return updateOrganizationKeysLegacy({
629             PrivateKey: privateKeyArmored,
630             BackupPrivateKey: backupArmoredPrivateKey,
631             BackupKeySalt: backupKeySalt,
632             Tokens: tokens,
633         });
634     };
637 export const createPasswordlessOrganizationKeys = ({
638     publicMembersToReEncryptPayload,
639     memberKeyPayloads,
640 }: OrganizationKeyRotationPayload): ThunkAction<
641     Promise<ReturnType<typeof createPasswordlessOrganizationKeysConfig>>,
642     OrganizationKeyState,
643     ProtonThunkArguments,
644     UnknownAction
645 > => {
646     return async (dispatch) => {
647         const userKeys = await dispatch(userKeysThunk());
648         const organizationKey = await dispatch(organizationKeyThunk());
649         if (!organizationKey) {
650             throw new Error('Org key loading');
651         }
652         const userKey = userKeys[0]?.privateKey;
653         if (!userKey) {
654             throw new Error('Missing primary user key');
655         }
656         const [primaryAddress] = await dispatch(addressesThunk());
657         if (!primaryAddress) {
658             throw new Error('Missing primary address');
659         }
660         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
661         if (!primaryAddressKey) {
662             throw new Error('Missing primary address key');
663         }
664         const { encryptedToken, signature, privateKey, privateKeyArmored } = await generatePasswordlessOrganizationKey({
665             userKey,
666             keyGenConfig,
667         });
668         const { publicAdminActivations, privateAdminInvitations } = await getReEncryptedAdminTokens({
669             armoredMessage: encryptedToken,
670             decryptionKeys: [userKey],
671             address: { ID: primaryAddress.ID, privateKey: primaryAddressKey.privateKey },
672             memberKeyPayloads,
673         });
674         const memberTokens = await getReEncryptedMemberTokens({
675             publicMembersToReEncryptPayload,
676             oldOrganizationKey: organizationKey,
677             newOrganizationKey: { privateKey, privateKeyArmored },
678         });
679         return createPasswordlessOrganizationKeysConfig({
680             Token: encryptedToken,
681             Signature: signature,
682             PrivateKey: privateKeyArmored,
683             Members: memberTokens,
684             AdminActivations: publicAdminActivations,
685             AdminInvitations: privateAdminInvitations,
686         });
687     };
690 export const rotatePasswordlessOrganizationKeys = ({
691     publicMembersToReEncryptPayload,
692     memberKeyPayloads,
693 }: OrganizationKeyRotationPayload): ThunkAction<
694     Promise<ReturnType<typeof updatePasswordlessOrganizationKeysConfig>>,
695     OrganizationKeyState,
696     ProtonThunkArguments,
697     UnknownAction
698 > => {
699     return async (dispatch) => {
700         const userKeys = await dispatch(userKeysThunk());
701         const organizationKey = await dispatch(organizationKeyThunk());
702         if (!getIsPasswordless(organizationKey?.Key)) {
703             throw new Error('Only used on passwordless organizations');
704         }
705         const userKey = userKeys[0]?.privateKey;
706         if (!userKey) {
707             throw new Error('Missing primary user key');
708         }
709         const [primaryAddress] = await dispatch(addressesThunk());
710         if (!primaryAddress) {
711             throw new Error('Missing primary address');
712         }
713         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
714         if (!primaryAddressKey) {
715             throw new Error('Missing primary address key');
716         }
717         const { signature, privateKey, privateKeyArmored, encryptedToken } = await generatePasswordlessOrganizationKey({
718             userKey,
719             keyGenConfig,
720         });
721         const { publicAdminActivations, privateAdminInvitations } = await getReEncryptedAdminTokens({
722             armoredMessage: encryptedToken,
723             decryptionKeys: [userKey],
724             address: { ID: primaryAddress.ID, privateKey: primaryAddressKey.privateKey },
725             memberKeyPayloads,
726         });
727         const memberTokens = await getReEncryptedMemberTokens({
728             publicMembersToReEncryptPayload,
729             oldOrganizationKey: organizationKey,
730             newOrganizationKey: { privateKey, privateKeyArmored },
731         });
732         return updatePasswordlessOrganizationKeysConfig({
733             PrivateKey: privateKeyArmored,
734             Signature: signature,
735             Token: encryptedToken,
736             Members: memberTokens,
737             AdminActivations: publicAdminActivations,
738             AdminInvitations: privateAdminInvitations,
739         });
740     };
743 export type AcceptOrganizationKeyInvitePayload =
744     | {
745           state: 'verified';
746           result: {
747               keyPacket: string;
748               signature: string;
749           };
750       }
751     | {
752           state: 'unverified' | 'public-keys';
753           result: null;
754       };
755 export const prepareAcceptOrganizationKeyInvite = ({
756     adminEmail,
757     verifyOutboundPublicKeys,
758     api,
759 }: {
760     adminEmail: string;
761     verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
762     api: Api;
763 }): ThunkAction<
764     Promise<AcceptOrganizationKeyInvitePayload>,
765     OrganizationKeyState,
766     ProtonThunkArguments,
767     UnknownAction
768 > => {
769     return async (dispatch) => {
770         const userKeys = await dispatch(userKeysThunk());
771         const organizationKey = await dispatch(organizationKeyThunk());
772         const addresses = await dispatch(addressesThunk());
774         if (!getIsPasswordless(organizationKey?.Key)) {
775             throw new Error('Can only be used on passwordless orgs');
776         }
777         const primaryUserKey = userKeys[0];
778         if (!primaryUserKey) {
779             throw new Error('Missing primary user key');
780         }
781         const targetAddress = addresses?.find((address) => address.ID === organizationKey.Key.EncryptionAddressID);
782         if (!targetAddress) {
783             throw new Error('Missing encryption address');
784         }
786         const addressKeys = await dispatch(addressKeysThunk({ addressID: targetAddress.ID }));
787         if (!addressKeys.length) {
788             throw new Error('Missing address keys');
789         }
791         const adminEmailPublicKeys = (
792             await getVerifiedPublicKeys({
793                 api,
794                 email: adminEmail,
795                 verifyOutboundPublicKeys,
796                 // In app context, can use default
797                 userContext: undefined,
798             })
799         ).map(({ publicKey }) => publicKey);
800         if (!adminEmailPublicKeys.length) {
801             return {
802                 state: 'public-keys',
803                 result: null,
804             };
805         }
807         const splitAddressKeys = splitKeys(addressKeys);
808         try {
809             const result = await acceptInvitation({
810                 Token: organizationKey.Key.Token,
811                 Signature: organizationKey.Key.Signature,
812                 decryptionKeys: splitAddressKeys.privateKeys,
813                 verificationKeys: adminEmailPublicKeys,
814                 encryptionKey: primaryUserKey.privateKey,
815             });
816             return {
817                 state: 'verified',
818                 result,
819             };
820         } catch (e: any) {
821             const error = getSentryError(e);
822             if (error) {
823                 captureMessage('Passwordless: Error accepting invite', { level: 'error', extra: { error } });
824             }
825             return {
826                 state: 'unverified',
827                 result: null,
828             };
829         }
830     };
833 export const acceptOrganizationKeyInvite = ({
834     api,
835     payload,
836 }: {
837     api: Api;
838     payload: AcceptOrganizationKeyInvitePayload;
839 }): ThunkAction<Promise<void>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
840     return async (dispatch) => {
841         if (payload.state === 'verified') {
842             await api(
843                 activatePasswordlessKey({
844                     TokenKeyPacket: payload.result.keyPacket,
845                     Signature: payload.result.signature,
846                 })
847             );
848             // Warning: Force a refetch of the org key because it's not present in the event manager.
849             await dispatch(organizationKeyThunk({ cache: CacheType.None }));
850         }
851     };
854 export const migrateOrganizationKeyPasswordless = (): ThunkAction<
855     Promise<void>,
856     OrganizationKeyState,
857     ProtonThunkArguments,
858     UnknownAction
859 > => {
860     return async (dispatch, _, { api, eventManager }) => {
861         const userKeys = await dispatch(userKeysThunk());
862         const organizationKey = await dispatch(organizationKeyThunk());
863         if (getIsPasswordless(organizationKey?.Key)) {
864             throw new Error('Only used on non-passwordless organizations');
865         }
866         if (!organizationKey?.privateKey) {
867             throw new Error('Organization key must be decryptable to migrate');
868         }
869         const userKey = userKeys[0]?.privateKey;
870         if (!userKey) {
871             throw new Error('Missing primary user key');
872         }
873         const [primaryAddress] = await dispatch(addressesThunk());
874         if (!primaryAddress) {
875             throw new Error('Missing primary address');
876         }
877         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
878         if (!primaryAddressKey) {
879             throw new Error('Missing primary address key');
880         }
881         const { token, encryptedToken, signature } = await generateOrganizationKeyToken(userKey);
882         const PrivateKey = await CryptoProxy.exportPrivateKey({
883             privateKey: organizationKey.privateKey,
884             passphrase: token,
885         });
887         const members = await dispatch(membersThunk());
888         const otherAdminMembersToMigrate = members.filter((member) => {
889             return (
890                 member.Role === MEMBER_ROLE.ORGANIZATION_ADMIN &&
891                 !member.Self &&
892                 (member.AccessToOrgKey === MEMBER_ORG_KEY_STATE.Active || member.Private === MEMBER_PRIVATE.READABLE)
893             );
894         });
896         const silentApi = getSilentApi(api);
898         try {
899             const memberKeyPayloads = await dispatch(
900                 getMemberKeyPayloads({
901                     api: silentApi,
902                     mode: {
903                         // Encrypting the generated token to the organization public key (instead of the invited user's primary address key)
904                         type: 'org-key',
905                         publicKey: organizationKey.publicKey,
906                     },
907                     members: otherAdminMembersToMigrate,
908                     ignorePasswordlessValidation: true,
909                 })
910             );
912             const { publicAdminActivations, privateAdminInvitations } = await getReEncryptedAdminTokens({
913                 armoredMessage: encryptedToken,
914                 decryptionKeys: [userKey],
915                 // Signing the token signature with the organization private key
916                 address: { ID: primaryAddress.ID, privateKey: organizationKey.privateKey },
917                 memberKeyPayloads,
918             });
920             await silentApi(
921                 migratePasswordlessOrganizationKey({
922                     PrivateKey,
923                     Token: encryptedToken,
924                     Signature: signature,
925                     AdminActivations: publicAdminActivations,
926                     AdminInvitations: privateAdminInvitations.map((invitation) => ({
927                         MemberID: invitation.MemberID,
928                         TokenKeyPacket: invitation.TokenKeyPacket,
929                         Signature: invitation.Signature,
930                     })),
931                 })
932             );
934             await eventManager.call();
935         } catch (e: any) {
936             const error = getSentryError(e);
937             if (error) {
938                 captureMessage('Passwordless: Error migrating organization', { level: 'error', extra: { error } });
939             }
940         }
941     };
944 export const migrateOrganizationKeyPasswordlessPrivateAdmin = (): ThunkAction<
945     Promise<void>,
946     OrganizationKeyState,
947     ProtonThunkArguments,
948     UnknownAction
949 > => {
950     return async (dispatch, _, { api, eventManager }) => {
951         const userKeys = await dispatch(userKeysThunk());
952         const organizationKey = await dispatch(organizationKeyThunk());
953         if (!getIsPasswordless(organizationKey?.Key) || !organizationKey.privateKey) {
954             throw new Error('Only used on passwordless organizations');
955         }
956         const primaryUserKey = userKeys[0];
957         if (!primaryUserKey) {
958             throw new Error('Missing primary user key');
959         }
960         const [primaryAddress] = await dispatch(addressesThunk());
961         if (!primaryAddress) {
962             throw new Error('Missing primary address');
963         }
964         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
965         if (!primaryAddressKey) {
966             throw new Error('Missing primary address key');
967         }
969         const silentApi = getSilentApi(api);
970         try {
971             const result = await acceptInvitation({
972                 Token: organizationKey.Key.Token,
973                 Signature: organizationKey.Key.Signature,
974                 decryptionKeys: [organizationKey.privateKey],
975                 verificationKeys: [organizationKey.privateKey],
976                 encryptionKey: primaryUserKey.privateKey,
977             });
978             await dispatch(
979                 acceptOrganizationKeyInvite({
980                     api: silentApi,
981                     payload: {
982                         state: 'verified',
983                         result,
984                     },
985                 })
986             );
987             await eventManager.call();
988         } catch (e: any) {
989             const error = getSentryError(e);
990             if (error) {
991                 captureMessage('Passwordless: Error accepting migration invite', { level: 'error', extra: { error } });
992             }
993         }
994     };
997 export const getIsEligibleOrganizationIdentityAddress = (address: Address) => {
998     return getIsAddressEnabled(address) && getIsAddressConfirmed(address);
1001 export const changeOrganizationSignature = ({
1002     address,
1003 }: {
1004     address: Address;
1005 }): ThunkAction<Promise<void>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
1006     return async (dispatch, getState, extra) => {
1007         const organizationKey = await dispatch(organizationKeyThunk());
1008         const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: address.ID }));
1009         if (!primaryAddressKey?.privateKey) {
1010             throw new Error('Missing primary address key');
1011         }
1012         if (!organizationKey?.privateKey) {
1013             throw new Error('Missing organization key');
1014         }
1016         const signature = await generateOrganizationKeySignature({
1017             signingKeys: primaryAddressKey.privateKey,
1018             organizationKey: organizationKey.privateKey,
1019         });
1020         const silentApi = getSilentApi(extra.api);
1021         await silentApi(
1022             uploadOrganizationKeySignature({
1023                 AddressID: address.ID,
1024                 Signature: signature,
1025             })
1026         );
1027         await dispatch(organizationKeyThunk({ cache: CacheType.None }));
1028     };