Skip to content

Commit 2a68056

Browse files
authored
feat: try to use crypto.hash first (#62)
> 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.
1 parent f4aeee1 commit 2a68056

File tree

5 files changed

+142
-6
lines changed

5 files changed

+142
-6
lines changed

.github/workflows/nodejs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ jobs:
1313
uses: node-modules/github-actions/.github/workflows/node-test.yml@master
1414
with:
1515
os: 'ubuntu-latest'
16-
version: '16, 18, 20'
16+
version: '16, 18, 20, 21'

benchmark/md5.cjs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
22
const Benchmark = require('benchmark');
3+
const crypto = require('node:crypto');
34
const utils = require('..');
45

56
const suite = new Benchmark.Suite();
67

8+
function createHashWithMD5(s) {
9+
const sum = crypto.createHash('md5');
10+
sum.update(s);
11+
return sum.digest('hex');
12+
}
13+
714
console.log("utils.md5({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", utils.md5({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
815
console.log("utils.md5(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))",
916
utils.md5(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] })));
@@ -24,8 +31,19 @@ suite
2431
})
2532
.add("utils.md5('苏千')", function() {
2633
utils.md5('苏千');
27-
})
34+
});
2835

36+
if (crypto.hash) {
37+
suite.add('createHashWithMD5(Buffer.alloc(1024))', function() {
38+
createHashWithMD5(Buffer.alloc(1024));
39+
}).add("crypto.hash('md5', Buffer.alloc(1024))", function() {
40+
crypto.hash('md5', Buffer.alloc(1024));
41+
});
42+
console.log("crypto.hash('md5', Buffer.alloc(1024))", crypto.hash('md5', Buffer.alloc(1024)));
43+
console.log('createHashWithMD5(Buffer.alloc(1024))', createHashWithMD5(Buffer.alloc(1024)));
44+
}
45+
46+
suite
2947
.add("utils.sha1({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", function() {
3048
utils.sha1({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] });
3149
})

benchmark/sha512.cjs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-disable @typescript-eslint/no-var-requires */
2+
const Benchmark = require('benchmark');
3+
const crypto = require('node:crypto');
4+
const utils = require('..');
5+
6+
const suite = new Benchmark.Suite();
7+
8+
function createHashWithSHA512(s) {
9+
const sum = crypto.createHash('sha512');
10+
sum.update(s);
11+
return sum.digest('hex');
12+
}
13+
14+
console.log("utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", utils.sha512({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
15+
console.log("utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))",
16+
utils.sha512(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] })));
17+
console.log("utils.sha512('苏千')", utils.sha512('苏千'));
18+
19+
console.log('------------- %s -----------', Date());
20+
21+
suite
22+
.add("utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]})", function() {
23+
utils.sha512({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] });
24+
})
25+
.add("utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]})))", function() {
26+
utils.sha512(JSON.stringify({ foo: 'bar', bar: 'foo', v: [ 1, 2, 3 ] }));
27+
})
28+
.add("utils.sha512('苏千')", function() {
29+
utils.sha512('苏千');
30+
});
31+
32+
if (crypto.hash) {
33+
suite.add('createHashWithSHA512(Buffer.alloc(1024))', function() {
34+
createHashWithSHA512(Buffer.alloc(1024));
35+
}).add("crypto.hash('sha512', Buffer.alloc(1024))", function() {
36+
crypto.hash('sha512', Buffer.alloc(1024));
37+
});
38+
console.log("crypto.hash('sha512', Buffer.alloc(1024))", crypto.hash('sha512', Buffer.alloc(1024)));
39+
console.log('createHashWithSHA512(Buffer.alloc(1024))', createHashWithSHA512(Buffer.alloc(1024)));
40+
}
41+
42+
suite
43+
44+
// add listeners
45+
.on('cycle', function(event) {
46+
console.log(String(event.target));
47+
})
48+
.on('complete', function() {
49+
console.log('Fastest is ' + this.filter('fastest').map('name'));
50+
})
51+
.run({ async: false });
52+
53+
// node benchmark/sha512.cjs
54+
// utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]}) 81e725c5a3e77365521c0f7448e2099d7400b92e8893230495b2ae54d7bb938c063a575ad7cb79750b45f59824a9ff0b251f9d0ba27cadcff0cda8f745538950
55+
// utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]})) 1f8288664f4ead755b2e6b5a6b4c4fdfd8fb4933fa398524461f598e22e402af7ae9b49e9473c9cbeb036abbe6e6c6ab3f8484f3d15acc79beaf8aecc0a9b076
56+
// utils.sha512('苏千') 913e9b219f70541725a6ed721b42ae88e79f7ea1c7aec53be80ab277d4704b556df265cc4235f942f9dfbbbbd88e02ba2e18f60b217853835aeb362fb1830016
57+
// ------------- Wed Mar 13 2024 10:27:21 GMT+0800 (中国标准时间) -----------
58+
// crypto.hash('sha512', Buffer.alloc(1024)) 8efb4f73c5655351c444eb109230c556d39e2c7624e9c11abc9e3fb4b9b9254218cc5085b454a9698d085cfa92198491f07a723be4574adc70617b73eb0b6461
59+
// createHashWithSHA512(Buffer.alloc(1024)) 8efb4f73c5655351c444eb109230c556d39e2c7624e9c11abc9e3fb4b9b9254218cc5085b454a9698d085cfa92198491f07a723be4574adc70617b73eb0b6461
60+
// utils.sha512({foo: 'bar', bar: 'foo', v: [1, 2, 3]}) x 1,169,875 ops/sec ±6.92% (95 runs sampled)
61+
// utils.sha512(JSON.stringify({foo: 'bar', bar: 'foo', v: [1, 2, 3]}))) x 1,742,893 ops/sec ±1.56% (98 runs sampled)
62+
// utils.sha512('苏千') x 3,102,952 ops/sec ±1.09% (97 runs sampled)
63+
// createHashWithSHA512(Buffer.alloc(1024)) x 597,443 ops/sec ±1.08% (90 runs sampled)
64+
// crypto.hash('sha512', Buffer.alloc(1024)) x 796,968 ops/sec ±0.59% (96 runs sampled)
65+
// Fastest is utils.sha512('苏千')

