diff --git a/lib/utils/services-utils.js b/lib/utils/services-utils.js --- a/lib/utils/services-utils.js +++ b/lib/utils/services-utils.js @@ -21,8 +21,18 @@ return `Bearer ${base64EncodedPayload}`; } +function createDefaultHTTPRequestHeaders(authMetadata: AuthMetadata): { + [string]: string, +} { + const authorization = createHTTPAuthorizationHeader(authMetadata); + return { + Authorization: authorization, + }; +} + export { handleHTTPResponseError, usingCommServicesAccessToken, createHTTPAuthorizationHeader, + createDefaultHTTPRequestHeaders, }; diff --git a/native/media/encrypted-image.react.js b/native/media/encrypted-image.react.js --- a/native/media/encrypted-image.react.js +++ b/native/media/encrypted-image.react.js @@ -7,7 +7,7 @@ import { connectionSelector } from 'lib/selectors/keyserver-selectors.js'; import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; -import { decryptBase64, fetchAndDecryptMedia } from './encryption-utils.js'; +import { decryptBase64, useFetchAndDecryptMedia } from './encryption-utils.js'; import LoadableImage from './loadable-image.react.js'; import { useSelector } from '../redux/redux-utils.js'; import type { ImageSource } from '../types/react-native.js'; @@ -34,6 +34,8 @@ thumbHash: encryptedThumbHash, } = props; + const fetchAndDecryptMedia = useFetchAndDecryptMedia(); + const mediaCache = React.useContext(MediaCacheContext); const [source, setSource] = React.useState(null); @@ -96,7 +98,7 @@ return () => { isMounted = false; }; - }, [attempt, blobURI, encryptionKey, mediaCache]); + }, [attempt, blobURI, encryptionKey, mediaCache, fetchAndDecryptMedia]); const onLoad = React.useCallback(() => { onLoadProp && onLoadProp(blobURI); 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 @@ -1,6 +1,7 @@ // @flow import invariant from 'invariant'; +import * as React from 'react'; import { uintArrayToHexString, hexToUintArray } from 'lib/media/data-utils.js'; import { @@ -10,14 +11,18 @@ readableFilename, pathFromURI, } from 'lib/media/file-utils.js'; +import type { AuthMetadata } from 'lib/shared/identity-client-context.js'; +import { IdentityClientContext } from 'lib/shared/identity-client-context.js'; import type { MediaMissionFailure, MediaMissionStep, DecryptFileMediaMissionStep, EncryptFileMediaMissionStep, } from 'lib/types/media-types.js'; +import { isBlobServiceURI } from 'lib/utils/blob-service.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { pad, unpad, calculatePaddedLength } from 'lib/utils/pkcs7-padding.js'; +import { createDefaultHTTPRequestHeaders } from 'lib/utils/services-utils.js'; import { temporaryDirectoryPath } from './file-utils.js'; import { getFetchableURI } from './identifier-utils.js'; @@ -245,26 +250,36 @@ }; } +type FetchAndDecryptMediaOptions = { + +destination: 'file' | 'data_uri', + +destinationDirectory?: string, +}; + +type FetchAndDecryptMediaOutput = { + steps: $ReadOnlyArray, + result: MediaMissionFailure | { success: true, uri: string }, +}; + async function fetchAndDecryptMedia( blobURI: string, encryptionKey: string, - options: { - +destination: 'file' | 'data_uri', - +destinationDirectory?: string, - }, -): Promise<{ - steps: $ReadOnlyArray, - result: MediaMissionFailure | { success: true, uri: string }, -}> { + authMetadata: AuthMetadata, + options: FetchAndDecryptMediaOptions, +): Promise { let success = true, exceptionMessage; const steps: DecryptFileMediaMissionStep[] = []; // Step 1. Fetch the file and convert it to a Uint8Array + let headers; + if (isBlobServiceURI(blobURI)) { + headers = createDefaultHTTPRequestHeaders(authMetadata); + } + const fetchStartTime = Date.now(); let data; try { - const response = await fetch(getFetchableURI(blobURI)); + const response = await fetch(getFetchableURI(blobURI), { headers }); if (!response.ok) { throw new Error(`HTTP error ${response.status}: ${response.statusText}`); } @@ -385,6 +400,29 @@ }; } +function useFetchAndDecryptMedia(): ( + blobURI: string, + encryptionKey: string, + options: FetchAndDecryptMediaOptions, +) => Promise { + const identityContext = React.useContext(IdentityClientContext); + invariant(identityContext, 'Identity context should be set'); + const { getAuthMetadata } = identityContext; + + return React.useCallback( + async (blobURI, encryptionKey, options) => { + const authMetadata = await getAuthMetadata(); + return fetchAndDecryptMedia( + blobURI, + encryptionKey, + authMetadata, + options, + ); + }, + [getAuthMetadata], + ); +} + function encryptBase64( base64: string, keyBytes?: Uint8Array, @@ -407,4 +445,10 @@ return commUtilsModule.base64EncodeBuffer(decryptedData.buffer); } -export { encryptMedia, fetchAndDecryptMedia, encryptBase64, decryptBase64 }; +export { + encryptMedia, + fetchAndDecryptMedia, + useFetchAndDecryptMedia, + encryptBase64, + decryptBase64, +}; 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 @@ -42,6 +42,7 @@ type FetchFileInfoResult, } from './file-utils.js'; import { getMediaLibraryIdentifier } from './identifier-utils.js'; +import { commCoreModule } from '../native-modules.js'; import { displayActionResultModal } from '../navigation/action-result-modal.js'; import { requestAndroidPermission } from '../utils/android-permissions.js'; @@ -377,8 +378,10 @@ ): Promise { const steps: Array = []; if (encryptionKey) { + const authMetadata = await commCoreModule.getCommServicesAuthMetadata(); + const { steps: decryptionSteps, result: decryptionResult } = - await fetchAndDecryptMedia(inputURI, encryptionKey, { + await fetchAndDecryptMedia(inputURI, encryptionKey, authMetadata, { destination: 'file', destinationDirectory: directory, }); diff --git a/native/media/video-playback-modal.react.js b/native/media/video-playback-modal.react.js --- a/native/media/video-playback-modal.react.js +++ b/native/media/video-playback-modal.react.js @@ -19,7 +19,7 @@ import { useIsAppBackgroundedOrInactive } from 'lib/shared/lifecycle-utils.js'; import type { MediaInfo } from 'lib/types/media-types.js'; -import { fetchAndDecryptMedia } from './encryption-utils.js'; +import { useFetchAndDecryptMedia } from './encryption-utils.js'; import { formatDuration } from './video-utils.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; @@ -98,6 +98,7 @@ ); const mediaCache = React.useContext(MediaCacheContext); + const fetchAndDecryptMedia = useFetchAndDecryptMedia(); React.useEffect(() => { // skip for unencrypted videos @@ -142,7 +143,7 @@ filesystem.unlink(uriToDispose); } }; - }, [blobURI, encryptionKey, mediaCache]); + }, [blobURI, encryptionKey, mediaCache, fetchAndDecryptMedia]); const closeButtonX = useValue(-1); const closeButtonY = useValue(-1);