Skip to content

Commit

Permalink
feat: try to use crypto.hash first (#62)
Browse files Browse the repository at this point in the history
> This can be 1.2-2x faster than the object-based createHash() for smaller inputs (<= 5MB)
> https://nodejs.org/en/blog/release/v21.7.0#crypto-implement-cryptohash

Add new `sha512` helper function.
  • Loading branch information
fengmk2 authored Mar 13, 2024
1 parent f4aeee1 commit 2a68056
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ jobs:
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
with:
os: 'ubuntu-latest'
version: '16, 18, 20'
version: '16, 18, 20, 21'
20 changes: 19 additions & 1 deletion benchmark/md5.cjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const Benchmark = require('benchmark');
const crypto = require('node:crypto');
const utils = require('..');

const suite = new Benchmark.Suite();

function createHashWithMD5(s) {
const sum = crypto.createHash('md5');
sum.update(s);
return sum.digest('hex');
}

console.log("utils.md5({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", utils.md5({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
console.log("utils.md5(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))",
utils.md5(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] })));
Expand All @@ -24,8 +31,19 @@ suite
})
.add("utils.md5('苏千')", function() {
utils.md5('苏千');
})
});

if (crypto.hash) {
suite.add('createHashWithMD5(Buffer.alloc(1024))', function() {
createHashWithMD5(Buffer.alloc(1024));
}).add("crypto.hash('md5', Buffer.alloc(1024))", function() {
crypto.hash('md5', Buffer.alloc(1024));
});
console.log("crypto.hash('md5', Buffer.alloc(1024))", crypto.hash('md5', Buffer.alloc(1024)));
console.log('createHashWithMD5(Buffer.alloc(1024))', createHashWithMD5(Buffer.alloc(1024)));
}

suite
.add("utils.sha1({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", function() {
utils.sha1({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] });
})
Expand Down
65 changes: 65 additions & 0 deletions benchmark/sha512.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const Benchmark = require('benchmark');
const crypto = require('node:crypto');
const utils = require('..');

const suite = new Benchmark.Suite();

function createHashWithSHA512(s) {
const sum = crypto.createHash('sha512');
sum.update(s);
return sum.digest('hex');
}

console.log("utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", utils.sha512({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
console.log("utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))",
utils.sha512(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] })));
console.log("utils.sha512('苏千')", utils.sha512('苏千'));

console.log('------------- %s -----------', Date());