src/crypto.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,36 @@
11
import { createHash, createHmac, BinaryToTextEncoding } from 'node:crypto';
2+
import crypto from 'node:crypto';
23

3-
type HashInput = string | Buffer | object;
4+
type HashInput = string | Buffer | ArrayBuffer | DataView | object;
5+
type HashMethod = (method: string, data: HashInput, outputEncoding?: BinaryToTextEncoding) => string;
6+
7+
const nativeHash = 'hash' in crypto ? crypto.hash as HashMethod : null;
48

59
/**
610
* hash
711
*
812
* @param {String} method hash method, e.g.: 'md5', 'sha1'
9-
* @param {String|Buffer|Object} s input value
13+
* @param {String|Buffer|ArrayBuffer|TypedArray|DataView|Object} s input value
1014
* @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
1115
* @return {String} md5 hash string
1216
* @public
1317
*/
1418
export function hash(method: string, s: HashInput, format?: BinaryToTextEncoding): string {
15-
const sum = createHash(method);
16-
const isBuffer = Buffer.isBuffer(s);
19+
if (s instanceof ArrayBuffer) {
20+
s = Buffer.from(s);
21+
}
22+
const isBuffer = Buffer.isBuffer(s) || ArrayBuffer.isView(s);
1723
if (!isBuffer && typeof s === 'object') {
1824
s = JSON.stringify(sortObject(s));
1925
}
26+
27+
if (nativeHash) {
28+
// try to use crypto.hash first
29+
// https://nodejs.org/en/blog/release/v21.7.0#crypto-implement-cryptohash
30+
return nativeHash(method, s, format);
31+
}
32+
33+
const sum = createHash(method);
2034
sum.update(s as string, isBuffer ? 'binary' : 'utf8');
2135
return sum.digest(format || 'hex');
2236
}
@@ -57,6 +71,18 @@ export function sha256(s: HashInput, format?: BinaryToTextEncoding): string {
5771
return hash('sha256', s, format);
5872
}
5973

