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';
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';
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,
52 linkId: encryptedLink.linkId,
53 createTime: encryptedLink.createTime,
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,
73 trashed: encryptedLink.trashed,
74 volumeId: encryptedLink.volumeId,
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>(
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.
102 return linkMetaToEncryptedLink(Link, shareId);
112 getDefaultShareAddressEmail,
113 getDirectSharingInfo,
116 CryptoProxy.importPrivateKey
120 export function useLinkInner(
121 fetchLink: (abortSignal: AbortSignal, shareId: string, linkId: string) => Promise<EncryptedLink>,
123 ReturnType<typeof useLinksKeys>,
126 | 'getPassphraseSessionKey'
127 | 'setPassphraseSessionKey'
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'],
142 integrityMetrics: IntegrityMetrics,
143 importPrivateKey: typeof CryptoProxy.importPrivateKey // passed as arg for easier mocking when testing
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
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];
162 return fetchLinkDONOTUSE(abortSignal, shareId, linkId).catch((err) => {
164 [RESPONSE_CODE.NOT_FOUND, RESPONSE_CODE.NOT_ALLOWED, RESPONSE_CODE.INVALID_ID].includes(err?.data?.Code)
166 linkFetchErrors.current[shareId + linkId] = err;
168 delete linkFetchErrors.current[shareId + linkId];
169 }, FAILING_FETCH_BACKOFF_MS);
175 const loadShareTypeString = async (shareId: string): Promise<ShareTypeString> => {
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')
187 const handleDecryptionError = (shareId: string, encryptedLink: EncryptedLink) => {
188 loadShareTypeString(shareId).then((shareType) => {
191 createTime: encryptedLink.createTime,
193 integrityMetrics.nodeDecryptionError(encryptedLink.linkId, shareType, options);
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
200 // - shareId: D4TVgdFKidFgQWd5IeXYyegjDNV9KWF1HDwjxZlesUo-Wc2NTL8mUQc6IlYwowznc5vHQkTL4iUbn6K0CorrjQ==
201 // - token: 2NR85F8NSC
202 if (tokenIsValid(shareId)) {
205 loadShareTypeString(shareId).then(async (shareType) => {
206 const email = await getDefaultShareAddressEmail();
207 const verificationKey = {
208 passphrase: 'SignatureEmail',
210 name: 'NameSignatureEmail',
211 xattrs: 'SignatureEmail',
212 contentKeyPacket: 'NodeKey',
214 thumbnail: 'NodeKey',
216 }[location] as VerificationKey;
220 createTime: encryptedLink.createTime,
221 addressMatchingDefaultShare: encryptedLink.signatureAddress === email,
223 integrityMetrics.signatureVerificationError(encryptedLink.linkId, shareType, verificationKey, options);
227 const handleSignatureCheck = (
229 encryptedLink: EncryptedLink,
230 location: SignatureIssueLocation,
231 verified: VERIFICATION_STATUS
233 if (verified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
234 const signatureIssues: SignatureIssues = {};
235 signatureIssues[location] = verified;
236 linksState.setLinks(shareId, [
245 reportSignatureError(shareId, encryptedLink, location);
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.
254 const debouncedFunctionDecorator = <T>(
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);
263 [cacheKey, shareId, linkId],
270 const getEncryptedLink = debouncedFunctionDecorator(
272 async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<EncryptedLink> => {
273 const cachedLink = linksState.getLink(shareId, linkId);
275 return cachedLink.encrypted;
277 const link = await fetchLink(abortSignal, shareId, linkId);
278 linksState.setLinks(shareId, [{ encrypted: link }]);
284 * getLinkPassphraseAndSessionKey returns the passphrase with session key
285 * used for locking the private key.
287 const getLinkPassphraseAndSessionKey = debouncedFunctionDecorator(
288 'getLinkPassphraseAndSessionKey',
290 abortSignal: AbortSignal,
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 };
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) : [],
312 // Fallback to parent NodeKey in case if we don't have addressPublicKey (Anonymous upload)
313 const publicKeys = encryptedLink.signatureAddress ? addressPublicKey : [parentPrivateKey];
316 sessionKey: passphraseSessionKey,
318 } = await decryptPassphrase({
319 armoredPassphrase: encryptedLink.nodePassphrase,
320 armoredSignature: encryptedLink.nodePassphraseSignature,
321 privateKeys: [parentPrivateKey],
323 validateSignature: false,
326 handleSignatureCheck(shareId, encryptedLink, 'passphrase', verified);
328 linksKeys.setPassphrase(shareId, linkId, decryptedPassphrase);
329 linksKeys.setPassphraseSessionKey(shareId, linkId, passphraseSessionKey);
332 passphrase: decryptedPassphrase,
333 passphraseSessionKey,
337 throw new EnrichedError('Failed to decrypt link passphrase', {
342 extra: { e, crypto: true },
349 * getLinkPrivateKey returns the private key used for link meta data encryption.
351 const getLinkPrivateKey = debouncedFunctionDecorator(
353 async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<PrivateKeyReference> => {
354 let privateKey = linksKeys.getPrivateKey(shareId, linkId);
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));
364 privateKey = await importPrivateKey({ armoredKey: encryptedLink.nodeKey, passphrase });
366 throw new EnrichedError('Failed to import link private key', {
371 extra: { e, crypto: true },
375 linksKeys.setPrivateKey(shareId, linkId, privateKey);
381 * getLinkSessionKey returns the session key used for block encryption.
383 const getLinkSessionKey = debouncedFunctionDecorator(
385 async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<SessionKey> => {
386 let sessionKey = linksKeys.getSessionKey(shareId, linkId);
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');
397 const privateKey = await getLinkPrivateKey(abortSignal, shareId, linkId);
398 const blockKeys = base64StringToUint8Array(encryptedLink.contentKeyPacket);
401 sessionKey = await getDecryptedSessionKey({
403 privateKeys: privateKey,
406 throw new EnrichedError('Failed to decrypt link session key', {
411 extra: { e, crypto: true },
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,
422 // iOS signed content key instead of session key in the past.
423 // Therefore we need to check that as well until we migrate
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,
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);
440 linksKeys.setSessionKey(shareId, linkId, sessionKey);
446 * getLinkHashKey returns the hash key used for checking name collisions.
448 const getLinkHashKey = debouncedFunctionDecorator(
450 async (abortSignal: AbortSignal, shareId: string, linkId: string): Promise<Uint8Array> => {
451 let cachedHashKey = linksKeys.getHashKey(shareId, linkId);
453 return cachedHashKey;
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');
462 const [privateKey, addressPrivateKey] = await Promise.all([
463 getLinkPrivateKey(abortSignal, shareId, linkId),
464 getVerificationKey(encryptedLink.signatureAddress),
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
474 const publicKey = [privateKey, ...addressPrivateKey];
477 const { data: hashKey, verified } = await decryptSigned({
478 armoredMessage: encryptedLink.nodeHashKey,
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)))
490 handleSignatureCheck(shareId, encryptedLink, 'hash', verified);
493 linksKeys.setHashKey(shareId, linkId, hashKey);
496 throw new EnrichedError('Failed to decrypt link hash key', {
501 extra: { e, crypto: true },
507 const getLinkRevision = async (
508 abortSignal: AbortSignal,
509 { shareId, linkId, revisionId }: { shareId: string; linkId: string; revisionId: string }
511 const { Revision } = await debouncedRequest<DriveFileRevisionResult>(
512 queryFileRevision(shareId, linkId, revisionId),
515 return revisionPayloadToRevision(Revision);
519 * decryptLink decrypts provided `encryptedLink`. The result is not stored
520 * anywhere, only returned back.
522 const decryptLink = async (
523 abortSignal: AbortSignal,
525 encryptedLink: EncryptedLink,
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) => {
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)
542 const { data, verified } = await decryptSigned({
543 armoredMessage: encryptedLink.name,
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.
555 resolve({ name: data, nameVerified: verified });
562 const revision = !!revisionId
563 ? await getLinkRevision(abortSignal, { shareId, linkId: encryptedLink.linkId, revisionId })
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
573 fileModifyTime: encryptedLink.metaDataModifyTime,
574 fileModifyTimeVerified: VERIFICATION_STATUS.SIGNED_AND_VALID,
575 originalSize: undefined,
576 originalDimensions: undefined,
580 : getLinkPrivateKey(abortSignal, shareId, encryptedLink.linkId)
581 .then(async (privateKey) =>
582 decryptExtendedAttributes(
586 signatureAddress ? await getVerificationKey(signatureAddress) : privateKey
589 .then(({ xattrs, verified }) => ({
590 fileModifyTime: xattrs.Common.ModificationTime || encryptedLink.metaDataModifyTime,
591 fileModifyTimeVerified: verified,
592 originalSize: xattrs.Common?.Size,
593 originalDimensions: xattrs.Media
595 width: xattrs.Media.Width,
596 height: xattrs.Media.Height,
599 duration: xattrs.Media?.Duration,
600 digests: xattrs.Common?.Digests
602 sha1: xattrs.Common.Digests.SHA1,
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, '�');
616 // Temp: debugging decryption issues
618 if (isDecryptionErrorDebuggingEnabled) {
621 nameResult.reason instanceof Error
622 ? nameResult.reason.message
623 : nameResult.reason.toString(),
628 linkId: encryptedLink.linkId,
629 revisionId: revisionId || encryptedLink.activeRevision?.id,
631 extra: { e: nameResult.reason },
640 handleDecryptionError(shareId, encryptedLink);
641 return generateCorruptDecryptedLink(encryptedLink, '�');
644 const { nameVerified, name } = nameResult.value;
646 const signatureIssues: SignatureIssues = {};
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)))
653 reportSignatureError(shareId, encryptedLink, 'name');
654 signatureIssues.name = nameVerified;
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);
664 // Temp: debugging decryption issues
666 if (isDecryptionErrorDebuggingEnabled) {
669 xattrResult.reason instanceof Error
670 ? xattrResult.reason.message
671 : xattrResult.reason.toString(),
676 linkId: encryptedLink.linkId,
677 revisionId: revisionId || encryptedLink.activeRevision?.id,
679 extra: { e: xattrResult.reason },
688 handleDecryptionError(shareId, encryptedLink);
689 return generateCorruptDecryptedLink(encryptedLink, name);
691 const { fileModifyTimeVerified, fileModifyTime, originalSize, originalDimensions, digests, duration } =
694 if (fileModifyTimeVerified !== VERIFICATION_STATUS.SIGNED_AND_VALID) {
695 reportSignatureError(shareId, encryptedLink, 'xattrs');
696 signatureIssues.xattrs = fileModifyTimeVerified;
699 // Share will already be in cache due to getSharePrivateKey above
700 const shareResult = !encryptedLink.parentLinkId
701 ? share || (await getShare(abortSignal, shareId))
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
708 !getIsPublicContext() && encryptedLink.sharingDetails && !encryptedLink.parentLinkId
709 ? await getDirectSharingInfo(abortSignal, encryptedLink.sharingDetails?.shareId)
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`;
721 ...(sharingInfo ? { sharedOn: sharingInfo.sharedOn, sharedBy: sharingInfo.sharedBy } : undefined),
722 encryptedName: encryptedLink.name,
724 fileModifyTime: fileModifyTime,
728 signatureIssues: Object.keys(signatureIssues).length > 0 ? signatureIssues : undefined,
732 ['decryptLink', shareId, encryptedLink.linkId],
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.
744 const getLink = debouncedFunctionDecorator(
747 abortSignal: AbortSignal,
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;
757 const encrypted = await getEncryptedLink(abortSignal, shareId, linkId);
760 const decrypted = await decryptLink(abortSignal, shareId, encrypted, undefined, share);
762 linksState.setLinks(shareId, [{ encrypted, decrypted }]);
766 if (isIgnoredError(e)) {
767 return generateCorruptDecryptedLink(encrypted, '�');
770 throw new EnrichedError('Failed to decrypt link', {
775 extra: { e, crypto: true },
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.
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);
792 const decryptedLink =
793 cachedLink && isDecryptedLinkSame(cachedLink.encrypted, encryptedLink)
795 : await decryptLink(abortSignal, shareId, encryptedLink);
797 linksState.setLinks(shareId, [{ encrypted: encryptedLink, decrypted: decryptedLink }]);
799 return linksState.getLink(shareId, linkId)?.decrypted as DecryptedLink;
801 if (isIgnoredError(e)) {
802 return generateCorruptDecryptedLink(encryptedLink, '�');
805 throw new EnrichedError('Failed to decrypt link', {
810 extra: { e, crypto: true },
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
821 const loadLinkThumbnail = async (
822 abortSignal: AbortSignal,
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;
837 downloadUrl: link.activeRevision.thumbnail?.bareUrl,
838 downloadToken: link.activeRevision.thumbnail?.token,
841 const loadDownloadUrl = async (activeRevisionId: string) => {
842 const res = (await debouncedRequest(
843 queryFileRevisionThumbnail(shareId, linkId, activeRevisionId)
844 )) as DriveFileRevisionThumbnailResult;
848 downloadUrl: res.ThumbnailBareURL,
849 downloadToken: res.ThumbnailToken,
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);
861 const verified = await verifiedPromise;
862 handleSignatureCheck(shareId, cachedLink.encrypted, 'thumbnail', verified);
868 if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
869 downloadInfo = await loadDownloadUrl(link.activeRevision.id);
872 if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
877 return await loadThumbnailUrl(downloadInfo.downloadUrl, downloadInfo.downloadToken);
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) {
886 downloadInfo = await loadDownloadUrl(link.activeRevision.id);
887 if (!downloadInfo.downloadUrl || !downloadInfo.downloadToken) {
890 return loadThumbnailUrl(downloadInfo.downloadUrl, downloadInfo.downloadToken);
894 const setSignatureIssues = async (
895 abortSignal: AbortSignal,
898 signatureIssues: SignatureIssues
900 const link = await getEncryptedLink(abortSignal, shareId, linkId);
901 linksState.setLinks(shareId, [
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
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);
928 getLinkPassphraseAndSessionKey,