suite
.add("utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", function() {
utils.sha512({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] });
})
.add("utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]})))", function() {
utils.sha512(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
})
.add("utils.sha512('苏千')", function() {
utils.sha512('苏千');
});

if (crypto.hash) {
suite.add('createHashWithSHA512(Buffer.alloc(1024))', function() {
createHashWithSHA512(Buffer.alloc(1024));
}).add("crypto.hash('sha512', Buffer.alloc(1024))", function() {
crypto.hash('sha512', Buffer.alloc(1024));
});
console.log("crypto.hash('sha512', Buffer.alloc(1024))", crypto.hash('sha512', Buffer.alloc(1024)));
console.log('createHashWithSHA512(Buffer.alloc(1024))', createHashWithSHA512(Buffer.alloc(1024)));
}

suite

// add listeners
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
.run({ async: false });

// node benchmark/sha512.cjs
// utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]}) 81e725c5a3e77365521c0f7448e2099d7400b92e8893230495b2ae54d7bb938c063a575ad7cb79750b45f59824a9ff0b251f9d0ba27cadcff0cda8f745538950
// utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]})) 1f8288664f4ead755b2e6b5a6b4c4fdfd8fb4933fa398524461f598e22e402af7ae9b49e9473c9cbeb036abbe6e6c6ab3f8484f3d15acc79beaf8aecc0a9b076
// utils.sha512('苏千') 913e9b219f70541725a6ed721b42ae88e79f7ea1c7aec53be80ab277d4704b556df265cc4235f942f9dfbbbbd88e02ba2e18f60b217853835aeb362fb1830016
// ------------- Wed Mar 13 2024 10:27:21 GMT+0800 (中国标准时间) -----------
// crypto.hash('sha512', Buffer.alloc(1024)) 8efb4f73c5655351c444eb109230c556d39e2c7624e9c11abc9e3fb4b9b9254218cc5085b454a9698d085cfa92198491f07a723be4574adc70617b73eb0b6461
// createHashWithSHA512(Buffer.alloc(1024)) 8efb4f73c5655351c444eb109230c556d39e2c7624e9c11abc9e3fb4b9b9254218cc5085b454a9698d085cfa92198491f07a723be4574adc70617b73eb0b6461
// utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]}) x 1,169,875 ops/sec ±6.92% (95 runs sampled)
// utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))) x 1,742,893 ops/sec ±1.56% (98 runs sampled)
// utils.sha512('苏千') x 3,102,952 ops/sec ±1.09% (97 runs sampled)
// createHashWithSHA512(Buffer.alloc(1024)) x 597,443 ops/sec ±1.08% (90 runs sampled)
// crypto.hash('sha512', Buffer.alloc(1024)) x 796,968 ops/sec ±0.59% (96 runs sampled)
// Fastest is utils.sha512('苏千')
34 changes: 30 additions & 4 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import { createHash, createHmac, BinaryToTextEncoding } from 'node:crypto';
import crypto from 'node:crypto';

type HashInput = string | Buffer | object;
type HashInput = string | Buffer | ArrayBuffer | DataView | object;
type HashMethod = (method: string, data: HashInput, outputEncoding?: BinaryToTextEncoding) => string;

const nativeHash = 'hash' in crypto ? crypto.hash as HashMethod : null;

/**
* hash
*
* @param {String} method hash method, e.g.: 'md5', 'sha1'
* @param {String|Buffer|Object} s input value
* @param {String|Buffer|ArrayBuffer|TypedArray|DataView|Object} s input value
* @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
* @return {String} md5 hash string
* @public
*/
export function hash(method: string, s: HashInput, format?: BinaryToTextEncoding): string {
const sum = createHash(method);
const isBuffer = Buffer.isBuffer(s);
if (s instanceof ArrayBuffer) {
s = Buffer.from(s);
}
const isBuffer = Buffer.isBuffer(s) || ArrayBuffer.isView(s);
if (!isBuffer && typeof s === 'object') {
s = JSON.stringify(sortObject(s));
}

if (nativeHash) {
// try to use crypto.hash first
// https://nodejs.org/en/blog/release/v21.7.0#crypto-implement-cryptohash
return nativeHash(method, s, format);
}

const sum = createHash(method);
sum.update(s as string, isBuffer ? 'binary' : 'utf8');
return sum.digest(format || 'hex');
}
Expand Down Expand Up @@ -57,6 +71,18 @@ export function sha256(s: HashInput, format?: BinaryToTextEncoding): string {
return hash('sha256', s, format);
}

/**
* sha512 hash
*
* @param {String|Buffer|Object} s input value
* @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
* @return {String} sha512 hash string
* @public
*/
export function sha512(s: HashInput, format?: BinaryToTextEncoding): string {
return hash('sha512', s, format);
}

/**
* HMAC algorithm.
*
Expand Down
27 changes: 27 additions & 0 deletions test/crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ describe('test/crypto.test.ts', () => {
assert.equal(md5({ foo: 'bar', bar: 'foo', args: { age: 1, name: 'foo' }, args2: { haha: '哈哈', bi: 'boo' }, v: [ 1, 2, 3 ] }),
md5({ v: [ 1, 2, 3 ], bar: 'foo', foo: 'bar', args2: { bi: 'boo', haha: '哈哈' }, args: { name: 'foo', age: 1 } }));
});

it('should work on ArrayBuffer, TypedArray, DateView', () => {
const nodeBuffer = Buffer.from('中文');
const arrayBuffer = nodeBuffer.buffer.slice(nodeBuffer.byteOffset, nodeBuffer.byteOffset + nodeBuffer.length);
const uintBytes = new Uint8Array(nodeBuffer.length);
for (let i = 0; i < nodeBuffer.byteLength; ++i) {
uintBytes[i] = nodeBuffer[i];
}
const dataview = new DataView(arrayBuffer);
assert.equal(md5(nodeBuffer), 'a7bac2239fcdcb3a067903d8077c4a07');
assert.equal(md5(arrayBuffer), 'a7bac2239fcdcb3a067903d8077c4a07', 'ArrayBuffer md5 invalid');
assert.equal(md5(dataview), 'a7bac2239fcdcb3a067903d8077c4a07', 'DataView md5 invalid');
assert.equal(md5(uintBytes), 'a7bac2239fcdcb3a067903d8077c4a07', 'Int32Array md5 invalid');
});
});

describe('sha1()', () => {
Expand Down Expand Up @@ -71,6 +85,19 @@ describe('test/crypto.test.ts', () => {
});
});

describe('sha512()', () => {
it('should return sha512 hex string', () => {
assert.equal(utility.sha512(''), 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e');
assert.equal(utility.sha512('123'), '3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2');
assert.equal(utility.sha512('哈哈中文'), '648c07b8103f2c9600163fccccdb0268fd98e0aedf002d0a29b270190d0d3ad44ca9484f8a11711672abe704e97f26b55e3a090a1969aeba052b9b783c4eff6c');
assert.equal(utility.sha512(Buffer.from('')), 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e');
assert.equal(utility.sha512(Buffer.from('123')), '3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2');
assert.equal(utility.sha512(Buffer.from('哈哈中文')), '648c07b8103f2c9600163fccccdb0268fd98e0aedf002d0a29b270190d0d3ad44ca9484f8a11711672abe704e97f26b55e3a090a1969aeba052b9b783c4eff6c');
assert.equal(utility.sha512(Buffer.from('@Python发烧友')), 'e387db347ab42a7e44aebc8f165e0b6e42941692efa38fa82d0bea6844cf80d060fa3df7c9eafc2accecca436a6c3fa905920d130b6e1cc8f5a80f1a514f358f');
assert.equal(utility.sha512(Buffer.from('苏千')), '913e9b219f70541725a6ed721b42ae88e79f7ea1c7aec53be80ab277d4704b556df265cc4235f942f9dfbbbbd88e02ba2e18f60b217853835aeb362fb1830016');
});
});

describe('hmac()', () => {
it('should return hmac-sha1', () => {
// $ echo -n "hello world" | openssl dgst -binary -sha1 -hmac "I am a key" | openssl base64
Expand Down

0 comments on commit 2a68056

Please sign in to comment.