74+
/**
75+
* sha512 hash
76+
*
77+
* @param {String|Buffer|Object} s input value
78+
* @param {String} [format] output string format, could be 'hex' or 'base64'. default is 'hex'.
79+
* @return {String} sha512 hash string
80+
* @public
81+
*/
82+
export function sha512(s: HashInput, format?: BinaryToTextEncoding): string {
83+
return hash('sha512', s, format);
84+
}
85+
6086
/**
6187
* HMAC algorithm.
6288
*

test/crypto.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ describe('test/crypto.test.ts', () => {
3535
assert.equal(md5({ foo: 'bar', bar: 'foo', args: { age: 1, name: 'foo' }, args2: { haha: '哈哈', bi: 'boo' }, v: [ 1, 2, 3 ] }),
3636
md5({ v: [ 1, 2, 3 ], bar: 'foo', foo: 'bar', args2: { bi: 'boo', haha: '哈哈' }, args: { name: 'foo', age: 1 } }));
3737
});
38+
39+
it('should work on ArrayBuffer, TypedArray, DateView', () => {
40+
const nodeBuffer = Buffer.from('中文');
41+
const arrayBuffer = nodeBuffer.buffer.slice(nodeBuffer.byteOffset, nodeBuffer.byteOffset + nodeBuffer.length);
42+
const uintBytes = new Uint8Array(nodeBuffer.length);
43+
for (let i = 0; i < nodeBuffer.byteLength; ++i) {
44+
uintBytes[i] = nodeBuffer[i];
45+
}
46+
const dataview = new DataView(arrayBuffer);
47+
assert.equal(md5(nodeBuffer), 'a7bac2239fcdcb3a067903d8077c4a07');
48+
assert.equal(md5(arrayBuffer), 'a7bac2239fcdcb3a067903d8077c4a07', 'ArrayBuffer md5 invalid');
49+
assert.equal(md5(dataview), 'a7bac2239fcdcb3a067903d8077c4a07', 'DataView md5 invalid');
50+
assert.equal(md5(uintBytes), 'a7bac2239fcdcb3a067903d8077c4a07', 'Int32Array md5 invalid');
51+
});
3852
});
3953

4054
describe('sha1()', () => {
@@ -71,6 +85,19 @@ describe('test/crypto.test.ts', () => {
7185
});
7286
});
7387

88+
describe('sha512()', () => {
89+
it('should return sha512 hex string', () => {
90+
assert.equal(utility.sha512(''), 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e');
91+
assert.equal(utility.sha512('123'), '3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2');
92+
assert.equal(utility.sha512('哈哈中文'), '648c07b8103f2c9600163fccccdb0268fd98e0aedf002d0a29b270190d0d3ad44ca9484f8a11711672abe704e97f26b55e3a090a1969aeba052b9b783c4eff6c');
93+
assert.equal(utility.sha512(Buffer.from('')), 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e');
94+
assert.equal(utility.sha512(Buffer.from('123')), '3c9909afec25354d551dae21590bb26e38d53f2173b8d3dc3eee4c047e7ab1c1eb8b85103e3be7ba613b31bb5c9c36214dc9f14a42fd7a2fdb84856bca5c44c2');
95+
assert.equal(utility.sha512(Buffer.from('哈哈中文')), '648c07b8103f2c9600163fccccdb0268fd98e0aedf002d0a29b270190d0d3ad44ca9484f8a11711672abe704e97f26b55e3a090a1969aeba052b9b783c4eff6c');
96+
assert.equal(utility.sha512(Buffer.from('@Python发烧友')), 'e387db347ab42a7e44aebc8f165e0b6e42941692efa38fa82d0bea6844cf80d060fa3df7c9eafc2accecca436a6c3fa905920d130b6e1cc8f5a80f1a514f358f');
97+
assert.equal(utility.sha512(Buffer.from('苏千')), '913e9b219f70541725a6ed721b42ae88e79f7ea1c7aec53be80ab277d4704b556df265cc4235f942f9dfbbbbd88e02ba2e18f60b217853835aeb362fb1830016');
98+
});
99+
});
100+
74101
describe('hmac()', () => {
75102
it('should return hmac-sha1', () => {
76103
// $ echo -n "hello world" | openssl dgst -binary -sha1 -hmac "I am a key" | openssl base64

0 commit comments

Comments
 (0)