UNPKG

react-native-quick-crypto

Version:

A fast implementation of Node's `crypto` module written in C/C++ JSI

282 lines (266 loc) 10.3 kB
"use strict"; import { Stream } from 'readable-stream'; import { NitroModules } from 'react-native-nitro-modules'; import { Buffer } from '@craftzdog/react-native-buffer'; import { ab2str, binaryLikeToArrayBuffer, bufferLikeToArrayBuffer } from './utils'; import { validateMaxBufferLength } from './utils/validation'; import { lazyDOMException } from './utils/errors'; import { normalizeHashName } from './utils/hashnames'; class HashUtils { static native = NitroModules.createHybridObject('Hash'); static getSupportedHashAlgorithms() { return this.native.getSupportedHashAlgorithms(); } } export function getHashes() { return HashUtils.getSupportedHashAlgorithms(); } class Hash extends Stream.Transform { validate(args) { if (typeof args.algorithm !== 'string' || args.algorithm.length === 0) throw new Error('Algorithm must be a non-empty string'); if (args.options?.outputLength !== undefined && args.options.outputLength < 0) throw new Error('Output length must be a non-negative number'); if (args.options?.outputLength !== undefined && typeof args.options.outputLength !== 'number') throw new Error('Output length must be a number'); } /** * @internal use `createHash()` instead */ constructor(args) { super(args.options); this.validate(args); this.algorithm = args.algorithm; this.options = args.options ?? {}; if (args.native) { this.native = args.native; return; } this.native = NitroModules.createHybridObject('Hash'); this.native.createHash(this.algorithm, this.options.outputLength); } /** * Updates the hash content with the given `data`, the encoding of which * is given in `inputEncoding`. * If `encoding` is not provided, and the `data` is a string, an * encoding of `'utf8'` is enforced. If `data` is a `Buffer`, `TypedArray`, or`DataView`, then `inputEncoding` is ignored. * * This can be called many times with new data as it is streamed. * @since v1.0.0 * @param inputEncoding The `encoding` of the `data` string. */ update(data, inputEncoding) { const defaultEncoding = 'utf8'; inputEncoding = inputEncoding ?? defaultEncoding; // OPTIMIZED PATH: Pass UTF-8 strings directly to native without conversion if (typeof data === 'string' && inputEncoding === 'utf8') { this.native.update(data); } else { this.native.update(binaryLikeToArrayBuffer(data, inputEncoding)); } return this; // to support chaining syntax createHash().update().digest() } /** * Calculates the digest of all of the data passed to be hashed (using the `hash.update()` method). * If `encoding` is provided a string will be returned; otherwise * a `Buffer` is returned. * * The `Hash` object can not be used again after `hash.digest()` method has been * called. Multiple calls will cause an error to be thrown. * @since v1.0.0 * @param encoding The `encoding` of the return value. */ digest(encoding) { const nativeDigest = this.native.digest(encoding); if (encoding && encoding !== 'buffer') { return ab2str(nativeDigest, encoding); } return Buffer.from(nativeDigest); } /** * Creates a new `Hash` object that contains a deep copy of the internal state * of the current `Hash` object. * * The optional `options` argument controls stream behavior. For XOF hash * functions such as `'shake256'`, the `outputLength` option can be used to * specify the desired output length in bytes. * * An error is thrown when an attempt is made to copy the `Hash` object after * its `hash.digest()` method has been called. * * ```js * // Calculate a rolling hash. * import { createHash } from 'react-native-quick-crypto'; * * const hash = createHash('sha256'); * * hash.update('one'); * console.log(hash.copy().digest('hex')); * * hash.update('two'); * console.log(hash.copy().digest('hex')); * * hash.update('three'); * console.log(hash.copy().digest('hex')); * * // Etc. * ``` * @since v1.0.0 * @param options `stream.transform` options */ copy(options) { const newOptions = options ?? this.options; const newNativeHash = this.native.copy(newOptions.outputLength); const hash = new Hash({ algorithm: this.algorithm, options: newOptions, native: newNativeHash }); return hash; } /** * Returns the OpenSSL version string * @since v1.0.0 */ getOpenSSLVersion() { return this.native.getOpenSSLVersion(); } // Stream interface — surface synchronous errors via the callback so // they emit as stream 'error' events instead of throwing out of the // Transform plumbing (which would crash the host pipeline). _transform(chunk, encoding, callback) { try { this.update(chunk, encoding); callback(); } catch (err) { callback(err); } } _flush(callback) { try { this.push(this.digest()); callback(); } catch (err) { callback(err); } } } /** * Creates and returns a `Hash` object that can be used to generate hash digests * using the given `algorithm`. Optional `options` argument controls stream * behavior. For XOF hash functions such as `'shake256'`, the `outputLength` option * can be used to specify the desired output length in bytes. * * The `algorithm` is dependent on the available algorithms supported by the * version of OpenSSL on the platform. Examples are `'sha256'`, `'sha512'`, etc. * On recent releases of OpenSSL, `openssl list -digest-algorithms` will * display the available digest algorithms. * * Example: generating the sha256 sum of a file * * ```js * import crypto from 'react-native-quick-crypto'; * * const hash = crypto.createHash('sha256').update('Test123').digest('hex'); * console.log('SHA-256 of "Test123":', hash); * ``` * @since v1.0.0 * @param options `stream.transform` options */ export function createHash(algorithm, options) { // @ts-expect-error private constructor return new Hash({ algorithm, options }); } // Implementation for WebCrypto subtle.digest() /** * Asynchronous digest function for WebCrypto SubtleCrypto API * @param algorithm The hash algorithm to use * @param data The data to hash * @returns Promise resolving to the hash digest as ArrayBuffer */ export const asyncDigest = async (algorithm, data) => { validateMaxBufferLength(data, 'data'); const name = algorithm.name; if (name === 'SHA-1' || name === 'SHA-256' || name === 'SHA-384' || name === 'SHA-512' || name === 'SHA3-256' || name === 'SHA3-384' || name === 'SHA3-512') { return internalDigest(algorithm, data); } if (name === 'cSHAKE128' || name === 'cSHAKE256') { // CShakeParams.outputLength is required (in bits) per the WICG modern-algos // spec, renamed from `length` (commit ab8dc2b84c2). Mirror Node's // hash.js:223-228 / webidl.js:570-595. if (typeof algorithm.outputLength !== 'number' || algorithm.outputLength <= 0) { throw lazyDOMException('CShakeParams.outputLength is required', 'OperationError'); } if (algorithm.outputLength % 8) { throw lazyDOMException('Unsupported CShakeParams outputLength', 'NotSupportedError'); } return internalDigest(algorithm, data, algorithm.outputLength / 8); } if (name === 'TurboSHAKE128' || name === 'TurboSHAKE256') { return turboShakeDigest(name, algorithm, data); } if (name === 'KT128' || name === 'KT256') { return kangarooTwelveDigest(name, algorithm, data); } throw lazyDOMException(`Unrecognized algorithm name: ${name}`, 'NotSupportedError'); }; // TurboSHAKE / KangarooTwelve are not exposed by OpenSSL EVP, so we route // them to a dedicated Nitro module (cpp/turboshake) that ports the Node.js // reference implementation. Lazy-load to keep the module out of the hot path // for callers that only use SHA-2/SHA-3. let nativeTurboShake; const getTurboShake = () => { if (!nativeTurboShake) { nativeTurboShake = NitroModules.createHybridObject('TurboShake'); } return nativeTurboShake; }; // RFC 9861 §2.2 default per the WICG Modern Algos draft for TurboSHAKE. const kDefaultTurboShakeDomainSeparation = 0x1f; const turboShakeDigest = async (name, algorithm, data) => { if (typeof algorithm.outputLength !== 'number' || algorithm.outputLength <= 0) { throw lazyDOMException('TurboShakeParams.outputLength is required', 'OperationError'); } if (algorithm.outputLength % 8) { throw lazyDOMException('Invalid TurboShakeParams outputLength', 'OperationError'); } const ds = algorithm.domainSeparation ?? kDefaultTurboShakeDomainSeparation; if (typeof ds !== 'number' || !Number.isInteger(ds) || ds < 0x01 || ds > 0x7f) { throw lazyDOMException('TurboShakeParams.domainSeparation must be in range 0x01-0x7f', 'OperationError'); } return getTurboShake().turboShake(name, ds, algorithm.outputLength / 8, bufferLikeToArrayBuffer(data)); }; const kangarooTwelveDigest = async (name, algorithm, data) => { if (typeof algorithm.outputLength !== 'number' || algorithm.outputLength <= 0) { throw lazyDOMException('KangarooTwelveParams.outputLength is required', 'OperationError'); } if (algorithm.outputLength % 8) { throw lazyDOMException('Invalid KangarooTwelveParams outputLength', 'OperationError'); } const customization = algorithm.customization !== undefined ? bufferLikeToArrayBuffer(algorithm.customization) : undefined; return getTurboShake().kangarooTwelve(name, algorithm.outputLength / 8, bufferLikeToArrayBuffer(data), customization); }; const internalDigest = (algorithm, data, outputLength) => { const normalizedHashName = normalizeHashName(algorithm.name); const hash = createHash(normalizedHashName, outputLength ? { outputLength } : undefined); hash.update(bufferLikeToArrayBuffer(data)); const result = hash.digest(); const arrayBuffer = new ArrayBuffer(result.length); const view = new Uint8Array(arrayBuffer); view.set(result); return arrayBuffer; }; export function hash(algorithm, data, outputEncoding) { const h = createHash(algorithm); h.update(data); return outputEncoding ? h.digest(outputEncoding) : h.digest(); } export const hashExports = { createHash, getHashes, hash, asyncDigest }; //# sourceMappingURL=hash.js.map