Merge branch 'MAILWEB-6067-improve-circular-dependencies-prevention' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _shares / useShare.ts
blob3dcd2bf0695b8f53d669680fab808c532cfb3922
1 import type { PrivateKeyReference, SessionKey } from '@proton/crypto';
2 import { CryptoProxy } from '@proton/crypto';
3 import { queryShareMeta } from '@proton/shared/lib/api/drive/share';
4 import type { ShareMeta } from '@proton/shared/lib/interfaces/drive/share';
6 import { sendErrorReport } from '../../utils/errorHandling';
7 import { EnrichedError } from '../../utils/errorHandling/EnrichedError';
8 import { shareMetaToShareWithKey, useDebouncedRequest } from '../_api';
9 import { integrityMetrics, useDriveCrypto } from '../_crypto';
10 import { useIsPaid } from '../_user';
11 import { useDebouncedFunction } from '../_utils';
12 import type { Share, ShareWithKey } from './interface';
13 import { getShareTypeString } from './shareType';
14 import type { ShareKeys } from './useSharesKeys';
15 import useSharesKeys from './useSharesKeys';
16 import useSharesState from './useSharesState';
18 export default function useShare() {
19     const isPaid = useIsPaid();
20     const debouncedFunction = useDebouncedFunction();
21     const debouncedRequest = useDebouncedRequest();
22     const driveCrypto = useDriveCrypto();
23     const sharesKeys = useSharesKeys();
24     const sharesState = useSharesState();
26     const fetchShare = async (abortSignal: AbortSignal, shareId: string): Promise<ShareWithKey> => {
27         const Share = await debouncedRequest<ShareMeta>({
28             ...queryShareMeta(shareId),
29             signal: abortSignal,
30         });
31         return shareMetaToShareWithKey(Share);
32     };
34     /**
35      * getShareWithKey returns share with keys. That is not available after
36      * listing user's shares and thus needs extra API call. Use wisely.
37      */
38     const getShareWithKey = async (abortSignal: AbortSignal, shareId: string): Promise<ShareWithKey> => {
39         return debouncedFunction(
40             async (abortSignal: AbortSignal) => {
41                 const cachedShare = sharesState.getShare(shareId);
42                 if (cachedShare && 'key' in cachedShare) {
43                     return cachedShare;
44                 }
46                 const share = await fetchShare(abortSignal, shareId);
47                 sharesState.setShares([share]);
48                 return share;
49             },
50             ['getShareWithKey', shareId],
51             abortSignal
52         );
53     };
55     /**
56      * getShare returns share from cache or it fetches the full share from API.
57      */
58     const getShare = async (abortSignal: AbortSignal, shareId: string): Promise<Share> => {
59         const cachedShare = sharesState.getShare(shareId);
60         if (cachedShare) {
61             return cachedShare;
62         }
63         return getShareWithKey(abortSignal, shareId);
64     };
66     const getShareKeys = async (
67         abortSignal: AbortSignal,
68         shareId: string,
69         linkPrivateKey?: PrivateKeyReference
70     ): Promise<ShareKeys> => {
71         const keys = sharesKeys.get(shareId);
72         if (keys) {
73             return keys;
74         }
76         const share = await getShareWithKey(abortSignal, shareId);
78         /**
79          * Decrypt the share with linkPrivateKey (NodeKey) if provided and fallback if it failed
80          * Fallback will use user's privateKey (retrieved in driveCrypto.decryptSharePassphrase function)
81          */
82         const decryptSharePassphrase = async (
83             fallback: boolean = false
84         ): ReturnType<typeof driveCrypto.decryptSharePassphrase> => {
85             // TODO: Change the logic when we will migrate to encryption with only link's privateKey
86             // If the share passphrase was encrypted with multiple KeyPacket,
87             // that mean it was encrypted with link's privateKey and user's privateKey
88             const haveMultipleEncryptionKey = await CryptoProxy.getMessageInfo({
89                 armoredMessage: share.passphrase,
90             }).then((messageInfo) => messageInfo.encryptionKeyIDs.length > 1);
91             const decryptWithLinkPrivateKey = !!linkPrivateKey && !fallback && haveMultipleEncryptionKey;
92             try {
93                 return await driveCrypto.decryptSharePassphrase(
94                     share,
95                     decryptWithLinkPrivateKey ? [linkPrivateKey] : undefined
96                 );
97             } catch (e) {
98                 if (decryptWithLinkPrivateKey) {
99                     sendErrorReport(
100                         new EnrichedError('Failed to decrypt share passphrase with link privateKey', {
101                             tags: { keyId: linkPrivateKey.getKeyID(), shareId },
102                             extra: { e },
103                         })
104                     );
105                     return decryptSharePassphrase(true);
106                 }
108                 const shareType = getShareTypeString(share);
109                 const options = {
110                     isPaid,
111                     createTime: share.createTime,
112                 };
113                 integrityMetrics.shareDecryptionError(shareId, shareType, options);
115                 throw new EnrichedError('Failed to decrypt share passphrase', {
116                     tags: {
117                         shareId,
118                     },
119                     extra: {
120                         e,
121                     },
122                 });
123             }
124         };
126         const { decryptedPassphrase, sessionKey } = await decryptSharePassphrase();
127         const privateKey = await CryptoProxy.importPrivateKey({
128             armoredKey: share.key,
129             passphrase: decryptedPassphrase,
130         }).catch((e) =>
131             Promise.reject(
132                 new EnrichedError('Failed to import share private key', {
133                     tags: {
134                         shareId,
135                     },
136                     extra: { e },
137                 })
138             )
139         );
141         sharesKeys.set(shareId, privateKey, sessionKey);
143         return {
144             privateKey,
145             sessionKey,
146         };
147     };
149     /**
150      * getSharePrivateKey returns private key used for link private key encryption.
151      */
152     const getSharePrivateKey = async (abortSignal: AbortSignal, shareId: string): Promise<PrivateKeyReference> => {
153         const keys = await getShareKeys(abortSignal, shareId);
154         return keys.privateKey;
155     };
157     /**
158      * getShareSessionKey returns session key used for sharing links.
159      */
160     const getShareSessionKey = async (
161         abortSignal: AbortSignal,
162         shareId: string,
163         linkPrivateKey?: PrivateKeyReference
164     ): Promise<SessionKey> => {
165         const keys = await getShareKeys(abortSignal, shareId, linkPrivateKey);
166         if (!keys.sessionKey) {
167             // This should not happen. All shares have session key, only
168             // publicly shared link will not have it, but it is bug if
169             // it is needed.
170             throw new Error('Share is missing session key');
171         }
172         return keys.sessionKey;
173     };
175     /**
176      * getShareCreatorKeys returns the share creator address' keys
177      * TODO: Change this function name as it doesn't fetch creator key but your own member keys for that share
178      * Also share.adressId can be null
179      */
180     const getShareCreatorKeys = async (abortSignal: AbortSignal, shareId: string) => {
181         const share = await getShareWithKey(abortSignal, shareId);
182         const keys = await driveCrypto.getOwnAddressAndPrimaryKeys(share.addressId);
184         return keys;
185     };
187     return {
188         getShareWithKey,
189         getShare,
190         getSharePrivateKey,
191         getShareSessionKey,
192         getShareCreatorKeys,
193         removeShares: sharesState.removeShares,
194     };