diff --git a/lib/types/media-types.js b/lib/types/media-types.js --- a/lib/types/media-types.js +++ b/lib/types/media-types.js @@ -678,6 +678,7 @@ | { +success: false, +reason: 'digest_failed' } | { +success: false, +reason: 'thumbhash_failed' } | { +success: false, +reason: 'preload_image_failed' } + | { +success: false, +reason: 'invalid_csat' } | DecryptionFailure; export type DecryptionFailure = { diff --git a/native/media/encryption-utils.js b/native/media/encryption-utils.js --- a/native/media/encryption-utils.js +++ b/native/media/encryption-utils.js @@ -3,6 +3,7 @@ import invariant from 'invariant'; import * as React from 'react'; +import { useInvalidCSATLogOut } from 'lib/actions/user-actions.js'; import { uintArrayToHexString, hexToUintArray } from 'lib/media/data-utils.js'; import { replaceExtension, @@ -25,6 +26,7 @@ import { createDefaultHTTPRequestHeaders, usingCommServicesAccessToken, + httpResponseIsInvalidCSAT, } from 'lib/utils/services-utils.js'; import { temporaryDirectoryPath } from './file-utils.js'; @@ -274,8 +276,10 @@ const steps: DecryptFileMediaMissionStep[] = []; // Step 1. Fetch the file and convert it to a Uint8Array + const isBlobServiceHosted = isBlobServiceURI(blobURI); + let headers; - if (isBlobServiceURI(blobURI) && authMetadata) { + if (isBlobServiceHosted && authMetadata) { headers = createDefaultHTTPRequestHeaders(authMetadata); } @@ -284,6 +288,9 @@ try { const response = await fetch(getFetchableURI(blobURI), { headers }); if (!response.ok) { + if (isBlobServiceHosted && httpResponseIsInvalidCSAT(response)) { + return { steps, result: { success: false, reason: 'invalid_csat' } }; + } throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } const blob = await response.blob(); @@ -412,6 +419,8 @@ invariant(identityContext, 'Identity context should be set'); const { getAuthMetadata } = identityContext; + const invalidTokenLogOut = useInvalidCSATLogOut(); + return React.useCallback( async (blobURI, encryptionKey, options) => { let authMetadata; @@ -422,14 +431,19 @@ console.warn('Failed to get auth metadata:', err); } } - return fetchAndDecryptMedia( + const output = await fetchAndDecryptMedia( blobURI, encryptionKey, authMetadata, options, ); + + if (!output.result.success && output.result.reason === 'invalid_csat') { + void invalidTokenLogOut(); + } + return output; }, - [getAuthMetadata], + [getAuthMetadata, invalidTokenLogOut], ); } diff --git a/native/media/save-media.js b/native/media/save-media.js --- a/native/media/save-media.js +++ b/native/media/save-media.js @@ -7,6 +7,7 @@ import filesystem from 'react-native-fs'; import { queueReportsActionType } from 'lib/actions/report-actions.js'; +import { useInvalidCSATLogOut } from 'lib/actions/user-actions.js'; import { readableFilename, pathFromURI } from 'lib/media/file-utils.js'; import { isLocalUploadID } from 'lib/media/media-utils.js'; import type { @@ -58,6 +59,7 @@ function useIntentionalSaveMedia(): IntentionalSaveMedia { const dispatch = useDispatch(); const mediaReportsEnabled = useIsReportEnabled('mediaReports'); + const invalidTokenLogOut = useInvalidCSATLogOut(); return React.useCallback( async ( mediaInfo: MediaInfo, @@ -87,6 +89,9 @@ let message; if (result.success) { message = 'saved!'; + } else if (result.reason === 'invalid_csat') { + void invalidTokenLogOut(); + return; } else if (result.reason === 'save_unsupported') { const os: string = Platform.select({ ios: 'iOS', @@ -134,7 +139,7 @@ payload: { reports: [report] }, }); }, - [dispatch, mediaReportsEnabled], + [dispatch, mediaReportsEnabled, invalidTokenLogOut], ); } diff --git a/web/media/encryption-utils.js b/web/media/encryption-utils.js --- a/web/media/encryption-utils.js +++ b/web/media/encryption-utils.js @@ -4,6 +4,7 @@ import * as React from 'react'; import { thumbHashToDataURL } from 'thumbhash'; +import { useInvalidCSATLogOut } from 'lib/actions/user-actions.js'; import * as AES from 'lib/media/aes-crypto-utils-common.js'; import { hexToUintArray, uintArrayToHexString } from 'lib/media/data-utils.js'; import { fileInfoFromData } from 'lib/media/file-utils.js'; @@ -17,7 +18,10 @@ import { isBlobServiceURI } from 'lib/utils/blob-service.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { calculatePaddedLength, pad, unpad } from 'lib/utils/pkcs7-padding.js'; -import { createDefaultHTTPRequestHeaders } from 'lib/utils/services-utils.js'; +import { + createDefaultHTTPRequestHeaders, + httpResponseIsInvalidCSAT, +} from 'lib/utils/services-utils.js'; import { base64DecodeBuffer } from '../utils/base64-utils.js'; @@ -157,8 +161,10 @@ const steps: DecryptFileStep[] = []; // Step 1 - Fetch the encrypted media and convert it to a Uint8Array + const isBlobServiceHosted = isBlobServiceURI(blobURI); + let headers; - if (isBlobServiceURI(blobURI)) { + if (isBlobServiceHosted) { headers = createDefaultHTTPRequestHeaders(authMetadata); } @@ -168,6 +174,9 @@ try { const response = await fetch(url, { headers }); if (!response.ok) { + if (isBlobServiceHosted && httpResponseIsInvalidCSAT(response)) { + return { steps, result: { success: false, reason: 'invalid_csat' } }; + } throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } const buffer = await response.arrayBuffer(); @@ -259,12 +268,23 @@ invariant(identityContext, 'Identity context should be set'); const { getAuthMetadata } = identityContext; + const invalidTokenLogOut = useInvalidCSATLogOut(); + return React.useCallback( async (blobURI, encryptionKey) => { const authMetadata = await getAuthMetadata(); - return fetchAndDecryptMedia(blobURI, encryptionKey, authMetadata); + const output = await fetchAndDecryptMedia( + blobURI, + encryptionKey, + authMetadata, + ); + + if (!output.result.success && output.result.reason === 'invalid_csat') { + void invalidTokenLogOut(); + } + return output; }, - [getAuthMetadata], + [getAuthMetadata, invalidTokenLogOut], ); }