diff --git a/native/expo-modules/comm-expo-package/expo-module.config.json b/native/expo-modules/comm-expo-package/expo-module.config.json index 7188031fe..a22b29a75 100644 --- a/native/expo-modules/comm-expo-package/expo-module.config.json +++ b/native/expo-modules/comm-expo-package/expo-module.config.json @@ -1,13 +1,13 @@ { "platforms": ["ios", "android"], "ios": { - "modules": ["AESCryptoModule", "ThumbhashModule"] + "modules": ["AESCryptoModule", "BlobUtilsModule", "ThumbhashModule"] }, "android": { "modules": [ "app.comm.android.aescrypto.AESCryptoModule", "app.comm.android.lifecycle.AndroidLifecycleModule", "app.comm.android.thumbhash.ThumbhashModule" ] } } diff --git a/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift b/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift index 6aea340b3..66d568ee2 100644 --- a/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift +++ b/native/expo-modules/comm-expo-package/ios/AESCryptoModule.swift @@ -1,100 +1,87 @@ import ExpoModulesCore import CryptoKit private let KEY_SIZE = 32 // bytes private let IV_LENGTH = 12 // bytes, IV - unique Initialization Vector (nonce) private let TAG_LENGTH = 16 // bytes - GCM auth tag public class AESCryptoModule: Module { public func definition() -> ModuleDefinition { Name("AESCrypto") Function("generateKey", generateKey) Function("encrypt", encrypt) Function("decrypt", decrypt) } } // MARK: - Function implementations private func generateKey(destination: Uint8Array) throws { guard destination.byteLength == KEY_SIZE else { throw InvalidKeyLengthException() } let key = SymmetricKey(size: .bits256) key.withUnsafeBytes { bytes in let _ = bytes.copyBytes(to: destination.rawBufferPtr()) } } private func encrypt(rawKey: Uint8Array, plaintext: Uint8Array, destination: Uint8Array) throws { guard destination.byteLength == plaintext.byteLength + IV_LENGTH + TAG_LENGTH else { throw InvalidDataLengthException() } let key = SymmetricKey(data: rawKey.data()) let iv = AES.GCM.Nonce() let encryptionResult = try AES.GCM.seal(plaintext.data(), using: key, nonce: iv) // 'combined' returns concatenated: iv || ciphertext || tag guard let sealedData = encryptionResult.combined else { // this happens only if Nonce/IV != 12 bytes long throw EncryptionFailedException("Incorrect AES configuration") } guard sealedData.count == destination.byteLength else { throw EncryptionFailedException("Encrypted data has unexpected length") } sealedData.copyBytes(to: destination.rawBufferPtr()) } private func decrypt(rawKey: Uint8Array, sealedData: Uint8Array, destination: Uint8Array) throws { guard destination.byteLength == sealedData.byteLength - IV_LENGTH - TAG_LENGTH else { throw InvalidDataLengthException() } let key = SymmetricKey(data: rawKey.data()) let sealedBox = try AES.GCM.SealedBox(combined: sealedData.data()) let plaintext = try AES.GCM.open(sealedBox, using: key) plaintext.copyBytes(to: destination.rawBufferPtr()) } -// MARK: - Utilities - -extension TypedArray { - func data() -> Data { - Data(bytes: self.rawPointer, count: self.byteLength) - } - - func rawBufferPtr() -> UnsafeMutableRawBufferPointer { - UnsafeMutableRawBufferPointer(start: self.rawPointer, - count: self.byteLength) - } -} - // MARK: - Exception definitions private class InvalidKeyLengthException: Exception { override var reason: String { "The AES key has invalid length" } } private class InvalidDataLengthException: Exception { override var reason: String { "Source or destination array has invalid length" } } private class EncryptionFailedException: GenericException { override var reason: String { "Failed to encrypt data: \(param)" } } diff --git a/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift b/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift new file mode 100644 index 000000000..488d92dd4 --- /dev/null +++ b/native/expo-modules/comm-expo-package/ios/BlobUtilsModule.swift @@ -0,0 +1,70 @@ +import ExpoModulesCore + +// This type corresponds to the BlobData interface in react-native +struct BlobMetadata: Record { + @Field var blobId: String + @Field var size: Int + @Field var offset: Int +} + +public class BlobUtilsModule: Module { + public func definition() -> ModuleDefinition { + Name("BlobUtils") + + Function("copyBlobToTypedArray") { + (blob: BlobMetadata, destination: Uint8Array) throws in + let blobManager = try self.getReactBlobManager() + guard let blobData = blobManager.resolve(blob.blobId, + offset: blob.offset, + size: blob.size) else { + throw NoSuchBlobException(blob.blobId) + } + blobData.copyBytes(to: destination.rawBufferPtr()) + } + + Function("blobFromTypedArray") { (source: TypedArray) throws -> String in + let blobManager = try self.getReactBlobManager() + guard let blobID = blobManager.store(source.data()) else { + throw BlobCreationFailedException() + } + return blobID + } + } + + private func getReactBlobManager() throws -> RCTBlobManager { + guard let bridge = self.appContext?.reactBridge else { + throw BridgeNotFoundException() + } + guard let blobManager = bridge.module(for: RCTBlobManager.self) + as? RCTBlobManager else { + throw BlobManagerNotFoundException() + } + return blobManager + } +} + +// MARK: Exception definitions + +class BridgeNotFoundException: Exception { + override var reason: String { + "React bridge is null" + } +} + +class BlobManagerNotFoundException: Exception { + override var reason: String { + "Module RCTBlobManager not found" + } +} + +class NoSuchBlobException: GenericException { + override var reason: String { + "No blob data found for blob id=\(param)" + } +} + +class BlobCreationFailedException: Exception { + override var reason: String { + "Failed to store blob" + } +} diff --git a/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift b/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift new file mode 100644 index 000000000..b1f8926bc --- /dev/null +++ b/native/expo-modules/comm-expo-package/ios/TypedArray+data.swift @@ -0,0 +1,12 @@ +import ExpoModulesCore + +extension TypedArray { + func data() -> Data { + Data(bytes: self.rawPointer, count: self.byteLength) + } + + func rawBufferPtr() -> UnsafeMutableRawBufferPointer { + UnsafeMutableRawBufferPointer(start: self.rawPointer, + count: self.byteLength) + } +} diff --git a/native/utils/blob-utils-module.js b/native/utils/blob-utils-module.js new file mode 100644 index 000000000..53eda1371 --- /dev/null +++ b/native/utils/blob-utils-module.js @@ -0,0 +1,44 @@ +// @flow + +import { requireNativeModule } from 'expo-modules-core'; +import RNBlob from 'react-native/Libraries/Blob/Blob.js'; +import BlobManager from 'react-native/Libraries/Blob/BlobManager.js'; +import type { BlobData } from 'react-native/Libraries/Blob/BlobTypes.js'; + +const BlobUtilsModule: { + +copyBlobToTypedArray: (blob: BlobData, destination: Uint8Array) => void, + +blobFromTypedArray: (data: $TypedArray) => string, +} = requireNativeModule('BlobUtils'); + +function arrayBufferFromBlob(blob: Blob): ArrayBuffer { + // $FlowFixMe: react-native Blob type is incompatible with global Blob type + const rnBlob = (blob: RNBlob); + if (!(rnBlob instanceof RNBlob)) { + throw new Error( + 'Given blob is not a React Native blob. Missing "data" property.', + ); + } + const blobData = rnBlob.data; + + const resultArray = new Uint8Array(blob.size); + BlobUtilsModule.copyBlobToTypedArray(blobData, resultArray); + return resultArray.buffer; +} + +function blobFromArrayBuffer(arrayBuffer: ArrayBuffer, type?: string): Blob { + const typedArray = new Uint8Array(arrayBuffer); + const blobID = BlobUtilsModule.blobFromTypedArray(typedArray); + const reactNativeBlob = BlobManager.createFromOptions({ + blobId: blobID, + offset: 0, + size: arrayBuffer.byteLength, + type: type || '', + lastModified: Date.now(), + }); + + // $FlowFixMe: react-native Blob type is incompatible with global Blob type + // $FlowFixMe: even though they have the same properties + return (reactNativeBlob: Blob); +} + +export { arrayBufferFromBlob, blobFromArrayBuffer };