Merge branch 'renovate/playwright' into 'main'
[ProtonMail-WebClient.git] / packages / account / sso / memberAuthDeviceActions.ts
bloba03bb0b81c7add4da653cf37e215abc03308f0d5
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';
12 import {
13     AuthDeviceState,
14     decryptAuthDeviceActivationToken,
15     encryptAuthDeviceSecret,
16     getValidActivation,
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;
31     member: Member;
32     memberUserKeys: DecryptedKey[];
33     cleanup: () => void;
36 export const prepareConfirmPendingMemberAuthDevice = ({
37     memberAuthDevice,
38     member: initialMember,
39 }: {
40     memberAuthDevice: MemberAuthDeviceOutput;
41     member: Member;
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()),
48         ]);
49         const activation = getValidActivation({ addresses: memberAddresses, pendingAuthDevice: memberAuthDevice });
50         if (!activation) {
51             throw new Error('Unable to find member address for device');
52         }
53         if (!organizationKey?.privateKey) {
54             throw new Error('Organization key must be activated to activate a member device');
55         }
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] }));
59             if (updatedMember) {
60                 member = updatedMember;
61             }
62         }
63         const { memberUserKeys, memberAddressesKeys } = await getMemberKeys({
64             member,
65             memberAddresses,
66             organizationKey: {
67                 privateKey: organizationKey.privateKey,
68                 publicKey: organizationKey.publicKey,
69             },
70         });
71         const addressKeys =
72             memberAddressesKeys.find(({ address }) => {
73                 return address.ID === activation.address.ID;
74             })?.keys || [];
75         const deviceSecretData = await decryptAuthDeviceActivationToken({
76             deviceID: memberAuthDevice.ID,
77             decryptionKeys: addressKeys.map(({ privateKey }) => privateKey),
78             armoredMessage: activation.token,
79         });
80         return {
81             deviceSecretData,
82             activation,
83             pendingAuthDevice: memberAuthDevice,
84             member,
85             memberUserKeys,
86             cleanup: () => {
87                 memberUserKeys.forEach((memberUserKey) => {
88                     CryptoProxy.clearKey({ key: memberUserKey.privateKey }).catch(noop);
89                     CryptoProxy.clearKey({ key: memberUserKey.publicKey }).catch(noop);
90                 });
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);
95                     });
96                 });
97             },
98         };
99     };
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 }) => {
107             return {
108                 ID,
109                 PrivateKey: await CryptoProxy.exportPrivateKey({ privateKey, passphrase }),
110             };
111         })
112     );
114     return {
115         keyPassword: passphrase,
116         memberUserKeys: reEncryptedMemberUserKeys,
117     };
120 export const confirmPendingMemberAuthDevice = ({
121     confirmationCode,
122     pendingMemberAuthDevice,
123 }: {
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)
130         );
131         try {
132             if (confirmMemberAuthDeviceData.deviceSecretData.confirmationCode !== confirmationCode) {
133                 throw new Error(c('sso').t`Invalid confirmation code`);
134             }
135             const api = getSilentApi(extra.api);
136             const { keyPassword, memberUserKeys } = await getReEncryptedMemberUserKeys(
137                 confirmMemberAuthDeviceData.memberUserKeys
138             );
139             const encryptedSecret = await encryptAuthDeviceSecret({
140                 keyPassword,
141                 deviceSecretData: confirmMemberAuthDeviceData.deviceSecretData,
142             });
143             await api(
144                 activateMemberAuthDeviceConfig({
145                     MemberID: confirmMemberAuthDeviceData.member.ID,
146                     AuthDeviceID: confirmMemberAuthDeviceData.pendingAuthDevice.ID,
147                     EncryptedSecret: encryptedSecret,
148                     UserKeys: memberUserKeys,
149                 })
150             );
151             dispatch(
152                 memberAuthDeviceActions.updateMemberAuthDevice({
153                     ID: confirmMemberAuthDeviceData.pendingAuthDevice.ID,
154                     State: AuthDeviceState.Active,
155                 })
156             );
157         } finally {
158             confirmMemberAuthDeviceData.cleanup();
159         }
160     };
163 export const rejectMemberAuthDevice = ({
164     memberID,
165     memberAuthDevice,
166     type,
167 }: {
168     memberID: string;
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');
176         } else {
177             await api(rejectMemberAuthDeviceConfig({ MemberID: memberID, DeviceID: memberAuthDevice.ID }));
178             dispatch(
179                 memberAuthDeviceActions.updateMemberAuthDevice({
180                     ID: memberAuthDevice.ID,
181                     State: AuthDeviceState.Rejected,
182                 })
183             );
184         }
185     };