Changeset View
Changeset View
Standalone View
Standalone View
native/media/encryption-utils.js
// @flow | // @flow | ||||
import invariant from 'invariant'; | |||||
import filesystem from 'react-native-fs'; | import filesystem from 'react-native-fs'; | ||||
import { | import { | ||||
base64FromIntArray, | base64FromIntArray, | ||||
uintArrayToHexString, | uintArrayToHexString, | ||||
hexToUintArray, | hexToUintArray, | ||||
} from 'lib/media/data-utils.js'; | } from 'lib/media/data-utils.js'; | ||||
import { | import { | ||||
replaceExtension, | replaceExtension, | ||||
fileInfoFromData, | fileInfoFromData, | ||||
readableFilename, | readableFilename, | ||||
pathFromURI, | |||||
} from 'lib/media/file-utils.js'; | } from 'lib/media/file-utils.js'; | ||||
import type { | import type { | ||||
MediaMissionFailure, | MediaMissionFailure, | ||||
MediaMissionStep, | |||||
EncryptFileMediaMissionStep, | EncryptFileMediaMissionStep, | ||||
} from 'lib/types/media-types.js'; | } from 'lib/types/media-types.js'; | ||||
import { getMessageForException } from 'lib/utils/errors.js'; | import { getMessageForException } from 'lib/utils/errors.js'; | ||||
import { pad, unpad, calculatePaddedLength } from 'lib/utils/pkcs7-padding.js'; | import { pad, unpad, calculatePaddedLength } from 'lib/utils/pkcs7-padding.js'; | ||||
import { temporaryDirectoryPath } from './file-utils.js'; | import { temporaryDirectoryPath } from './file-utils.js'; | ||||
import { getFetchableURI } from './identifier-utils.js'; | import { getFetchableURI } from './identifier-utils.js'; | ||||
import type { MediaResult } from './media-utils.js'; | |||||
import * as AES from '../utils/aes-crypto-module.js'; | import * as AES from '../utils/aes-crypto-module.js'; | ||||
const PADDING_THRESHOLD = 5000000; // we don't pad files larger than this | const PADDING_THRESHOLD = 5000000; // we don't pad files larger than this | ||||
type EncryptedFileResult = { | type EncryptedFileResult = { | ||||
+success: true, | +success: true, | ||||
+uri: string, | +uri: string, | ||||
+encryptionKey: string, | +encryptionKey: string, | ||||
▲ Show 20 Lines • Show All 98 Lines • ▼ Show 20 Lines | return { | ||||
result: { | result: { | ||||
success: true, | success: true, | ||||
uri: destination, | uri: destination, | ||||
encryptionKey: uintArrayToHexString(key), | encryptionKey: uintArrayToHexString(key), | ||||
}, | }, | ||||
}; | }; | ||||
} | } | ||||
/** | |||||
* Encrypts a single photo or video. Replaces the uploadURI with the encrypted | |||||
* file URI. Attaches `encryptionKey` to the result. Changes the mediaType to | |||||
* `encrypted_photo` or `encrypted_video`. | |||||
* | |||||
* @param preprocessedMedia - Result of `processMedia()` call | |||||
* @returns a `preprocessedMedia` param, but with encryption applied | |||||
*/ | |||||
async function encryptMedia(preprocessedMedia: MediaResult): Promise<{ | |||||
result: MediaResult | MediaMissionFailure, | |||||
steps: $ReadOnlyArray<MediaMissionStep>, | |||||
}> { | |||||
invariant(preprocessedMedia.success, 'encryptMedia called on failure result'); | |||||
invariant( | |||||
preprocessedMedia.mediaType === 'photo' || | |||||
preprocessedMedia.mediaType === 'video', | |||||
'encryptMedia should only be called on unencrypted photos and videos', | |||||
); | |||||
const { uploadURI } = preprocessedMedia; | |||||
const steps = []; | |||||
// Encrypt the media file | |||||
const { steps: encryptionSteps, result: encryptionResult } = | |||||
await encryptFile(uploadURI); | |||||
steps.push(...encryptionSteps); | |||||
if (!encryptionResult.success) { | |||||
return { steps, result: encryptionResult }; | |||||
} | |||||
if (preprocessedMedia.mediaType === 'photo') { | |||||
return { | |||||
steps, | |||||
result: { | |||||
...preprocessedMedia, | |||||
mediaType: 'encrypted_photo', | |||||
uploadURI: encryptionResult.uri, | |||||
encryptionKey: encryptionResult.encryptionKey, | |||||
shouldDisposePath: pathFromURI(encryptionResult.uri), | |||||
}, | |||||
}; | |||||
} | |||||
// For videos, we also need to encrypt the thumbnail | |||||
const { steps: thumbnailEncryptionSteps, result: thumbnailEncryptionResult } = | |||||
await encryptFile(preprocessedMedia.uploadThumbnailURI); | |||||
steps.push(...thumbnailEncryptionSteps); | |||||
if (!thumbnailEncryptionResult.success) { | |||||
return { steps, result: thumbnailEncryptionResult }; | |||||
} | |||||
return { | |||||
steps, | |||||
result: { | |||||
...preprocessedMedia, | |||||
mediaType: 'encrypted_video', | |||||
uploadURI: encryptionResult.uri, | |||||
encryptionKey: encryptionResult.encryptionKey, | |||||
uploadThumbnailURI: thumbnailEncryptionResult.uri, | |||||
thumbnailEncryptionKey: thumbnailEncryptionResult.encryptionKey, | |||||
shouldDisposePath: pathFromURI(encryptionResult.uri), | |||||
}, | |||||
}; | |||||
} | |||||
type DecryptFileStep = | type DecryptFileStep = | ||||
| { | | { | ||||
+step: 'fetch_file', | +step: 'fetch_file', | ||||
+file: string, | +file: string, | ||||
+time: number, | +time: number, | ||||
+success: boolean, | +success: boolean, | ||||
+exceptionMessage: ?string, | +exceptionMessage: ?string, | ||||
} | } | ||||
▲ Show 20 Lines • Show All 158 Lines • ▼ Show 20 Lines | ): Promise<{ | ||||
} | } | ||||
return { | return { | ||||
steps, | steps, | ||||
result: { success: true, uri }, | result: { success: true, uri }, | ||||
}; | }; | ||||
} | } | ||||
export { encryptFile, decryptMedia }; | export { encryptMedia, decryptMedia }; |