From 2f9c4fe499ef4091be3c728548ee514365d98a27 Mon Sep 17 00:00:00 2001 From: Joyee Cheung Date: Fri, 5 Jan 2024 22:17:00 +0100 Subject: [PATCH] crypto: implement crypto.hash() This patch introduces a helper crypto.hash() that computes a digest from the input at one shot. This can be 1.2-1.6x faster than the object-based createHash() for smaller inputs (<= 5MB) that are readily available (not streamed) and incur less memory overhead since no intermediate objects will be created. --- benchmark/crypto/oneshot-hash.js | 42 +++++++++++ doc/api/crypto.md | 61 ++++++++++++++++ lib/crypto.js | 2 + lib/internal/crypto/hash.js | 19 +++++ src/api/encoding.cc | 10 +++ src/crypto/crypto_hash.cc | 86 ++++++++++++++++++++--- src/crypto/crypto_hash.h | 1 + src/node_internals.h | 4 ++ test/parallel/test-crypto-oneshot-hash.js | 28 ++++++++ 9 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 benchmark/crypto/oneshot-hash.js create mode 100644 test/parallel/test-crypto-oneshot-hash.js diff --git a/benchmark/crypto/oneshot-hash.js b/benchmark/crypto/oneshot-hash.js new file mode 100644 index 00000000000000..0d518c5afe2e95 --- /dev/null +++ b/benchmark/crypto/oneshot-hash.js @@ -0,0 +1,42 @@ +'use strict'; + +const common = require('../common.js'); +const { createHash, hash } = require('crypto'); +const path = require('path'); +const filepath = path.resolve(__dirname, '../../test/fixtures/snapshot/typescript.js'); +const fs = require('fs'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + length: [1000, 100_000], + method: ['md5', 'sha1', 'sha256'], + type: ['string', 'buffer'], + n: [100_000, 1000], +}, { + combinationFilter: ({ length, n }) => { + return length * n <= 100_000 * 1000; + }, +}); + +function main({ length, type, method, n }) { + let data = fs.readFileSync(filepath); + if (type === 'string') { + data = data.toString().slice(0, length); + } else { + data = Uint8Array.prototype.slice.call(data, 0, length); + } + + const oneshotHash = hash ? + (method, input) => hash(method, input, 'hex') : + (method, input) => createHash(method).update(input).digest('hex'); + const array = []; + for (let i = 0; i < n; i++) { + array.push(null); + } + bench.start(); + for (let i = 0; i < n; i++) { + array[i] = oneshotHash(method, data); + } + bench.end(n); + assert.strictEqual(typeof array[n - 1], 'string'); +} diff --git a/doc/api/crypto.md b/doc/api/crypto.md index deb85404249700..79f62e787dc123 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -3510,6 +3510,67 @@ Computes the Diffie-Hellman secret based on a `privateKey` and a `publicKey`. Both keys must have the same `asymmetricKeyType`, which must be one of `'dh'` (for Diffie-Hellman), `'ec'` (for ECDH), `'x448'`, or `'x25519'` (for ECDH-ES). +### `crypto.hash(algorith, data[, outputEncoding])` + + + +* `algorithm` {string|undefined} +* `data` {string|ArrayBuffer|Buffer|TypedArray|DataView} When `data` is a + string, it will be encoded as UTF-8 before being hashed. If a different + input encoding is desired for a string input, user could encode the string + into a TypedArray using either `TextEncoder` or `Buffer.from()` and passing + the encoded TypedArray into this API instead. +* `outputEncoding` {string|undefined} [Encoding][encoding] used to encode the + returned digest. **Default:** `'hex'`. +* Returns: {string|Buffer} + +A utility for creating one-shot hash digests of data. It can be faster than +the object-based `crypto.createHash()` when hashing a smaller amount of data +(<= 5MB) that's readily available. If the data can be big or if it is streamed, +it's still recommended to use `crypto.createHash()` instead. + +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: + +```cjs +const crypto = require('node:crypto'); +const { Buffer } = require('node:buffer'); + +// Hashing a string and return the result as a hex-encoded string. +const string = 'Node.js'; +// 10b3493287f831e81a438811a1ffba01f8cec4b7 +console.log(crypto.hash('sha1', string)); + +// Encode a base64-encoded string into a Buffer, hash it and return +// the result as a buffer. +const base64 = 'Tm9kZS5qcw=='; +// +console.log(crypto.hash('sha1', Buffer.from(base64, 'base64'), 'buffer')); +``` + +```mjs +import crypto from 'node:crypto'; +import { Buffer } from 'node:buffer'; + +// Hashing a string and return the result as a hex-encoded string. +const string = 'Node.js'; +// 10b3493287f831e81a438811a1ffba01f8cec4b7 +console.log(crypto.hash('sha1', string)); + +// Encode a base64-encoded string into a Buffer, hash it and return +// the result as a buffer. +const base64 = 'Tm9kZS5qcw=='; +// +console.log(crypto.hash('sha1', Buffer.from(base64, 'base64'), 'buffer')); +``` + ### `crypto.generateKey(type, options, callback)`