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';
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';
23 CachedOrganizationKey,
26 VerifyOutboundPublicKeys,
27 } from '@proton/shared/lib/interfaces';
28 import { MEMBER_ORG_KEY_STATE } from '@proton/shared/lib/interfaces';
31 generateOrganizationKeySignature,
32 generateOrganizationKeyToken,
33 generateOrganizationKeys,
34 generatePasswordlessOrganizationKey,
35 generatePrivateMemberInvitation,
36 generatePublicMemberInvitation,
38 getHasMigratedAddressKeys,
40 getOrganizationKeyToken,
42 getReEncryptedPublicMemberTokensPayloadLegacy,
43 getReEncryptedPublicMemberTokensPayloadV2,
45 getVerifiedPublicKeys,
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<
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 });
89 export interface PublicMemberKeyPayload {
93 address: Address | undefined;
94 privateKey: PrivateKeyReference;
97 export interface PrivateMemberKeyPayload {
102 publicKey: PublicKeyReference;
105 export type MemberKeyPayload = PrivateMemberKeyPayload | PublicMemberKeyPayload;
107 export type PublicMembersReEncryptPayload = {
109 memberAddresses: Address[];
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 ({
127 organizationKey: CachedOrganizationKey;
131 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
135 publicKey: PublicKeyReference;
139 memberAddresses: Address[];
140 }): Promise<MemberKeyPayload> => {
141 const address: Address | undefined = memberAddresses.filter(
142 (address) => getIsAddressEnabled(address) && address.HasKeys
145 if (member.Private === MEMBER_PRIVATE.READABLE) {
146 if (!member.Keys?.length) {
147 throw new ConstraintError(getPublicAdminError());
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`);
153 const memberUserKeys = await getDecryptedUserKeys(member.Keys, '', organizationKey);
154 const privateKey = getPrimaryKey(memberUserKeys)?.privateKey;
156 throw new Error('Unable to decrypt non-private user keys');
162 email: address?.Email || member.Name,
167 if (mode.type === 'org-key') {
171 address: {} as any, // Unused
173 publicKey: mode.publicKey,
178 throw new ConstraintError(getPrivateAdminError());
180 const email = address.Email;
181 const memberPublicKey = (
182 await getVerifiedPublicKeys({
184 verifyOutboundPublicKeys: mode.verifyOutboundPublicKeys,
186 // In app context, can use default
187 userContext: undefined,
190 if (!memberPublicKey) {
191 throw new Error(getPrivateAdminError());
198 publicKey: memberPublicKey,
202 const getReEncryptedAdminTokens = async ({
208 armoredMessage: string;
209 decryptionKeys: PrivateKeyReference[];
210 address: { ID: string; privateKey: PrivateKeyReference };
211 memberKeyPayloads: MemberKeyPayload[];
213 const { sessionKey, message } = await decryptKeyPacket({
219 binaryData: message.data,
222 addressID: address.ID,
223 privateKey: address.privateKey,
226 const { privateAdminPromises, publicAdminPromises } = memberKeyPayloads.reduce<{
227 privateAdminPromises: ReturnType<typeof generatePrivateMemberInvitation>[];
228 publicAdminPromises: ReturnType<typeof generatePublicMemberInvitation>[];
230 (acc, memberPayload) => {
231 if (memberPayload.type === 1) {
232 const { member, publicKey, address } = memberPayload;
233 acc.privateAdminPromises.push(
234 generatePrivateMemberInvitation({
237 addressID: address.ID,
243 const { member, privateKey } = memberPayload;
244 acc.publicAdminPromises.push(
245 generatePublicMemberInvitation({
255 { privateAdminPromises: [], publicAdminPromises: [] }
258 const [privateAdminInvitations, publicAdminActivations] = await Promise.all([
259 Promise.all(privateAdminPromises),
260 Promise.all(publicAdminPromises),
263 return { privateAdminInvitations, publicAdminActivations };
266 const getReEncryptedMemberTokens = async ({
267 publicMembersToReEncryptPayload,
271 publicMembersToReEncryptPayload: PublicMembersReEncryptPayload;
272 oldOrganizationKey: CachedOrganizationKey;
273 newOrganizationKey: { privateKey: PrivateKeyReference; privateKeyArmored: string };
274 }): Promise<Parameters<typeof updatePasswordlessOrganizationKeysConfig>[0]['Members']> => {
275 if (!publicMembersToReEncryptPayload.length) {
278 if (!oldOrganizationKey?.privateKey) {
279 throw new Error('Public members received without an existing organization key.');
281 const publicKey = await CryptoProxy.importPublicKey({ armoredKey: newOrganizationKey.privateKeyArmored });
282 return getReEncryptedPublicMemberTokensPayloadV2({
283 publicMembers: publicMembersToReEncryptPayload,
284 oldOrganizationKey: oldOrganizationKey,
285 newOrganizationKey: { privateKey: newOrganizationKey.privateKey, publicKey },
289 type ConfirmPromotionMemberAction = {
290 type: 'confirm-promote';
291 payload: MemberKeyPayload;
294 type ConfirmDemotionMemberAction = {
295 type: 'confirm-demote';
298 export type MemberPromptAction = ConfirmPromotionMemberAction | ConfirmDemotionMemberAction;
300 export const getMemberEditPayload = ({
301 verifyOutboundPublicKeys,
306 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
307 member: EnhancedMember;
308 memberDiff: Partial<{
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) {
319 type: 'confirm-demote',
324 if (memberDiff.role === MEMBER_ROLE.ORGANIZATION_ADMIN && passwordlessMode) {
325 const payload = await getMemberKeyPayload({
329 verifyOutboundPublicKeys,
333 memberAddresses: await dispatch(getMemberAddresses({ member, retry: true })),
336 if (member.Private === MEMBER_PRIVATE.UNREADABLE) {
338 type: 'confirm-promote',
345 type: 'confirm-promote',
355 export const getMemberKeyPayloads = ({
359 ignorePasswordlessValidation,
362 mode: Parameters<typeof getMemberKeyPayload>[0]['mode'];
363 members: EnhancedMember[];
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');
373 if (!getIsPasswordless(organizationKey?.Key) && !ignorePasswordlessValidation) {
374 throw new Error('Only used on passwordless organizations');
378 members.map(async (member) => {
380 return await getMemberKeyPayload({
382 memberAddresses: await dispatch(getMemberAddresses({ member, retry: true })),
388 if (e instanceof ConstraintError) {
402 export const getPublicMembersToReEncryptPayload = (): ThunkAction<
403 Promise<PublicMembersReEncryptPayload>,
404 OrganizationKeyState,
405 ProtonThunkArguments,
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());
418 const publicMembersToReEncrypt = await Promise.all(
419 publicMembers.map(async (member) => {
420 if (!member.Keys?.length) {
423 if (!organizationKey?.privateKey) {
424 throw new Error(getPrivatizeError());
426 const memberUserKeys = await getDecryptedUserKeys(member.Keys, '', organizationKey);
427 const privateKey = getPrimaryKey(memberUserKeys)?.privateKey;
429 throw new Error('Missing private key');
431 const memberAddresses = await dispatch(getMemberAddresses({ member, retry: true }));
439 return publicMembersToReEncrypt.filter(isTruthy);
446 export const getKeyRotationPayload = ({
447 verifyOutboundPublicKeys,
449 ignorePasswordlessValidation,
451 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
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');
461 const userKey = userKeys[0]?.privateKey;
463 throw new Error('Missing primary user key');
465 const [primaryAddress] = await dispatch(addressesThunk());
466 if (!primaryAddress) {
467 throw new Error('Missing primary address');
469 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
470 if (!primaryAddressKey) {
471 throw new Error('Missing primary address key');
473 const members = await dispatch(membersThunk());
474 const otherAdminMembers = members.filter((member) => {
475 return member.Role === MEMBER_ROLE.ORGANIZATION_ADMIN && !member.Self;
478 const [memberKeyPayloads, publicMembersToReEncryptPayload] = await Promise.all([
480 getMemberKeyPayloads({
484 verifyOutboundPublicKeys,
486 members: otherAdminMembers,
487 ignorePasswordlessValidation,
490 dispatch(getPublicMembersToReEncryptPayload()),
495 publicMembersToReEncryptPayload,
499 export const setAdminRoles = ({
502 }: { memberKeyPayloads: MemberKeyPayload[] } & {
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');
511 if (!organizationKey?.privateKey) {
512 throw new Error('Organization key must be activated to set admin role');
514 const userKey = userKeys[0]?.privateKey;
516 throw new Error('Missing primary user key');
518 const [primaryAddress] = await dispatch(addressesThunk());
519 if (!primaryAddress) {
520 throw new Error('Missing primary address');
522 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
523 if (!primaryAddressKey) {
524 throw new Error('Missing primary address key');
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 },
534 const privatePromise = Promise.all(
535 privateAdminInvitations.map((invitation) => {
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,
551 const publicPromise = Promise.all(
552 publicAdminActivations.map((activation) => {
554 updateRolePasswordless({
555 memberID: activation.MemberID,
556 Role: MEMBER_ROLE.ORGANIZATION_ADMIN,
557 OrganizationKeyActivation: {
558 TokenKeyPacket: activation.TokenKeyPacket,
559 Signature: activation.Signature,
566 await Promise.all([privatePromise, publicPromise]);
570 export const rotateOrganizationKeys = ({
571 password: newPassword,
575 Promise<ReturnType<typeof updateOrganizationKeysV2> | ReturnType<typeof updateOrganizationKeysLegacy>>,
576 OrganizationKeyState,
577 ProtonThunkArguments,
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({
591 backupPassword: newPassword,
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());
603 members = await getReEncryptedPublicMemberTokensPayloadV2({
604 publicMembers: publicMembersToReEncrypt,
605 oldOrganizationKey: organizationKey,
606 newOrganizationKey: { privateKey, publicKey },
609 return updateOrganizationKeysV2({
610 PrivateKey: privateKeyArmored,
611 BackupPrivateKey: backupArmoredPrivateKey,
612 BackupKeySalt: backupKeySalt,
617 let tokens: Parameters<typeof updateOrganizationKeysLegacy>[0]['Tokens'] = [];
618 if (publicMembersToReEncrypt.length >= 1) {
619 if (!organizationKey?.privateKey) {
620 throw new Error(getPrivatizeError());
622 tokens = await getReEncryptedPublicMemberTokensPayloadLegacy({
623 publicMembers: publicMembersToReEncrypt,
624 oldOrganizationKey: organizationKey,
625 newOrganizationKey: { privateKey, publicKey },
628 return updateOrganizationKeysLegacy({
629 PrivateKey: privateKeyArmored,
630 BackupPrivateKey: backupArmoredPrivateKey,
631 BackupKeySalt: backupKeySalt,
637 export const createPasswordlessOrganizationKeys = ({
638 publicMembersToReEncryptPayload,
640 }: OrganizationKeyRotationPayload): ThunkAction<
641 Promise<ReturnType<typeof createPasswordlessOrganizationKeysConfig>>,
642 OrganizationKeyState,
643 ProtonThunkArguments,
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');
652 const userKey = userKeys[0]?.privateKey;
654 throw new Error('Missing primary user key');
656 const [primaryAddress] = await dispatch(addressesThunk());
657 if (!primaryAddress) {
658 throw new Error('Missing primary address');
660 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
661 if (!primaryAddressKey) {
662 throw new Error('Missing primary address key');
664 const { encryptedToken, signature, privateKey, privateKeyArmored } = await generatePasswordlessOrganizationKey({
668 const { publicAdminActivations, privateAdminInvitations } = await getReEncryptedAdminTokens({
669 armoredMessage: encryptedToken,
670 decryptionKeys: [userKey],
671 address: { ID: primaryAddress.ID, privateKey: primaryAddressKey.privateKey },
674 const memberTokens = await getReEncryptedMemberTokens({
675 publicMembersToReEncryptPayload,
676 oldOrganizationKey: organizationKey,
677 newOrganizationKey: { privateKey, privateKeyArmored },
679 return createPasswordlessOrganizationKeysConfig({
680 Token: encryptedToken,
681 Signature: signature,
682 PrivateKey: privateKeyArmored,
683 Members: memberTokens,
684 AdminActivations: publicAdminActivations,
685 AdminInvitations: privateAdminInvitations,
690 export const rotatePasswordlessOrganizationKeys = ({
691 publicMembersToReEncryptPayload,
693 }: OrganizationKeyRotationPayload): ThunkAction<
694 Promise<ReturnType<typeof updatePasswordlessOrganizationKeysConfig>>,
695 OrganizationKeyState,
696 ProtonThunkArguments,
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');
705 const userKey = userKeys[0]?.privateKey;
707 throw new Error('Missing primary user key');
709 const [primaryAddress] = await dispatch(addressesThunk());
710 if (!primaryAddress) {
711 throw new Error('Missing primary address');
713 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
714 if (!primaryAddressKey) {
715 throw new Error('Missing primary address key');
717 const { signature, privateKey, privateKeyArmored, encryptedToken } = await generatePasswordlessOrganizationKey({
721 const { publicAdminActivations, privateAdminInvitations } = await getReEncryptedAdminTokens({
722 armoredMessage: encryptedToken,
723 decryptionKeys: [userKey],
724 address: { ID: primaryAddress.ID, privateKey: primaryAddressKey.privateKey },
727 const memberTokens = await getReEncryptedMemberTokens({
728 publicMembersToReEncryptPayload,
729 oldOrganizationKey: organizationKey,
730 newOrganizationKey: { privateKey, privateKeyArmored },
732 return updatePasswordlessOrganizationKeysConfig({
733 PrivateKey: privateKeyArmored,
734 Signature: signature,
735 Token: encryptedToken,
736 Members: memberTokens,
737 AdminActivations: publicAdminActivations,
738 AdminInvitations: privateAdminInvitations,
743 export type AcceptOrganizationKeyInvitePayload =
752 state: 'unverified' | 'public-keys';
755 export const prepareAcceptOrganizationKeyInvite = ({
757 verifyOutboundPublicKeys,
761 verifyOutboundPublicKeys: VerifyOutboundPublicKeys;
764 Promise<AcceptOrganizationKeyInvitePayload>,
765 OrganizationKeyState,
766 ProtonThunkArguments,
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');
777 const primaryUserKey = userKeys[0];
778 if (!primaryUserKey) {
779 throw new Error('Missing primary user key');
781 const targetAddress = addresses?.find((address) => address.ID === organizationKey.Key.EncryptionAddressID);
782 if (!targetAddress) {
783 throw new Error('Missing encryption address');
786 const addressKeys = await dispatch(addressKeysThunk({ addressID: targetAddress.ID }));
787 if (!addressKeys.length) {
788 throw new Error('Missing address keys');
791 const adminEmailPublicKeys = (
792 await getVerifiedPublicKeys({
795 verifyOutboundPublicKeys,
796 // In app context, can use default
797 userContext: undefined,
799 ).map(({ publicKey }) => publicKey);
800 if (!adminEmailPublicKeys.length) {
802 state: 'public-keys',
807 const splitAddressKeys = splitKeys(addressKeys);
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,
821 const error = getSentryError(e);
823 captureMessage('Passwordless: Error accepting invite', { level: 'error', extra: { error } });
833 export const acceptOrganizationKeyInvite = ({
838 payload: AcceptOrganizationKeyInvitePayload;
839 }): ThunkAction<Promise<void>, OrganizationKeyState, ProtonThunkArguments, UnknownAction> => {
840 return async (dispatch) => {
841 if (payload.state === 'verified') {
843 activatePasswordlessKey({
844 TokenKeyPacket: payload.result.keyPacket,
845 Signature: payload.result.signature,
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 }));
854 export const migrateOrganizationKeyPasswordless = (): ThunkAction<
856 OrganizationKeyState,
857 ProtonThunkArguments,
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');
866 if (!organizationKey?.privateKey) {
867 throw new Error('Organization key must be decryptable to migrate');
869 const userKey = userKeys[0]?.privateKey;
871 throw new Error('Missing primary user key');
873 const [primaryAddress] = await dispatch(addressesThunk());
874 if (!primaryAddress) {
875 throw new Error('Missing primary address');
877 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
878 if (!primaryAddressKey) {
879 throw new Error('Missing primary address key');
881 const { token, encryptedToken, signature } = await generateOrganizationKeyToken(userKey);
882 const PrivateKey = await CryptoProxy.exportPrivateKey({
883 privateKey: organizationKey.privateKey,
887 const members = await dispatch(membersThunk());
888 const otherAdminMembersToMigrate = members.filter((member) => {
890 member.Role === MEMBER_ROLE.ORGANIZATION_ADMIN &&
892 (member.AccessToOrgKey === MEMBER_ORG_KEY_STATE.Active || member.Private === MEMBER_PRIVATE.READABLE)
896 const silentApi = getSilentApi(api);
899 const memberKeyPayloads = await dispatch(
900 getMemberKeyPayloads({
903 // Encrypting the generated token to the organization public key (instead of the invited user's primary address key)
905 publicKey: organizationKey.publicKey,
907 members: otherAdminMembersToMigrate,
908 ignorePasswordlessValidation: true,
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 },
921 migratePasswordlessOrganizationKey({
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,
934 await eventManager.call();
936 const error = getSentryError(e);
938 captureMessage('Passwordless: Error migrating organization', { level: 'error', extra: { error } });
944 export const migrateOrganizationKeyPasswordlessPrivateAdmin = (): ThunkAction<
946 OrganizationKeyState,
947 ProtonThunkArguments,
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');
956 const primaryUserKey = userKeys[0];
957 if (!primaryUserKey) {
958 throw new Error('Missing primary user key');
960 const [primaryAddress] = await dispatch(addressesThunk());
961 if (!primaryAddress) {
962 throw new Error('Missing primary address');
964 const [primaryAddressKey] = await dispatch(addressKeysThunk({ addressID: primaryAddress.ID }));
965 if (!primaryAddressKey) {
966 throw new Error('Missing primary address key');
969 const silentApi = getSilentApi(api);
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,
979 acceptOrganizationKeyInvite({
987 await eventManager.call();
989 const error = getSentryError(e);
991 captureMessage('Passwordless: Error accepting migration invite', { level: 'error', extra: { error } });
997 export const getIsEligibleOrganizationIdentityAddress = (address: Address) => {
998 return getIsAddressEnabled(address) && getIsAddressConfirmed(address);
1001 export const changeOrganizationSignature = ({
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');
1012 if (!organizationKey?.privateKey) {
1013 throw new Error('Missing organization key');
1016 const signature = await generateOrganizationKeySignature({
1017 signingKeys: primaryAddressKey.privateKey,
1018 organizationKey: organizationKey.privateKey,
1020 const silentApi = getSilentApi(extra.api);
1022 uploadOrganizationKeySignature({
1023 AddressID: address.ID,
1024 Signature: signature,
1027 await dispatch(organizationKeyThunk({ cache: CacheType.None }));