1+ import { CryptoProxy , type PrivateKeyReference , type PublicKeyReference } from '@protontech/crypto' ;
12import type { ThunkAction , UnknownAction } from '@reduxjs/toolkit' ;
23
3- import { CryptoProxy , type PrivateKeyReference , type PublicKeyReference } from '@protontech/crypto' ;
44import type { ProtonThunkArguments } from '@proton/redux-shared-store-types' ;
55import { CacheType } from '@proton/redux-utilities/interface' ;
6- import { addGroupMember as addGroupMemberApi } from '@proton/shared/lib/api/groups' ;
6+ import {
7+ addGroupMember as addGroupMemberApi ,
8+ addGroupMemberKeys as addGroupMemberKeysApi ,
9+ } from '@proton/shared/lib/api/groups' ;
710import { getSilentApi } from '@proton/shared/lib/api/helpers/customConfig' ;
811import { MEMBER_PRIVATE , RECIPIENT_TYPES } from '@proton/shared/lib/constants' ;
912import { getIsEncryptionDisabled } from '@proton/shared/lib/helpers/address' ;
@@ -16,6 +19,8 @@ import type {
1619 EnhancedMember ,
1720} from '@proton/shared/lib/interfaces' ;
1821import { GroupMemberType } from '@proton/shared/lib/interfaces' ;
22+ import { GROUP_MEMBER_TYPE } from '@proton/shared/lib/interfaces/GroupMember' ;
23+ import type { GroupMember } from '@proton/shared/lib/interfaces/GroupMember' ;
1924import { getAddressKeyToken , getDecryptedUserKeys , getEmailFromKey , splitKeys } from '@proton/shared/lib/keys' ;
2025import { getInternalParameters , getInternalParametersPrivate } from '@proton/shared/lib/keys/forward/forward' ;
2126import { getGroupMemberPublicKeys } from '@proton/shared/lib/keys/groupKeys' ;
@@ -296,3 +301,107 @@ export const addGroupMembersThunk = ({
296301 }
297302 } ;
298303} ;
304+
305+ export const addGroupMemberKeysThunk = ( {
306+ groupMember,
307+ groupAddress,
308+ getMemberPublicKeys,
309+ } : {
310+ groupMember : GroupMember ;
311+ groupAddress : Address ;
312+ getMemberPublicKeys : ( email : string ) => Promise < ApiKeysConfig > ;
313+ } ) : ThunkAction < Promise < void > , RequiredState , ProtonThunkArguments , UnknownAction > => {
314+ return async ( dispatch , _ , extra ) => {
315+ const email = groupMember . Email ;
316+ if ( ! email ) {
317+ throw new Error ( 'Group member has no email' ) ;
318+ }
319+
320+ const api = getSilentApi ( extra . api ) ;
321+ const forwarderKey = await dispatch ( getGroupKey ( { groupAddress } ) ) ;
322+ const { forwardeeKeysConfig, forwardeeArmoredPrimaryPublicKey } = await getGroupMemberPublicKeys ( {
323+ api,
324+ memberEmail : email ,
325+ getMemberPublicKeys,
326+ } ) ;
327+
328+ // External recipients can't keep E2EE, so disable it on the group address (skip if already off).
329+ // NOTE: getIsEncryptionDisabled reads cached state and is therefore not safe under concurrency.
330+ // Once the key addition is parallelized across multiple members, do this once up front (as addGroupMembersThunk does) instead of per member.
331+ if ( isExternalForMail ( forwardeeKeysConfig ) && ! getIsEncryptionDisabled ( groupAddress ) ) {
332+ await dispatch ( disableGroupAddressEncryption ( { groupAddress, forwarderKey } ) ) ;
333+ }
334+
335+ const [ members , organizationKey ] = await Promise . all ( [
336+ dispatch ( membersThunk ( ) ) ,
337+ dispatch ( organizationKeyThunk ( ) ) ,
338+ ] ) ;
339+
340+ const canonicalEmail = canonicalizeInternalEmail ( email ) ;
341+ const member = getMemberByEmail ( members , canonicalEmail ) ;
342+
343+ const AddressSignaturePacket = await signMemberEmail ( canonicalEmail , forwarderKey . privateKey ) ;
344+
345+ // Case 1 — External: no key to forward to, send email + signature only.
346+ if ( groupMember . Type === GROUP_MEMBER_TYPE . EXTERNAL ) {
347+ return api ( addGroupMemberKeysApi ( groupMember . ID , { Email : email , AddressSignaturePacket } ) ) ;
348+ }
349+
350+ // Case 2 — Internal we can't decrypt for (other org / private / no org key): build the key
351+ // from their public key and send an ActivationToken for them to activate it themselves.
352+ if ( ! member || isPrivate ( member ) || ! organizationKey . privateKey ) {
353+ const forwardeePublicKey = await getForwardeePublicKey ( forwardeeArmoredPrimaryPublicKey ) ;
354+ const Email = getEmailFromKey ( forwardeePublicKey ) ?? email ;
355+ const userIDsForForwardeeKey = [ { email : Email , name : Email } ] ;
356+ const { activationToken, forwardeeKey, proxyInstances } = await getInternalParametersPrivate (
357+ forwarderKey . privateKey ,
358+ userIDsForForwardeeKey ,
359+ forwardeePublicKey
360+ ) ;
361+ return api (
362+ addGroupMemberKeysApi ( groupMember . ID , {
363+ Email : canonicalEmail ,
364+ AddressSignaturePacket,
365+ ActivationToken : activationToken ,
366+ GroupMemberAddressPrivateKey : forwardeeKey ,
367+ ProxyInstances : proxyInstances . map ( mapProxyInstance ) ,
368+ } )
369+ ) ;
370+ }
371+
372+ // Case 3 — Managed internal member: we hold the org key, so we decrypt their address
373+ // token ourselves and forward a ready-to-use key (no activation step needed).
374+ await dispatch ( replaceMemberAddressTokensIfNeeded ( { member } ) ) ;
375+
376+ const memberAddresses = await dispatch ( getMemberAddresses ( { member, retry : true } ) ) ;
377+ const forwardeeAddress = memberAddresses . find (
378+ ( { Email } ) => canonicalizeInternalEmail ( Email ) === canonicalEmail
379+ ) ;
380+ if ( ! forwardeeAddress ) {
381+ throw new Error ( 'Member without matching address' ) ;
382+ }
383+
384+ const { decryptedToken, Token, Signature } = await getPrimaryMemberTokenAndSignature ( {
385+ member,
386+ forwardeeAddress,
387+ organizationKey,
388+ } ) ;
389+
390+ const userIDsForForwardeeKey = [ { email : canonicalEmail , name : canonicalEmail } ] ;
391+ const { forwardeeKey, proxyInstances } = await getInternalParameters (
392+ forwarderKey . privateKey ,
393+ userIDsForForwardeeKey ,
394+ decryptedToken
395+ ) ;
396+ return api (
397+ addGroupMemberKeysApi ( groupMember . ID , {
398+ Email : canonicalEmail ,
399+ AddressSignaturePacket,
400+ GroupMemberAddressPrivateKey : forwardeeKey ,
401+ ProxyInstances : proxyInstances . map ( mapProxyInstance ) ,
402+ Token,
403+ Signature,
404+ } )
405+ ) ;
406+ } ;
407+ } ;
0 commit comments