import { Buffer as CraftzdogBuffer } from '@craftzdog/react-native-buffer'; import { Buffer as SafeBuffer } from 'safe-buffer'; import { NitroModules } from 'react-native-nitro-modules'; import type { Utils } from '../specs/utils.nitro'; import type { ABV, BinaryLikeNode, BufferLike } from './types'; import { Platform } from 'react-native'; type UtilsWithStringConverter = Utils & { bufferToString( buffer: ArrayBuffer, encoding: string, start?: number, end?: number, ): string; stringToBuffer(str: string, encoding: string): ArrayBuffer; }; const utils = NitroModules.createHybridObject('Utils'); const isHermes = (global as { HermesInternal?: unknown }).HermesInternal != null; // v0.78.0, https://github.com/facebook/react-native/commit/c6f12254d16d87978383c08065a626d437e60450 // Use jsi::String::getStringData() rather than jsi::String::utf16() const canGetU16StringFromJsiString = !( Platform.constants.reactNativeVersion.major == 0 && Platform.constants.reactNativeVersion.minor < 78 ); // v0.79.0, https://github.com/facebook/react-native/commit/d9d824055e9f24614abd5657f9fc89a6ab3f2da2 const canCreateJsiStringFromUtf16 = !( Platform.constants.reactNativeVersion.major == 0 && Platform.constants.reactNativeVersion.minor < 79 ); const baseNativeEncodings = [ 'hex', 'base64', 'base64url', 'utf8', 'utf-8', 'latin1', 'binary', 'ascii', ]; const nativeStringToBufferEncodings = new Set(baseNativeEncodings); const nativeBufferToStringEncodings = new Set(baseNativeEncodings); // The fast and lossless paths for utf16le are only available on Hermes if (isHermes) { if (canGetU16StringFromJsiString) { nativeStringToBufferEncodings.add('utf16le'); } if (canCreateJsiStringFromUtf16) { nativeBufferToStringEncodings.add('utf16le'); } } // WebCrypto / Web IDL §BufferSource: SharedArrayBuffer-backed inputs must // be rejected. Concurrent writes from another worker during async crypto // can corrupt computations or leak intermediate state, so even copying // the source isn't safe (the copy itself races). Reject at conversion. // See Node's `lib/internal/webidl.js` BufferSource converter (commit // bee10872588) — it throws TypeError, matching the WebIDL spec. // // We apply this guard to *every* conversion helper, not just the ones // reached from `subtle.*`. That's deliberately stricter than Node, whose // classic APIs (`createHash().update`, `createHmac().update`, // `createCipheriv().update`, etc.) accept SAB-backed views. The TOCTOU // concern is the same on either side of the WebCrypto / classic line, so // we prefer the safer default everywhere. export function rejectSharedArrayBuffer(buf: unknown): void { if (typeof SharedArrayBuffer === 'undefined') return; if (buf instanceof SharedArrayBuffer) { throw new TypeError('SharedArrayBuffer is not a supported BufferSource'); } if ( ArrayBuffer.isView(buf) && (buf as ArrayBufferView).buffer instanceof SharedArrayBuffer ) { throw new TypeError( 'View on a SharedArrayBuffer is not a supported BufferSource', ); } } /** * Returns the underlying ArrayBuffer of a Buffer / TypedArray view **without * copying**, ignoring `byteOffset`/`byteLength`. The full backing storage is * exposed. * * Only use this when the caller separately tracks `byteOffset`/`byteLength` * and the native receiver needs to write back into the original memory * (e.g. `randomFill`). For data that will be read by native crypto, use * `binaryLikeToArrayBuffer`/`toArrayBuffer` instead — those return only the * view's region and won't leak unrelated bytes from the backing buffer. */ export const abvToArrayBuffer = (buf: ABV) => { rejectSharedArrayBuffer(buf); if (CraftzdogBuffer.isBuffer(buf)) { return buf.buffer as ArrayBuffer; } if (ArrayBuffer.isView(buf)) { return buf.buffer as ArrayBuffer; } return buf as ArrayBuffer; }; /** * Converts supplied argument to an ArrayBuffer. Note this copies data * only when the supplied view represents a subrange of the underlying * ArrayBuffer; otherwise the backing buffer is returned directly * (aliased — do not mutate after passing). * @param buf * @returns ArrayBuffer */ export function toArrayBuffer( buf: CraftzdogBuffer | SafeBuffer | ArrayBufferView, ): ArrayBuffer { if (CraftzdogBuffer.isBuffer(buf) || ArrayBuffer.isView(buf)) { if (buf.byteOffset === 0 && buf.byteLength === buf.buffer.byteLength) { return buf.buffer as ArrayBuffer; } else { return buf.buffer.slice( buf.byteOffset, buf.byteOffset + buf.byteLength, ) as ArrayBuffer; } } const ab = new ArrayBuffer(buf.length); const view = new Uint8Array(ab); for (let i = 0; i < buf.length; ++i) { view[i] = SafeBuffer.isBuffer(buf) ? buf.readUInt8(i) : buf[i]!; } return ab; } export function bufferLikeToArrayBuffer(buf: BufferLike): ArrayBuffer { rejectSharedArrayBuffer(buf); // Buffer if (CraftzdogBuffer.isBuffer(buf) || SafeBuffer.isBuffer(buf)) { return toArrayBuffer(buf); } // ArrayBufferView if (ArrayBuffer.isView(buf)) { return toArrayBuffer(buf); } // If buf is already an ArrayBuffer, return it. if (buf instanceof ArrayBuffer) { return buf; } throw new TypeError( 'Input must be a Buffer, ArrayBufferView, or ArrayBuffer.', ); } export function binaryLikeToArrayBuffer( input: BinaryLikeNode, // CipherKey adds compat with node types encoding: string = 'utf-8', ): ArrayBuffer { rejectSharedArrayBuffer(input); // string if (typeof input === 'string') { if (encoding === 'buffer') { throw new Error( 'Cannot create a buffer from a string with a buffer encoding', ); } if (nativeStringToBufferEncodings.has(encoding)) { return utils.stringToBuffer(input, encoding); } const buffer = CraftzdogBuffer.from(input, encoding); return buffer.buffer.slice( buffer.byteOffset, buffer.byteOffset + buffer.byteLength, ); } // Buffer if (CraftzdogBuffer.isBuffer(input) || SafeBuffer.isBuffer(input)) { return toArrayBuffer(input); } // ArrayBufferView // TODO add further binary types to BinaryLike, UInt8Array and so for have this array as property if (ArrayBuffer.isView(input)) { return toArrayBuffer(input); } // ArrayBuffer if (input instanceof ArrayBuffer) { return input; } // if (!(input instanceof ArrayBuffer)) { // try { // // this is a strange fallback case and input is unknown at this point // const buffer = Buffer.from(input as unknown as string); // return buffer.buffer.slice( // buffer.byteOffset, // buffer.byteOffset + buffer.byteLength // ); // } catch(e: unknown) { // console.log('throwing 1'); // const err = e as Error; // throw new Error(err.message); // } // } // KeyObject — duck-typed via Symbol.toStringTag to avoid circular dependency // with keys/classes. The type assertion must match KeyObjectHandle.exportKey(). if ( typeof input === 'object' && input != null && Object.prototype.toString.call(input) === '[object KeyObject]' ) { return ( input as { handle: { exportKey(): ArrayBuffer } } ).handle.exportKey(); } throw new Error( 'Invalid argument type for "key". Need ArrayBuffer, TypedArray, KeyObject, CryptoKey, string', ); } export function ab2str( buf: ArrayBuffer, encoding: string = 'hex', start?: number, end?: number, ): string { if (nativeBufferToStringEncodings.has(encoding)) { return bufferToString(buf, encoding, start, end); } return CraftzdogBuffer.from(buf).toString(encoding, start, end); } /** Native C++ buffer-to-string with arguments normalization*/ export function bufferToString( buf: ArrayBuffer, encoding: string = 'hex', start?: number, end?: number, ): string { // https://github.com/nodejs/node/blob/v24.15.0/lib/buffer.js#L915-L928 if (start === undefined || start < 0) { start = 0; } else if (start >= buf.byteLength) { return ''; } else { start = Math.trunc(start) || 0; } if (end === undefined || end > buf.byteLength) { end = buf.byteLength; } else { end = Math.trunc(end) || 0; } if (end <= start) { return ''; } return utils.bufferToString(buf, encoding, start, end); } /** Native C++ string-to-buffer — exposed for benchmarking */ export function stringToBuffer( str: string, encoding: string = 'utf-8', ): ArrayBuffer { return utils.stringToBuffer(str, encoding); } export const kEmptyObject = Object.freeze(Object.create(null)); export * from './noble';