Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / applications / drive / src / app / store / _links / useLink.ts
blob21aac66bf4cf42633017ca34323bc325c2395b55
1 import { useRef } from 'react';
3 import { fromUnixTime, isAfter } from 'date-fns';
4 import { c } from 'ttag';
6 import type { PrivateKeyReference, SessionKey } from '@proton/crypto';
7 import { CryptoProxy, VERIFICATION_STATUS } from '@proton/crypto';
8 import { queryFileRevision, queryFileRevisionThumbnail } from '@proton/shared/lib/api/drive/files';
9 import { queryGetLink } from '@proton/shared/lib/api/drive/link';
10 import { RESPONSE_CODE } from '@proton/shared/lib/drive/constants';
11 import { base64StringToUint8Array } from '@proton/shared/lib/helpers/encoding';
12 import type {
13     DriveFileRevisionResult,
14     DriveFileRevisionThumbnailResult,
15 } from '@proton/shared/lib/interfaces/drive/file';
16 import type { LinkMetaResult } from '@proton/shared/lib/interfaces/drive/link';
17 import { decryptSigned } from '@proton/shared/lib/keys/driveKeys';
18 import { decryptPassphrase, getDecryptedSessionKey } from '@proton/shared/lib/keys/drivePassphrase';
19 import useFlag from '@proton/unleash/useFlag';
21 import { isIgnoredError, isIgnoredErrorForReporting, sendErrorReport } from '../../utils/errorHandling';
22 import { EnrichedError } from '../../utils/errorHandling/EnrichedError';
23 import { getIsPublicContext } from '../../utils/getIsPublicContext';
24 import { tokenIsValid } from '../../utils/url/token';
25 import { linkMetaToEncryptedLink, revisionPayloadToRevision, useDebouncedRequest } from '../_api';
26 import type { IntegrityMetrics, VerificationKey } from '../_crypto';
27 import { integrityMetrics, useDriveCrypto } from '../_crypto';
28 import {
29     type Share,
30     ShareType,
31     type ShareTypeString,
32     type ShareWithKey,
33     getShareTypeString,
34     useDefaultShare,
35     useShare,
36 } from '../_shares';
37 import { useDirectSharingInfo } from '../_shares/useDirectSharingInfo';
38 import { useIsPaid } from '../_user';
39 import { useDebouncedFunction } from '../_utils';
40 import { decryptExtendedAttributes } from './extendedAttributes';
41 import type { DecryptedLink, EncryptedLink, SignatureIssueLocation, SignatureIssues } from './interface';
42 import { isDecryptedLinkSame } from './link';
43 import useLinksKeys from './useLinksKeys';
44 import useLinksState from './useLinksState';
46 // Interval should not be too low to not cause spikes on the server but at the
47 // same time not too high to not overflow available memory on the device.
48 const FAILING_FETCH_BACKOFF_MS = 10 * 60 * 1000; // 10 minutes.
49 const generateCorruptDecryptedLink = (encryptedLink: EncryptedLink, name: string): DecryptedLink => ({
50     encryptedName: encryptedLink.name,
51     name,
52     linkId: encryptedLink.linkId,
53     createTime: encryptedLink.createTime,
54     corruptedLink: true,
55     activeRevision: encryptedLink.activeRevision,
56     digests: { sha1: '' },
57     hash: encryptedLink.hash,
58     size: encryptedLink.size,
59     originalSize: encryptedLink.size,
60     fileModifyTime: encryptedLink.metaDataModifyTime,
61     metaDataModifyTime: encryptedLink.metaDataModifyTime,
62     isFile: encryptedLink.isFile,
63     mimeType: encryptedLink.mimeType,
64     hasThumbnail: encryptedLink.hasThumbnail,
65     isShared: encryptedLink.isShared,
66     parentLinkId: encryptedLink.parentLinkId,
67     rootShareId: encryptedLink.rootShareId,
68     signatureIssues: encryptedLink.signatureIssues,
69     originalDimensions: {
70         height: 0,
71         width: 0,
72     },
73     trashed: encryptedLink.trashed,
74     volumeId: encryptedLink.volumeId,
75 });
77 export default function useLink() {
78     const linksKeys = useLinksKeys();
79     const linksState = useLinksState();
80     const { getVerificationKey } = useDriveCrypto();
81     const { getSharePrivateKey, getShare } = useShare();
82     const { getDefaultShareAddressEmail } = useDefaultShare();
83     const { getDirectSharingInfo } = useDirectSharingInfo();
85     const isPaid = useIsPaid();
87     const debouncedRequest = useDebouncedRequest();
88     const fetchLink = async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<EncryptedLink> => {
89         const { Link } = await debouncedRequest<LinkMetaResult>(
90             {
91                 ...queryGetLink(shareId, linkId),
92                 // Ignore HTTP errors (e.g. "Not Found", "Unprocessable Entity"
93                 // etc). Not every `fetchLink` call relates to a user action
94                 // (it might be a helper function for a background job). Hence,
95                 // there are potential cases when displaying such messages will
96                 // confuse the user. Every higher-level caller should handle it
97                 // based on the context.
98                 silence: true,
99             },
100             abortSignal
101         );
102         return linkMetaToEncryptedLink(Link, shareId);
103     };
105     return useLinkInner(
106         fetchLink,
107         linksKeys,
108         linksState,
109         getVerificationKey,
110         getSharePrivateKey,
111         getShare,
112         getDefaultShareAddressEmail,
113         getDirectSharingInfo,
114         isPaid,
115         integrityMetrics,
116         CryptoProxy.importPrivateKey
117     );
120 export function useLinkInner(
121     fetchLink: (abortSignal: AbortSignal, shareId: string, linkId: string) => Promise<EncryptedLink>,
122     linksKeys: Pick<
123         ReturnType<typeof useLinksKeys>,
124         | 'getPassphrase'
125         | 'setPassphrase'
126         | 'getPassphraseSessionKey'
127         | 'setPassphraseSessionKey'
128         | 'getPrivateKey'
129         | 'setPrivateKey'
130         | 'getSessionKey'
131         | 'setSessionKey'
132         | 'getHashKey'
133         | 'setHashKey'
134     >,
135     linksState: Pick<ReturnType<typeof useLinksState>, 'getLink' | 'setLinks' | 'setCachedThumbnail'>,
136     getVerificationKey: ReturnType<typeof useDriveCrypto>['getVerificationKey'],
137     getSharePrivateKey: ReturnType<typeof useShare>['getSharePrivateKey'],
138     getShare: ReturnType<typeof useShare>['getShare'],
139     getDefaultShareAddressEmail: ReturnType<typeof useDefaultShare>['getDefaultShareAddressEmail'],
140     getDirectSharingInfo: ReturnType<typeof useDirectSharingInfo>['getDirectSharingInfo'],
141     userIsPaid: boolean,
142     integrityMetrics: IntegrityMetrics,
143     importPrivateKey: typeof CryptoProxy.importPrivateKey // passed as arg for easier mocking when testing
144 ) {
145     const debouncedFunction = useDebouncedFunction();
146     const debouncedRequest = useDebouncedRequest();
147     const isDecryptionErrorDebuggingEnabled = useFlag('DriveDecryptionErrorDebugging');
149     // Cache certain API errors in order to avoid sending multiple requests to
150     // the same failing link. For example, trying to fetch the same missing
151     // parent link for multiple descendants (when processing already outdated
152     // events).
153     const linkFetchErrors = useRef<{ [key: string]: any }>({});
155     const fetchLinkDONOTUSE = fetchLink;
156     fetchLink = async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<EncryptedLink> => {
157         const err = linkFetchErrors.current[shareId + linkId];
158         if (err) {
159             throw err;
160         }
162         return fetchLinkDONOTUSE(abortSignal, shareId, linkId).catch((err) => {
163             if (
164                 [RESPONSE_CODE.NOT_FOUND, RESPONSE_CODE.NOT_ALLOWED, RESPONSE_CODE.INVALID_ID].includes(err?.data?.Code)
165             ) {
166                 linkFetchErrors.current[shareId + linkId] = err;
167                 setTimeout(() => {
168                     delete linkFetchErrors.current[shareId + linkId];
169                 }, FAILING_FETCH_BACKOFF_MS);
170             }
171             throw err;
172         });
173     };
175     const loadShareTypeString = async (shareId: string): Promise<ShareTypeString> => {
176         return (
177             getShare(new AbortController().signal, shareId)
178                 .then(getShareTypeString)
179                 // getShare should be fast call as share is already cached by this time.
180                 // In case of failure, fallback 'shared' is good assumption as it might
181                 // mean some edge case for sharing.
182                 // After refactor, this should be handled better.
183                 .catch(() => 'shared')
184         );
185     };
187     const handleDecryptionError = (shareId: string, encryptedLink: EncryptedLink) => {
188         loadShareTypeString(shareId).then((shareType) => {
189             const options = {
190                 isPaid: userIsPaid,
191                 createTime: encryptedLink.createTime,
192             };
193             integrityMetrics.nodeDecryptionError(encryptedLink.linkId, shareType, options);
194         });
195     };
197     const reportSignatureError = (shareId: string, encryptedLink: EncryptedLink, location: SignatureIssueLocation) => {
198         // This means that the shareId is a token for a public link (bookmarking) and does not need signatureCheck
199         // Exemple:
200         // - shareId: D4TVgdFKidFgQWd5IeXYyegjDNV9KWF1HDwjxZlesUo-Wc2NTL8mUQc6IlYwowznc5vHQkTL4iUbn6K0CorrjQ==
201         // - token: 2NR85F8NSC
202         if (tokenIsValid(shareId)) {
203             return;
204         }
205         loadShareTypeString(shareId).then(async (shareType) => {
206             const email = await getDefaultShareAddressEmail();
207             const verificationKey = {
208                 passphrase: 'SignatureEmail',
209                 hash: 'NodeKey',
210                 name: 'NameSignatureEmail',
211                 xattrs: 'SignatureEmail',
212                 contentKeyPacket: 'NodeKey',
213                 blocks: 'NodeKey',
214                 thumbnail: 'NodeKey',
215                 manifest: 'NodeKey',
216             }[location] as VerificationKey;
218             const options = {
219                 isPaid: userIsPaid,
220                 createTime: encryptedLink.createTime,
221                 addressMatchingDefaultShare: encryptedLink.signatureAddress === email,
222             };
223             integrityMetrics.signatureVerificationError(encryptedLink.linkId, shareType, verificationKey, options);
224         });
225     };
227     const handleSignatureCheck = (
228         shareId: string,
229         encryptedLink: EncryptedLink,
230         location: SignatureIssueLocation,
231         verified: VERIFICATION_STATUS
232     ) => {
233         if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
234             const signatureIssues: SignatureIssues = {};
235             signatureIssues[location] = verified;
236             linksState.setLinks(shareId, [
237                 {
238                     encrypted: {
239                         ...encryptedLink,
240                         signatureIssues,
241                     },
242                 },
243             ]);
245             reportSignatureError(shareId, encryptedLink, location);
246         }
247     };
249     /**
250      * debouncedFunctionDecorator wraps original callback with debouncedFunction
251      * to ensure that if even two or more calls with the same parameters are
252      * executed only once. E.g., to not decrypt the same link keys twice.
253      */
254     const debouncedFunctionDecorator = <T>(
255         cacheKey: string,
256         callback: (abortSignal: AbortSignal, shareId: string, linkId: string) => Promise<T>
257     ): ((abortSignal: AbortSignal, shareId: string, linkId: string) => Promise<T>) => {
258         const wrapper = async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<T> => {
259             return debouncedFunction(
260                 async (abortSignal: AbortSignal) => {
261                     return callback(abortSignal, shareId, linkId);
262                 },
263                 [cacheKey, shareId, linkId],
264                 abortSignal
265             );
266         };
267         return wrapper;
268     };
270     const getEncryptedLink = debouncedFunctionDecorator(
271         'getEncryptedLink',
272         async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<EncryptedLink> => {
273             const cachedLink = linksState.getLink(shareId, linkId);
274             if (cachedLink) {
275                 return cachedLink.encrypted;
276             }
277             const link = await fetchLink(abortSignal, shareId, linkId);
278             linksState.setLinks(shareId, [{ encrypted: link }]);
279             return link;
280         }
281     );
283     /**
284      * getLinkPassphraseAndSessionKey returns the passphrase with session key
285      * used for locking the private key.
286      */
287     const getLinkPassphraseAndSessionKey = debouncedFunctionDecorator(
288         'getLinkPassphraseAndSessionKey',
289         async (
290             abortSignal: AbortSignal,
291             shareId: string,
292             linkId: string
293         ): Promise<{ passphrase: string; passphraseSessionKey: SessionKey; encryptedLink?: EncryptedLink }> => {
294             const passphrase = linksKeys.getPassphrase(shareId, linkId);
295             const sessionKey = linksKeys.getPassphraseSessionKey(shareId, linkId);
296             if (passphrase && sessionKey) {
297                 return { passphrase, passphraseSessionKey: sessionKey };
298             }
300             const encryptedLink = await getEncryptedLink(abortSignal, shareId, linkId);
302             const parentPrivateKeyPromise = encryptedLink.parentLinkId
303                 ? // eslint-disable-next-line @typescript-eslint/no-use-before-define
304                   getLinkPrivateKey(abortSignal, shareId, encryptedLink.parentLinkId)
305                 : getSharePrivateKey(abortSignal, shareId);
306             const [parentPrivateKey, addressPublicKey] = await Promise.all([
307                 parentPrivateKeyPromise,
308                 encryptedLink.signatureAddress ? getVerificationKey(encryptedLink.signatureAddress) : [],
309             ]);
311             try {
312                 // Fallback to parent NodeKey in case if we don't have addressPublicKey (Anonymous upload)
313                 const publicKeys = encryptedLink.signatureAddress ? addressPublicKey : [parentPrivateKey];
314                 const {
315                     decryptedPassphrase,
316                     sessionKey: passphraseSessionKey,
317                     verified,
318                 } = await decryptPassphrase({
319                     armoredPassphrase: encryptedLink.nodePassphrase,
320                     armoredSignature: encryptedLink.nodePassphraseSignature,
321                     privateKeys: [parentPrivateKey],
322                     publicKeys,
323                     validateSignature: false,
324                 });
326                 handleSignatureCheck(shareId, encryptedLink, 'passphrase', verified);
328                 linksKeys.setPassphrase(shareId, linkId, decryptedPassphrase);
329                 linksKeys.setPassphraseSessionKey(shareId, linkId, passphraseSessionKey);
331                 return {
332                     passphrase: decryptedPassphrase,
333                     passphraseSessionKey,
334                     encryptedLink,
335                 };
336             } catch (e) {
337                 throw new EnrichedError('Failed to decrypt link passphrase', {
338                     tags: {
339                         shareId,
340                         linkId,
341                     },
342                     extra: { e, crypto: true },
343                 });
344             }
345         }
346     );
348     /**
349      * getLinkPrivateKey returns the private key used for link meta data encryption.
350      */
351     const getLinkPrivateKey = debouncedFunctionDecorator(
352         'getLinkPrivateKey',
353         async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<PrivateKeyReference> => {
354             let privateKey = linksKeys.getPrivateKey(shareId, linkId);
355             if (privateKey) {
356                 return privateKey;
357             }
359             // getLinkPassphraseAndSessionKey already call getEncryptedLink, prevent to call fetchLink twice
360             let { passphrase, encryptedLink } = await getLinkPassphraseAndSessionKey(abortSignal, shareId, linkId);
361             encryptedLink = encryptedLink || (await getEncryptedLink(abortSignal, shareId, linkId));
363             try {
364                 privateKey = await importPrivateKey({ armoredKey: encryptedLink.nodeKey, passphrase });
365             } catch (e) {
366                 throw new EnrichedError('Failed to import link private key', {
367                     tags: {
368                         shareId,
369                         linkId,
370                     },
371                     extra: { e, crypto: true },
372                 });
373             }
375             linksKeys.setPrivateKey(shareId, linkId, privateKey);
376             return privateKey;
377         }
378     );
380     /**
381      * getLinkSessionKey returns the session key used for block encryption.
382      */
383     const getLinkSessionKey = debouncedFunctionDecorator(
384         'getLinkSessionKey',
385         async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<SessionKey> => {
386             let sessionKey = linksKeys.getSessionKey(shareId, linkId);
387             if (sessionKey) {
388                 return sessionKey;
389             }
391             const encryptedLink = await getEncryptedLink(abortSignal, shareId, linkId);
392             if (!encryptedLink.contentKeyPacket) {
393                 // This is dev error, should not happen in the wild.
394                 throw new Error('Content key is available only in file context');
395             }
397             const privateKey = await getLinkPrivateKey(abortSignal, shareId, linkId);
398             const blockKeys = base64StringToUint8Array(encryptedLink.contentKeyPacket);
400             try {
401                 sessionKey = await getDecryptedSessionKey({
402                     data: blockKeys,
403                     privateKeys: privateKey,
404                 });
405             } catch (e) {
406                 throw new EnrichedError('Failed to decrypt link session key', {
407                     tags: {
408                         shareId,
409                         linkId,
410                     },
411                     extra: { e, crypto: true },
412                 });
413             }
415             if (encryptedLink.contentKeyPacketSignature) {
416                 const publicKeys = [privateKey, ...(await getVerificationKey(encryptedLink.signatureAddress))];
417                 const { verified } = await CryptoProxy.verifyMessage({
418                     binaryData: sessionKey.data,
419                     verificationKeys: publicKeys,
420                     armoredSignature: encryptedLink.contentKeyPacketSignature,
421                 });
422                 // iOS signed content key instead of session key in the past.
423                 // Therefore we need to check that as well until we migrate
424                 // old files.
425                 if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
426                     const { verified: blockKeysVerified } = await CryptoProxy.verifyMessage({
427                         binaryData: blockKeys,
428                         verificationKeys: publicKeys,
429                         armoredSignature: encryptedLink.contentKeyPacketSignature,
430                     });
431                     if (blockKeysVerified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
432                         // If even fall back solution does not succeed, report
433                         // the original verified status of the session key as
434                         // that one is the one we want to verify here.
435                         handleSignatureCheck(shareId, encryptedLink, 'contentKeyPacket', verified);
436                     }
437                 }
438             }
440             linksKeys.setSessionKey(shareId, linkId, sessionKey);
441             return sessionKey;
442         }
443     );
445     /**
446      * getLinkHashKey returns the hash key used for checking name collisions.
447      */
448     const getLinkHashKey = debouncedFunctionDecorator(
449         'getLinkHashKey',
450         async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<Uint8Array> => {
451             let cachedHashKey = linksKeys.getHashKey(shareId, linkId);
452             if (cachedHashKey) {
453                 return cachedHashKey;
454             }
456             const encryptedLink = await getEncryptedLink(abortSignal, shareId, linkId);
457             if (!encryptedLink.nodeHashKey) {
458                 // This is dev error, should not happen in the wild.
459                 throw new Error('Hash key is available only in folder context');
460             }
462             const [privateKey, addressPrivateKey] = await Promise.all([
463                 getLinkPrivateKey(abortSignal, shareId, linkId),
464                 getVerificationKey(encryptedLink.signatureAddress),
465             ]);
466             // In the past we had misunderstanding what key is used to sign
467             // hash key. Originally it meant to be node key, which web used
468             // for all links besides the root one, where address key was
469             // used instead. Similarly, iOS or Android used address key for
470             // all links. Latest versions should use node key in all cases
471             // but we accept also address key. Its still signed with valid
472             // key. In future we might re-sign bad links so we can get rid
473             // of this.
474             const publicKey = [privateKey, ...addressPrivateKey];
476             try {
477                 const { data: hashKey, verified } = await decryptSigned({
478                     armoredMessage: encryptedLink.nodeHashKey,
479                     privateKey,
480                     publicKey,
481                     format: 'binary',
482                 });
484                 if (
485                     verified === VERIFICATION_STATUS.SIGNED_AND_INVALID ||
486                     // The hash was not signed until Beta 17 (DRVWEB-1219).
487                     (verified === VERIFICATION_STATUS.NOT_SIGNED &&
488                         isAfter(fromUnixTime(encryptedLink.createTime), new Date(2021, 7, 1)))
489                 ) {
490                     handleSignatureCheck(shareId, encryptedLink, 'hash', verified);
491                 }
493                 linksKeys.setHashKey(shareId, linkId, hashKey);
494                 return hashKey;
495             } catch (e) {
496                 throw new EnrichedError('Failed to decrypt link hash key', {
497                     tags: {
498                         shareId,
499                         linkId,
500                     },
501                     extra: { e, crypto: true },
502                 });
503             }
504         }
505     );
507     const getLinkRevision = async (
508         abortSignal: AbortSignal,
509         { shareId, linkId, revisionId }: { shareId: string; linkId: string; revisionId: string }
510     ) => {
511         const { Revision } = await debouncedRequest<DriveFileRevisionResult>(
512             queryFileRevision(shareId, linkId, revisionId),
513             abortSignal
514         );
515         return revisionPayloadToRevision(Revision);
516     };
518     /**
519      * decryptLink decrypts provided `encryptedLink`. The result is not stored
520      * anywhere, only returned back.
521      */
522     const decryptLink = async (
523         abortSignal: AbortSignal,
524         shareId: string,
525         encryptedLink: EncryptedLink,
526         revisionId?: string,
527         share?: ShareWithKey | Share
528     ): Promise<DecryptedLink> => {
529         return debouncedFunction(
530             async (abortSignal: AbortSignal): Promise<DecryptedLink> => {
531                 const namePromise = new Promise<{ name: string; nameVerified: VERIFICATION_STATUS }>(
532                     async (resolve, reject) => {
533                         try {
534                             const privateKey = !encryptedLink.parentLinkId
535                                 ? await getSharePrivateKey(abortSignal, shareId)
536                                 : await getLinkPrivateKey(abortSignal, shareId, encryptedLink.parentLinkId);
537                             const signatureAddress =
538                                 encryptedLink.nameSignatureAddress || encryptedLink.signatureAddress;
539                             const publicKey = signatureAddress
540                                 ? await getVerificationKey(signatureAddress)
541                                 : privateKey;
542                             const { data, verified } = await decryptSigned({
543                                 armoredMessage: encryptedLink.name,
544                                 privateKey,
545                                 // nameSignatureAddress is missing for some old files.
546                                 // Fallback to signatureAddress might result in failed
547                                 // signature check, but no one reported it so far so
548                                 // we should be good. Important is that user can access
549                                 // the file and the verification do not hard fail.
550                                 // If we find out that it doesnt work for some user,
551                                 // we could skip the verification instead. But the best
552                                 // would be to fix it properly in the database.
553                                 publicKey,
554                             });
555                             resolve({ name: data, nameVerified: verified });
556                         } catch (error) {
557                             reject(error);
558                         }
559                     }
560                 );
562                 const revision = !!revisionId
563                     ? await getLinkRevision(abortSignal, { shareId, linkId: encryptedLink.linkId, revisionId })
564                     : undefined;
565                 // Files have signature address on the revision.
566                 // Folders have signature address on the link itself.
567                 const signatureAddress =
568                     revision?.signatureAddress ||
569                     encryptedLink.activeRevision?.signatureAddress ||
570                     encryptedLink.signatureAddress;
571                 const xattrPromise = !encryptedLink.xAttr
572                     ? {
573                           fileModifyTime: encryptedLink.metaDataModifyTime,
574                           fileModifyTimeVerified: VERIFICATION_STATUS.SIGNED_AND_VALID,
575                           originalSize: undefined,
576                           originalDimensions: undefined,
577                           digests: undefined,
578                           duration: undefined,
579                       }
580                     : getLinkPrivateKey(abortSignal, shareId, encryptedLink.linkId)
581                           .then(async (privateKey) =>
582                               decryptExtendedAttributes(
583                                   encryptedLink.xAttr,
584                                   privateKey,
586                                   signatureAddress ? await getVerificationKey(signatureAddress) : privateKey
587                               )
588                           )
589                           .then(({ xattrs, verified }) => ({
590                               fileModifyTime: xattrs.Common.ModificationTime || encryptedLink.metaDataModifyTime,
591                               fileModifyTimeVerified: verified,
592                               originalSize: xattrs.Common?.Size,
593                               originalDimensions: xattrs.Media
594                                   ? {
595                                         width: xattrs.Media.Width,
596                                         height: xattrs.Media.Height,
597                                     }
598                                   : undefined,
599                               duration: xattrs.Media?.Duration,
600                               digests: xattrs.Common?.Digests
601                                   ? {
602                                         sha1: xattrs.Common.Digests.SHA1,
603                                     }
604                                   : undefined,
605                           }));
607                 const [nameResult, xattrResult] = await Promise.allSettled([namePromise, xattrPromise]);
609                 if (nameResult.status === 'rejected') {
610                     // 'AbortError' signify the user has navigated away mid-decryption
611                     // We don't count this as error in our metrics
612                     if (nameResult.reason instanceof Error && isIgnoredErrorForReporting(nameResult.reason)) {
613                         return generateCorruptDecryptedLink(encryptedLink, '�');
614                     }
616                     // Temp: debugging decryption issues
617                     try {
618                         if (isDecryptionErrorDebuggingEnabled) {
619                             sendErrorReport(
620                                 new EnrichedError(
621                                     nameResult.reason instanceof Error
622                                         ? nameResult.reason.message
623                                         : nameResult.reason.toString(),
624                                     {
625                                         tags: {
626                                             attribute: 'name',
627                                             shareId,
628                                             linkId: encryptedLink.linkId,
629                                             revisionId: revisionId || encryptedLink.activeRevision?.id,
630                                         },
631                                         extra: { e: nameResult.reason },
632                                     },
633                                     'Decryption error'
634                                 )
635                             );
636                         }
637                     } catch {
638                         /* silent */
639                     }
640                     handleDecryptionError(shareId, encryptedLink);
641                     return generateCorruptDecryptedLink(encryptedLink, '�');
642                 }
644                 const { nameVerified, name } = nameResult.value;
646                 const signatureIssues: SignatureIssues = {};
647                 if (
648                     nameVerified === VERIFICATION_STATUS.SIGNED_AND_INVALID ||
649                     // The name was not signed until Beta 3 (DRVWEB-673).
650                     (nameVerified === VERIFICATION_STATUS.NOT_SIGNED &&
651                         isAfter(fromUnixTime(encryptedLink.createTime), new Date(2021, 0, 1)))
652                 ) {
653                     reportSignatureError(shareId, encryptedLink, 'name');
654                     signatureIssues.name = nameVerified;
655                 }
657                 if (xattrResult.status === 'rejected') {
658                     // 'AbortError' signify the user has navigated away mid-decryption
659                     // We don't count this as error in our metrics
660                     if (xattrResult.reason instanceof Error && isIgnoredErrorForReporting(xattrResult.reason)) {
661                         return generateCorruptDecryptedLink(encryptedLink, name);
662                     }
664                     // Temp: debugging decryption issues
665                     try {
666                         if (isDecryptionErrorDebuggingEnabled) {
667                             sendErrorReport(
668                                 new EnrichedError(
669                                     xattrResult.reason instanceof Error
670                                         ? xattrResult.reason.message
671                                         : xattrResult.reason.toString(),
672                                     {
673                                         tags: {
674                                             attribute: 'xattr',
675                                             shareId,
676                                             linkId: encryptedLink.linkId,
677                                             revisionId: revisionId || encryptedLink.activeRevision?.id,
678                                         },
679                                         extra: { e: xattrResult.reason },
680                                     },
681                                     'Decryption error'
682                                 )
683                             );
684                         }
685                     } catch {
686                         /* silent */
687                     }
688                     handleDecryptionError(shareId, encryptedLink);
689                     return generateCorruptDecryptedLink(encryptedLink, name);
690                 }
691                 const { fileModifyTimeVerified, fileModifyTime, originalSize, originalDimensions, digests, duration } =
692                     xattrResult.value;
694                 if (fileModifyTimeVerified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
695                     reportSignatureError(shareId, encryptedLink, 'xattrs');
696                     signatureIssues.xattrs = fileModifyTimeVerified;
697                 }
699                 // Share will already be in cache due to getSharePrivateKey above
700                 const shareResult = !encryptedLink.parentLinkId
701                     ? share || (await getShare(abortSignal, shareId))
702                     : undefined;
704                 // Sharing info will only be get in case the share is not own by the current user
705                 // Also we only want it in the shared with me root view, so we need to check if parentLinkId is not present
706                 // TODO: Improve that as we remove the parentLinkId for now, but it will be present in the future
707                 let sharingInfo =
708                     !getIsPublicContext() && encryptedLink.sharingDetails && !encryptedLink.parentLinkId
709                         ? await getDirectSharingInfo(abortSignal, encryptedLink.sharingDetails?.shareId)
710                         : undefined;
712                 let displayName = name;
713                 if (shareResult?.type === ShareType.default) {
714                     displayName = c('Title').t`My files`;
715                 } else if (shareResult?.type === ShareType.photos) {
716                     displayName = c('Title').t`Photos`;
717                 }
719                 return {
720                     ...encryptedLink,
721                     ...(sharingInfo ? { sharedOn: sharingInfo.sharedOn, sharedBy: sharingInfo.sharedBy } : undefined),
722                     encryptedName: encryptedLink.name,
723                     name: displayName,
724                     fileModifyTime: fileModifyTime,
725                     originalSize,
726                     originalDimensions,
727                     duration,
728                     signatureIssues: Object.keys(signatureIssues).length > 0 ? signatureIssues : undefined,
729                     digests,
730                 };
731             },
732             ['decryptLink', shareId, encryptedLink.linkId],
733             abortSignal
734         );
735     };
737     /**
738      * getLInk provides decrypted link. If the cached link is available, it is
739      * returned right away. In other cases it might first fetch link from API,
740      * or just decrypt the encrypted cached one. If the decrypted link is stale
741      * (that means new version of encrypted link was fetched but not decrypted
742      * yet), it is first re-decrypted.
743      */
744     const getLink = debouncedFunctionDecorator(
745         'getLink',
746         async (
747             abortSignal: AbortSignal,
748             shareId: string,
749             linkId: string,
750             share?: ShareWithKey | Share
751         ): Promise<DecryptedLink> => {
752             const cachedLink = linksState.getLink(shareId, linkId);
753             if (cachedLink && cachedLink.decrypted && !cachedLink.decrypted.isStale) {
754                 return cachedLink.decrypted;
755             }
757             const encrypted = await getEncryptedLink(abortSignal, shareId, linkId);
759             try {
760                 const decrypted = await decryptLink(abortSignal, shareId, encrypted, undefined, share);
762                 linksState.setLinks(shareId, [{ encrypted, decrypted }]);
764                 return decrypted;
765             } catch (e) {
766                 if (isIgnoredError(e)) {
767                     return generateCorruptDecryptedLink(encrypted, '�');
768                 }
770                 throw new EnrichedError('Failed to decrypt link', {
771                     tags: {
772                         shareId,
773                         linkId,
774                     },
775                     extra: { e, crypto: true },
776                 });
777             }
778         }
779     );
781     /**
782      * loadFreshLink always fetches the fresh link meta data from API, but
783      * the decryption is done only when its needed. Anyway, this should be
784      * used only when really needed, for example, if we need to make sure if
785      * the link doesn't have any shared link already before creating new one.
786      */
787     const loadFreshLink = async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<DecryptedLink> => {
788         const cachedLink = linksState.getLink(shareId, linkId);
789         const encryptedLink = await fetchLink(abortSignal, shareId, linkId);
791         try {
792             const decryptedLink =
793                 cachedLink && isDecryptedLinkSame(cachedLink.encrypted, encryptedLink)
794                     ? undefined
795                     : await decryptLink(abortSignal, shareId, encryptedLink);
797             linksState.setLinks(shareId, [{ encrypted: encryptedLink, decrypted: decryptedLink }]);
799             return linksState.getLink(shareId, linkId)?.decrypted as DecryptedLink;
800         } catch (e) {
801             if (isIgnoredError(e)) {
802                 return generateCorruptDecryptedLink(encryptedLink, '�');
803             }
805             throw new EnrichedError('Failed to decrypt link', {
806                 tags: {
807                     shareId,
808                     linkId,
809                 },
810                 extra: { e, crypto: true },
811             });
812         }
813     };
815     /**
816      * loadLinkThumbnail gets thumbnail URL either from cached link or fetches
817      * it from API, then downloads the thumbnail block and decrypts it using
818      * `downloadCallback`, and finally creates local URL to it which is set to
819      * the cached link.
820      */
821     const loadLinkThumbnail = async (
822         abortSignal: AbortSignal,
823         shareId: string,
824         linkId: string,
825         downloadCallback: (
826             downloadUrl: string,
827             downloadToken: string
828         ) => Promise<{ contents: Promise<Uint8Array[]>; verifiedPromise: Promise<VERIFICATION_STATUS> }>
829     ): Promise<string | undefined> => {
830         const link = await getLink(abortSignal, shareId, linkId);
831         if (link.cachedThumbnailUrl || !link.hasThumbnail || !link.activeRevision) {
832             return link.cachedThumbnailUrl;
833         }
835         let downloadInfo = {
836             isFresh: false,
837             downloadUrl: link.activeRevision.thumbnail?.bareUrl,
838             downloadToken: link.activeRevision.thumbnail?.token,
839         };
841         const loadDownloadUrl = async (activeRevisionId: string) => {
842             const res = (await debouncedRequest(
843                 queryFileRevisionThumbnail(shareId, linkId, activeRevisionId)
844             )) as DriveFileRevisionThumbnailResult;
846             return {
847                 isFresh: true,
848                 downloadUrl: res.ThumbnailBareURL,
849                 downloadToken: res.ThumbnailToken,
850             };
851         };
853         const loadThumbnailUrl = async (downloadUrl: string, downloadToken: string): Promise<string> => {
854             const { contents, verifiedPromise } = await downloadCallback(downloadUrl, downloadToken);
855             const data = await contents;
856             const url = URL.createObjectURL(new Blob(data, { type: 'image/jpeg' }));
857             linksState.setCachedThumbnail(shareId, linkId, url);
859             const cachedLink = linksState.getLink(shareId, linkId);
860             if (cachedLink) {
861                 const verified = await verifiedPromise;
862                 handleSignatureCheck(shareId, cachedLink.encrypted, 'thumbnail', verified);
863             }
865             return url;
866         };
868         if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
869             downloadInfo = await loadDownloadUrl(link.activeRevision.id);
870         }
872         if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
873             return;
874         }
876         try {
877             return await loadThumbnailUrl(downloadInfo.downloadUrl, downloadInfo.downloadToken);
878         } catch (err) {
879             // Download URL and token can be expired if we used cached version.
880             // We get thumbnail info with the link, but if user don't scroll
881             // to the item before cached version expires, we need to try again
882             // with a loading the new URL and token.
883             if (downloadInfo.isFresh) {
884                 throw err;
885             }
886             downloadInfo = await loadDownloadUrl(link.activeRevision.id);
887             if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
888                 return;
889             }
890             return loadThumbnailUrl(downloadInfo.downloadUrl, downloadInfo.downloadToken);
891         }
892     };
894     const setSignatureIssues = async (
895         abortSignal: AbortSignal,
896         shareId: string,
897         linkId: string,
898         signatureIssues: SignatureIssues
899     ) => {
900         const link = await getEncryptedLink(abortSignal, shareId, linkId);
901         linksState.setLinks(shareId, [
902             {
903                 encrypted: {
904                     ...link,
905                     signatureIssues,
906                 },
907             },
908         ]);
910         const locations = Object.keys(signatureIssues) as SignatureIssueLocation[];
911         if (locations.length) {
912             // Signature issues can have multiple sources.
913             // If the problem comes from NodeKey, its more important,
914             // as that is more serious bug than when it comes from
915             // user key.
916             const hasSignatureIssueForNodeKey = (
917                 ['hash', 'contentKeyPacket', 'blocks', 'thumbnail', 'manifest'] as SignatureIssueLocation[]
918             ).some((location) => locations.includes(location));
919             // But if its not due to NodeKey, we take random source
920             // of issue, as it doesnt matter that much.
921             const location = hasSignatureIssueForNodeKey ? 'hash' : locations[0];
923             reportSignatureError(shareId, link, location);
924         }
925     };
927     return {
928         getLinkPassphraseAndSessionKey,
929         getLinkPrivateKey,
930         getLinkSessionKey,
931         getLinkHashKey,
932         decryptLink,
933         getLink,
934         loadFreshLink,
935         loadLinkThumbnail,
936         setSignatureIssues,
937     };