diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index c8d545ef..04ddbf87 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1940,7 +1940,7 @@ SPEC CHECKSUMS: fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a hermes-engine: 46f1ffbf0297f4298862068dd4c274d4ac17a1fd - NitroModules: 3a58d9bc70815a0d5de4476ed6a36eff05a6a0ae + NitroModules: c36d6f656038a56beb1b1bcab2d0252d71744013 OpenSSL-Universal: b60a3702c9fea8b3145549d421fdb018e53ab7b4 QuickCrypto: 6cb5f98b793407d884c39a93e610ebcf7421b56f RCT-Folly: 84578c8756030547307e4572ab1947de1685c599 diff --git a/example/src/tests/hash/hash_tests.ts b/example/src/tests/hash/hash_tests.ts index 60ead5fe..ec3e4393 100644 --- a/example/src/tests/hash/hash_tests.ts +++ b/example/src/tests/hash/hash_tests.ts @@ -26,6 +26,13 @@ test(SUITE, 'createHash with invalid algorithm', () => { }).to.throw(/Unknown hash algorithm: sha123/); }); +test(SUITE, 'createHash with null algorithm', () => { + expect(() => { + // @ts-expect-error bad algorithm + createHash(null); + }).to.throw(/Algorithm must be a non-empty string/); +}); + // test hashing const a0 = createHash('md5').update('Test123').digest('latin1'); const a1 = createHash('sha1').update('Test123').digest('hex'); @@ -161,7 +168,7 @@ test(SUITE, 'digest - segfault', () => { throw new Error('segfault'); }, } as unknown as Encoding); - }).to.throw(/Value is an object/); + }).to.throw(); }); test(SUITE, 'update - calling update without argument', () => { const hash = createHash('sha256'); @@ -223,3 +230,14 @@ test(SUITE, 'unreasonable output length', () => { /Output length 1073741824 exceeds maximum allowed size of 16777216/, ); }); +test(SUITE, 'createHash with negative outputLength', () => { + expect(() => { + createHash('shake128', { outputLength: -1 }); + }).to.throw(/Output length must be a non-negative number/); +}); +test(SUITE, 'createHash with null outputLength', () => { + expect(() => { + // @ts-expect-error bad outputLength + createHash('shake128', { outputLength: null }); + }).to.throw(/Output length must be a number/); +}); diff --git a/packages/react-native-quick-crypto/src/hash.ts b/packages/react-native-quick-crypto/src/hash.ts index e2efdad2..e8b5e593 100644 --- a/packages/react-native-quick-crypto/src/hash.ts +++ b/packages/react-native-quick-crypto/src/hash.ts @@ -35,21 +35,39 @@ class Hash extends Stream.Transform { private options: HashOptions; private native: NativeHash; + private validate(args: HashArgs) { + 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 */ - private constructor({ algorithm, options, native }: HashArgs) { - super(options); + private constructor(args: HashArgs) { + super(args.options); + + this.validate(args); - this.algorithm = algorithm; - this.options = options ?? {}; + this.algorithm = args.algorithm; + this.options = args.options ?? {}; - if (native) { - this.native = native; - } else { - this.native = NitroModules.createHybridObject('Hash'); - this.native.createHash(algorithm, this.options.outputLength); + if (args.native) { + this.native = args.native; + return; } + + this.native = NitroModules.createHybridObject('Hash'); + this.native.createHash(this.algorithm, this.options.outputLength); } /** @@ -70,11 +88,7 @@ class Hash extends Stream.Transform { this.native.update(binaryLikeToArrayBuffer(data, inputEncoding)); - if (typeof data === 'string' && inputEncoding !== 'buffer') { - return this; // to support chaining syntax createHash().update().digest() - } - - return Buffer.from([]); // returning empty buffer as _flush calls digest + return this; // to support chaining syntax createHash().update().digest() } /** @@ -149,7 +163,7 @@ class Hash extends Stream.Transform { encoding: BufferEncoding, callback: () => void, ) { - this.push(this.update(chunk, encoding as Encoding)); + this.update(chunk, encoding as Encoding); callback(); } _flush(callback: () => void) { @@ -158,6 +172,28 @@ class Hash extends Stream.Transform { } } +/** + * 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: string, options?: HashOptions): Hash { // @ts-expect-error private constructor return new Hash({