import { NitroModules } from 'react-native-nitro-modules' import { bufferToInt8Array, int8ArrayToBuffer } from './utils/HybridInterop' import { hybridStrongRandom } from './StrongRandomService' import { type Hmac, HmacAlgorithmHybridSpec } from './specs/Hmac.nitro' export enum HmacAlgorithm { HmacSha512 = 'HmacSha512', HmacSha256 = 'HmacSha256', } export interface XHmacKey { readonly hmacKey: Int8Array readonly keySize: number readonly algorithm: HmacAlgorithm } export interface XHmacService { generateKey(algorithm: HmacAlgorithm, keySize?: number): Promise exportKey(key: XHmacKey): Promise loadKey(algorithm: HmacAlgorithm, bytes: Int8Array): Promise sign(data: Int8Array, key: XHmacKey): Promise verify(signature: Int8Array, data: Int8Array, key: XHmacKey): Promise } const hmacSha256KeySizeInfo = { minKeySize: 32, recommendedKeySize: 64 } const hmacSha512KeySizeInfo = { minKeySize: 64, recommendedKeySize: 128 } function validateAndGetAlgorithmKeySizeInfo(algorithm: HmacAlgorithm): { minKeySize: number recommendedKeySize: number } { switch (algorithm) { case HmacAlgorithm.HmacSha256: return hmacSha256KeySizeInfo case HmacAlgorithm.HmacSha512: return hmacSha512KeySizeInfo default: throw new Error(`Unsupported/invalid HMAC algorithm ${algorithm}`) } } function validateAndGetHybridAlgorithmSpec( algorithm: XHmacKey ): HmacAlgorithmHybridSpec { switch (algorithm.algorithm) { case HmacAlgorithm.HmacSha256: return HmacAlgorithmHybridSpec.HmacSha256 case HmacAlgorithm.HmacSha512: return HmacAlgorithmHybridSpec.HmacSha512 default: throw new Error(`Unsupported/invalid HMAC algorithm ${algorithm} in key`) } } const hybridHmac = NitroModules.createHybridObject('Hmac') export const HmacService: XHmacService = { exportKey(key: XHmacKey): Promise { return Promise.resolve(new Int8Array(key.hmacKey)) }, generateKey(algorithm: HmacAlgorithm, keySize?: number): Promise { const sizeInfo = validateAndGetAlgorithmKeySizeInfo(algorithm) if (keySize != undefined && keySize < sizeInfo.minKeySize) throw new Error( `Invalid key size for algorithm ${algorithm}, must be at least ${sizeInfo.minKeySize} bytes long, recommended ${sizeInfo.recommendedKeySize}` ) const newKeySize = keySize ?? sizeInfo.recommendedKeySize const key = new Int8Array(newKeySize) hybridStrongRandom.fillPrivate(key.buffer) return Promise.resolve({ algorithm: algorithm, keySize: newKeySize, hmacKey: key, }) }, loadKey(algorithm: HmacAlgorithm, bytes: Int8Array): Promise { const sizeInfo = validateAndGetAlgorithmKeySizeInfo(algorithm) if (bytes.byteLength < sizeInfo.minKeySize) throw new Error( `Invalid key size for algorithm ${algorithm}, must be at least ${sizeInfo.minKeySize} bytes long, recommended ${sizeInfo.recommendedKeySize}` ) return Promise.resolve({ algorithm: algorithm, keySize: bytes.byteLength, hmacKey: new Int8Array(bytes), }) }, async sign(data: Int8Array, key: XHmacKey): Promise { const res = await hybridHmac.sign( validateAndGetHybridAlgorithmSpec(key), int8ArrayToBuffer(data), int8ArrayToBuffer(key.hmacKey) ) return bufferToInt8Array(res) }, async verify( signature: Int8Array, data: Int8Array, key: XHmacKey ): Promise { const recomputedSignature = new Int8Array( await hybridHmac.sign( validateAndGetHybridAlgorithmSpec(key), int8ArrayToBuffer(data), int8ArrayToBuffer(key.hmacKey) ) ) // Not reusing HmacService.sign because we always want to wrap in this case, never copy (choice of wrap/copy in `bufferToInt8Array` implementation may change in future) if (signature.byteLength !== recomputedSignature.byteLength) return false for (let i = 0; i < signature.length; i++) { if (signature[i] !== recomputedSignature[i]) return false } return true }, }