1 import type { UnknownAction } from '@reduxjs/toolkit';
2 import type { ThunkAction } from 'redux-thunk';
3 import { c } from 'ttag';
5 import { CryptoProxy } from '@proton/crypto';
6 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
7 import { activateMemberAuthDeviceConfig, rejectMemberAuthDeviceConfig } from '@proton/shared/lib/api/authDevice';
8 import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig';
9 import type { DecryptedKey, Member } from '@proton/shared/lib/interfaces';
10 import { generateKeySaltAndPassphrase, getMemberKeys } from '@proton/shared/lib/keys';
11 import type { DeviceSecretData, MemberAuthDeviceOutput } from '@proton/shared/lib/keys/device';
14 decryptAuthDeviceActivationToken,
15 encryptAuthDeviceSecret,
17 } from '@proton/shared/lib/keys/device';
18 import { generatePassword } from '@proton/shared/lib/password';
19 import noop from '@proton/utils/noop';
21 import { getMemberAddresses } from '../members';
22 import { getMemberToUnprivatizeApproval, unprivatizeApprovalMembers } from '../members/unprivatizeMembers';
23 import { organizationKeyThunk } from '../organizationKey';
24 import type { MemberAuthDevicesState, PendingAdminActivation } from './memberAuthDevices';
25 import { memberAuthDeviceActions } from './memberAuthDevices';
27 export interface ConfirmMemberAuthDeviceData {
28 activation: ReturnType<typeof getValidActivation>;
29 deviceSecretData: DeviceSecretData;
30 pendingAuthDevice: MemberAuthDeviceOutput;
32 memberUserKeys: DecryptedKey[];
36 export const prepareConfirmPendingMemberAuthDevice = ({
38 member: initialMember,
40 memberAuthDevice: MemberAuthDeviceOutput;
42 }): ThunkAction<Promise<ConfirmMemberAuthDeviceData>, MemberAuthDevicesState, ProtonThunkArguments, UnknownAction> => {
43 return async (dispatch) => {
44 let member = initialMember;
45 const [memberAddresses, organizationKey] = await Promise.all([
46 dispatch(getMemberAddresses({ member, retry: true })),
47 dispatch(organizationKeyThunk()),
49 const activation = getValidActivation({ addresses: memberAddresses, pendingAuthDevice: memberAuthDevice });
51 throw new Error('Unable to find member address for device');
53 if (!organizationKey?.privateKey) {
54 throw new Error('Organization key must be activated to activate a member device');
56 // If the member needs to get unprivatized, let's do it first
57 if (getMemberToUnprivatizeApproval(member)) {
58 const [updatedMember] = await dispatch(unprivatizeApprovalMembers({ membersToUnprivatize: [member] }));
60 member = updatedMember;
63 const { memberUserKeys, memberAddressesKeys } = await getMemberKeys({
67 privateKey: organizationKey.privateKey,
68 publicKey: organizationKey.publicKey,
72 memberAddressesKeys.find(({ address }) => {
73 return address.ID === activation.address.ID;
75 const deviceSecretData = await decryptAuthDeviceActivationToken({
76 deviceID: memberAuthDevice.ID,
77 decryptionKeys: addressKeys.map(({ privateKey }) => privateKey),
78 armoredMessage: activation.token,
83 pendingAuthDevice: memberAuthDevice,
87 memberUserKeys.forEach((memberUserKey) => {
88 CryptoProxy.clearKey({ key: memberUserKey.privateKey }).catch(noop);
89 CryptoProxy.clearKey({ key: memberUserKey.publicKey }).catch(noop);
91 memberAddressesKeys.forEach((memberAddressKeys) => {
92 memberAddressKeys.keys.forEach((memberAddressKey) => {
93 CryptoProxy.clearKey({ key: memberAddressKey.privateKey }).catch(noop);
94 CryptoProxy.clearKey({ key: memberAddressKey.publicKey }).catch(noop);
102 const getReEncryptedMemberUserKeys = async (memberUserKeys: DecryptedKey[]) => {
103 const password = generatePassword({ useSpecialChars: true, length: 16 });
104 const { passphrase } = await generateKeySaltAndPassphrase(password);
105 const reEncryptedMemberUserKeys = await Promise.all(
106 memberUserKeys.map(async ({ ID, privateKey }) => {
109 PrivateKey: await CryptoProxy.exportPrivateKey({ privateKey, passphrase }),
115 keyPassword: passphrase,
116 memberUserKeys: reEncryptedMemberUserKeys,
120 export const confirmPendingMemberAuthDevice = ({
122 pendingMemberAuthDevice,
124 confirmationCode: string;
125 pendingMemberAuthDevice: PendingAdminActivation;
126 }): ThunkAction<Promise<void>, MemberAuthDevicesState, ProtonThunkArguments, UnknownAction> => {
127 return async (dispatch, getState, extra) => {
128 const confirmMemberAuthDeviceData = await dispatch(
129 prepareConfirmPendingMemberAuthDevice(pendingMemberAuthDevice)
132 if (confirmMemberAuthDeviceData.deviceSecretData.confirmationCode !== confirmationCode) {
133 throw new Error(c('sso').t`Invalid confirmation code`);
135 const api = getSilentApi(extra.api);
136 const { keyPassword, memberUserKeys } = await getReEncryptedMemberUserKeys(
137 confirmMemberAuthDeviceData.memberUserKeys
139 const encryptedSecret = await encryptAuthDeviceSecret({
141 deviceSecretData: confirmMemberAuthDeviceData.deviceSecretData,
144 activateMemberAuthDeviceConfig({
145 MemberID: confirmMemberAuthDeviceData.member.ID,
146 AuthDeviceID: confirmMemberAuthDeviceData.pendingAuthDevice.ID,
147 EncryptedSecret: encryptedSecret,
148 UserKeys: memberUserKeys,
152 memberAuthDeviceActions.updateMemberAuthDevice({
153 ID: confirmMemberAuthDeviceData.pendingAuthDevice.ID,
154 State: AuthDeviceState.Active,
158 confirmMemberAuthDeviceData.cleanup();
163 export const rejectMemberAuthDevice = ({
169 memberAuthDevice: MemberAuthDeviceOutput;
170 type: 'reject' | 'delete';
171 }): ThunkAction<Promise<void>, MemberAuthDevicesState, ProtonThunkArguments, UnknownAction> => {
172 return async (dispatch, getState, extra) => {
173 const api = getSilentApi(extra.api);
174 if (type === 'delete') {
175 throw new Error('todo');
177 await api(rejectMemberAuthDeviceConfig({ MemberID: memberID, DeviceID: memberAuthDevice.ID }));
179 memberAuthDeviceActions.updateMemberAuthDevice({
180 ID: memberAuthDevice.ID,
181 State: AuthDeviceState.Rejected,