Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _links / useLinkActions.ts
blob4544e1e5db93f35c7b466270e59f824e1238146a
1 import { usePreventLeave } from '@proton/components';
2 import { CryptoProxy } from '@proton/crypto';
3 import { queryCreateFolder } from '@proton/shared/lib/api/drive/folder';
4 import { queryRenameLink } from '@proton/shared/lib/api/drive/share';
5 import {
6     encryptName,
7     generateLookupHash,
8     generateNodeHashKey,
9     generateNodeKeys,
10 } from '@proton/shared/lib/keys/driveKeys';
11 import { getDecryptedSessionKey } from '@proton/shared/lib/keys/drivePassphrase';
12 import getRandomString from '@proton/utils/getRandomString';
14 import { EnrichedError } from '../../utils/errorHandling/EnrichedError';
15 import { ValidationError } from '../../utils/errorHandling/ValidationError';
16 import { useDebouncedRequest } from '../_api';
17 import { useDriveEventManager } from '../_events';
18 import { useShare } from '../_shares';
19 import { useVolumesState } from '../_volumes';
20 import { encryptFolderExtendedAttributes } from './extendedAttributes';
21 import useLink from './useLink';
22 import { validateLinkName } from './validation';
24 /**
25  * useLinkActions provides actions for manipulating with individual link.
26  */
27 export default function useLinkActions() {
28     const { preventLeave } = usePreventLeave();
29     const debouncedRequest = useDebouncedRequest();
30     const events = useDriveEventManager();
31     const { getLink, getLinkPrivateKey, getLinkSessionKey, getLinkHashKey } = useLink();
32     const { getSharePrivateKey, getShareCreatorKeys } = useShare();
33     const volumeState = useVolumesState();
35     const createFolder = async (
36         abortSignal: AbortSignal,
37         shareId: string,
38         parentLinkId: string,
39         name: string,
40         modificationTime?: Date
41     ) => {
42         // Name Hash is generated from LC, for case-insensitive duplicate detection.
43         const error = validateLinkName(name);
44         if (error) {
45             throw new ValidationError(error);
46         }
48         const [parentPrivateKey, parentHashKey, { privateKey: addressKey, address }] = await Promise.all([
49             getLinkPrivateKey(abortSignal, shareId, parentLinkId),
50             getLinkHashKey(abortSignal, shareId, parentLinkId),
51             getShareCreatorKeys(abortSignal, shareId),
52         ]);
54         const [Hash, { NodeKey, NodePassphrase, privateKey, NodePassphraseSignature }, encryptedName] =
55             await Promise.all([
56                 generateLookupHash(name, parentHashKey).catch((e) =>
57                     Promise.reject(
58                         new EnrichedError('Failed to generate folder link lookup hash during folder creation', {
59                             tags: {
60                                 shareId,
61                                 parentLinkId,
62                             },
63                             extra: { e },
64                         })
65                     )
66                 ),
67                 generateNodeKeys(parentPrivateKey, addressKey).catch((e) =>
68                     Promise.reject(
69                         new EnrichedError('Failed to generate folder link node keys during folder creation', {
70                             tags: {
71                                 shareId,
72                                 parentLinkId,
73                             },
74                             extra: { e },
75                         })
76                     )
77                 ),
78                 encryptName(name, parentPrivateKey, addressKey).catch((e) =>
79                     Promise.reject(
80                         new EnrichedError('Failed to encrypt folder link name during folder creation', {
81                             tags: {
82                                 shareId,
83                                 parentLinkId,
84                             },
85                             extra: { e },
86                         })
87                     )
88                 ),
89             ]);
91         // We use private key instead of address key to sign the hash key
92         // because its internal property of the folder. We use address key for
93         // name or content to have option to trust some users more or less.
94         const { NodeHashKey } = await generateNodeHashKey(privateKey, privateKey).catch((e) =>
95             Promise.reject(
96                 new EnrichedError('Failed to encrypt node hash key during folder creation', {
97                     tags: {
98                         shareId,
99                         parentLinkId,
100                     },
101                     extra: { e },
102                 })
103             )
104         );
106         const xattr = !modificationTime
107             ? undefined
108             : await encryptFolderExtendedAttributes(modificationTime, privateKey, addressKey);
110         const { Folder } = await preventLeave(
111             debouncedRequest<{ Folder: { ID: string } }>(
112                 queryCreateFolder(shareId, {
113                     Hash,
114                     NodeHashKey,
115                     Name: encryptedName,
116                     NodeKey,
117                     NodePassphrase,
118                     NodePassphraseSignature,
119                     SignatureAddress: address.Email,
120                     ParentLinkID: parentLinkId,
121                     XAttr: xattr,
122                 })
123             )
124         );
126         const volumeId = volumeState.findVolumeId(shareId);
127         if (volumeId) {
128             await events.pollEvents.volumes(volumeId);
129         }
130         return Folder.ID;
131     };
133     const renameLink = async (abortSignal: AbortSignal, shareId: string, linkId: string, newName: string) => {
134         const error = validateLinkName(newName);
135         if (error) {
136             throw new ValidationError(error);
137         }
139         const [meta, { privateKey: addressKey, address }] = await Promise.all([
140             getLink(abortSignal, shareId, linkId),
141             getShareCreatorKeys(abortSignal, shareId),
142         ]);
144         if (meta.corruptedLink) {
145             throw new Error('Cannot rename corrupted file');
146         }
148         const [parentPrivateKey, parentHashKey] = await Promise.all([
149             meta.parentLinkId
150                 ? getLinkPrivateKey(abortSignal, shareId, meta.parentLinkId)
151                 : getSharePrivateKey(abortSignal, shareId),
152             meta.parentLinkId ? getLinkHashKey(abortSignal, shareId, meta.parentLinkId) : null,
153         ]);
155         const sessionKey = await getDecryptedSessionKey({
156             data: meta.encryptedName,
157             privateKeys: parentPrivateKey,
158         }).catch((e) =>
159             Promise.reject(
160                 new EnrichedError('Failed to decrypt link name session key during rename', {
161                     tags: {
162                         shareId,
163                         linkId,
164                     },
165                     extra: { e },
166                 })
167             )
168         );
170         const [Hash, { message: encryptedName }] = await Promise.all([
171             parentHashKey
172                 ? generateLookupHash(newName, parentHashKey).catch((e) =>
173                       Promise.reject(
174                           new EnrichedError('Failed to generate link lookup hash during rename', {
175                               tags: {
176                                   shareId,
177                                   linkId,
178                               },
179                               extra: { e },
180                           })
181                       )
182                   )
183                 : getRandomString(64),
184             CryptoProxy.encryptMessage({
185                 textData: newName,
186                 stripTrailingSpaces: true,
187                 sessionKey,
188                 signingKeys: addressKey,
189             }).catch((e) =>
190                 Promise.reject(
191                     new EnrichedError('Failed to encrypt link name during rename', {
192                         tags: {
193                             shareId,
194                             linkId,
195                         },
196                         extra: { e },
197                     })
198                 )
199             ),
200         ]);
202         await preventLeave(
203             debouncedRequest(
204                 queryRenameLink(shareId, linkId, {
205                     Name: encryptedName,
206                     Hash,
207                     SignatureAddress: address.Email,
208                     OriginalHash: meta.hash,
209                 })
210             )
211         );
212         const volumeId = volumeState.findVolumeId(shareId);
214         if (volumeId) {
215             await events.pollEvents.volumes(volumeId);
216         }
217     };
219     /**
220      * checkLinkMetaSignatures checks for all signatures of various attributes:
221      * passphrase, hash key, name or xattributes. It does not check content,
222      * that is file blocks including thumbnail block.
223      */
224     const checkLinkMetaSignatures = async (abortSignal: AbortSignal, shareId: string, linkId: string) => {
225         const [link] = await Promise.all([
226             // Decrypts name and xattributes.
227             getLink(abortSignal, shareId, linkId),
228             // Decrypts passphrase.
229             getLinkPrivateKey(abortSignal, shareId, linkId),
230         ]);
231         if (link.isFile) {
232             await getLinkSessionKey(abortSignal, shareId, linkId);
233         } else {
234             await getLinkHashKey(abortSignal, shareId, linkId);
235         }
236         // Get latest link with signature updates.
237         return (await getLink(abortSignal, shareId, linkId)).signatureIssues;
238     };
240     return {
241         createFolder,
242         renameLink,
243         checkLinkMetaSignatures,
244     };