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';
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';
25 * useLinkActions provides actions for manipulating with individual link.
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,
40 modificationTime?: Date
42 // Name Hash is generated from LC, for case-insensitive duplicate detection.
43 const error = validateLinkName(name);
45 throw new ValidationError(error);
48 const [parentPrivateKey, parentHashKey, { privateKey: addressKey, address }] = await Promise.all([
49 getLinkPrivateKey(abortSignal, shareId, parentLinkId),
50 getLinkHashKey(abortSignal, shareId, parentLinkId),
51 getShareCreatorKeys(abortSignal, shareId),
54 const [Hash, { NodeKey, NodePassphrase, privateKey, NodePassphraseSignature }, encryptedName] =
56 generateLookupHash(name, parentHashKey).catch((e) =>
58 new EnrichedError('Failed to generate folder link lookup hash during folder creation', {
67 generateNodeKeys(parentPrivateKey, addressKey).catch((e) =>
69 new EnrichedError('Failed to generate folder link node keys during folder creation', {
78 encryptName(name, parentPrivateKey, addressKey).catch((e) =>
80 new EnrichedError('Failed to encrypt folder link name during folder creation', {
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) =>
96 new EnrichedError('Failed to encrypt node hash key during folder creation', {
106 const xattr = !modificationTime
108 : await encryptFolderExtendedAttributes(modificationTime, privateKey, addressKey);
110 const { Folder } = await preventLeave(
111 debouncedRequest<{ Folder: { ID: string } }>(
112 queryCreateFolder(shareId, {
118 NodePassphraseSignature,
119 SignatureAddress: address.Email,
120 ParentLinkID: parentLinkId,
126 const volumeId = volumeState.findVolumeId(shareId);
128 await events.pollEvents.volumes(volumeId);
133 const renameLink = async (abortSignal: AbortSignal, shareId: string, linkId: string, newName: string) => {
134 const error = validateLinkName(newName);
136 throw new ValidationError(error);
139 const [meta, { privateKey: addressKey, address }] = await Promise.all([
140 getLink(abortSignal, shareId, linkId),
141 getShareCreatorKeys(abortSignal, shareId),
144 if (meta.corruptedLink) {
145 throw new Error('Cannot rename corrupted file');
148 const [parentPrivateKey, parentHashKey] = await Promise.all([
150 ? getLinkPrivateKey(abortSignal, shareId, meta.parentLinkId)
151 : getSharePrivateKey(abortSignal, shareId),
152 meta.parentLinkId ? getLinkHashKey(abortSignal, shareId, meta.parentLinkId) : null,
155 const sessionKey = await getDecryptedSessionKey({
156 data: meta.encryptedName,
157 privateKeys: parentPrivateKey,
160 new EnrichedError('Failed to decrypt link name session key during rename', {
170 const [Hash, { message: encryptedName }] = await Promise.all([
172 ? generateLookupHash(newName, parentHashKey).catch((e) =>
174 new EnrichedError('Failed to generate link lookup hash during rename', {
183 : getRandomString(64),
184 CryptoProxy.encryptMessage({
186 stripTrailingSpaces: true,
188 signingKeys: addressKey,
191 new EnrichedError('Failed to encrypt link name during rename', {
204 queryRenameLink(shareId, linkId, {
207 SignatureAddress: address.Email,
208 OriginalHash: meta.hash,
212 const volumeId = volumeState.findVolumeId(shareId);
215 await events.pollEvents.volumes(volumeId);
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.
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),
232 await getLinkSessionKey(abortSignal, shareId, linkId);
234 await getLinkHashKey(abortSignal, shareId, linkId);
236 // Get latest link with signature updates.
237 return (await getLink(abortSignal, shareId, linkId)).signatureIssues;
243 checkLinkMetaSignatures,