Skip to content

Commit 497b59b

Browse files
bnoordhuistargos
authored andcommitted
crypto: add Hash.prototype.copy() method
Make it possible to clone the internal state of a Hash object into a new Hash object, i.e., to fork the state of the object. Fixes: #29903 PR-URL: #29910 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Sam Roberts <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: David Carlier <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 83e225b commit 497b59b

File tree

5 files changed

+94
-8
lines changed

5 files changed

+94
-8
lines changed

doc/api/crypto.md

+37
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,43 @@ console.log(hash.digest('hex'));
10411041
// 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50
10421042
```
10431043

1044+
### `hash.copy([options])`
1045+
<!-- YAML
1046+
added:
1047+
- version: REPLACEME
1048+
pr-url: https://github.com/nodejs/node/pull/29910
1049+
-->
1050+
1051+
* `options` {Object} [`stream.transform` options][]
1052+
* Returns: {Hash}
1053+
1054+
Creates a new `Hash` object that contains a deep copy of the internal state
1055+
of the current `Hash` object.
1056+
1057+
The optional `options` argument controls stream behavior. For XOF hash
1058+
functions such as `'shake256'`, the `outputLength` option can be used to
1059+
specify the desired output length in bytes.
1060+
1061+
An error is thrown when an attempt is made to copy the `Hash` object after
1062+
its [`hash.digest()`][] method has been called.
1063+
1064+
```js
1065+
// Calculate a rolling hash.
1066+
const crypto = require('crypto');
1067+
const hash = crypto.createHash('sha256');
1068+
1069+
hash.update('one');
1070+
console.log(hash.copy().digest('hex'));
1071+
1072+
hash.update('two');
1073+
console.log(hash.copy().digest('hex'));
1074+
1075+
hash.update('three');
1076+
console.log(hash.copy().digest('hex'));
1077+
1078+
// Etc.
1079+
```
1080+
10441081
### `hash.digest([encoding])`
10451082
<!-- YAML
10461083
added: v0.1.92

lib/internal/crypto/hash.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const kFinalized = Symbol('kFinalized');
3636
function Hash(algorithm, options) {
3737
if (!(this instanceof Hash))
3838
return new Hash(algorithm, options);
39-
validateString(algorithm, 'algorithm');
39+
if (!(algorithm instanceof _Hash))
40+
validateString(algorithm, 'algorithm');
4041
const xofLen = typeof options === 'object' && options !== null ?
4142
options.outputLength : undefined;
4243
if (xofLen !== undefined)
@@ -51,6 +52,14 @@ function Hash(algorithm, options) {
5152
Object.setPrototypeOf(Hash.prototype, LazyTransform.prototype);
5253
Object.setPrototypeOf(Hash, LazyTransform);
5354

55+
Hash.prototype.copy = function copy(options) {
56+
const state = this[kState];
57+
if (state[kFinalized])
58+
throw new ERR_CRYPTO_HASH_FINALIZED();
59+
60+
return new Hash(this[kHandle], options);
61+
};
62+
5463
Hash.prototype._transform = function _transform(chunk, encoding, callback) {
5564
this[kHandle].update(chunk, encoding);
5665
callback();

src/node_crypto.cc

+17-6
Original file line numberDiff line numberDiff line change
@@ -4718,7 +4718,16 @@ void Hash::Initialize(Environment* env, Local<Object> target) {
47184718
void Hash::New(const FunctionCallbackInfo<Value>& args) {
47194719
Environment* env = Environment::GetCurrent(args);
47204720

4721-
const node::Utf8Value hash_type(env->isolate(), args[0]);
4721+
const Hash* orig = nullptr;
4722+
const EVP_MD* md = nullptr;
4723+
4724+
if (args[0]->IsObject()) {
4725+
ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As<Object>());
4726+
md = EVP_MD_CTX_md(orig->mdctx_.get());
4727+
} else {
4728+
const node::Utf8Value hash_type(env->isolate(), args[0]);
4729+
md = EVP_get_digestbyname(*hash_type);
4730+
}
47224731

47234732
Maybe<unsigned int> xof_md_len = Nothing<unsigned int>();
47244733
if (!args[1]->IsUndefined()) {
@@ -4727,17 +4736,19 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) {
47274736
}
47284737

47294738
Hash* hash = new Hash(env, args.This());
4730-
if (!hash->HashInit(*hash_type, xof_md_len)) {
4739+
if (md == nullptr || !hash->HashInit(md, xof_md_len)) {
47314740
return ThrowCryptoError(env, ERR_get_error(),
47324741
"Digest method not supported");
47334742
}
4743+
4744+
if (orig != nullptr &&
4745+
0 >= EVP_MD_CTX_copy(hash->mdctx_.get(), orig->mdctx_.get())) {
4746+
return ThrowCryptoError(env, ERR_get_error(), "Digest copy error");
4747+
}
47344748
}
47354749

47364750

4737-
bool Hash::HashInit(const char* hash_type, Maybe<unsigned int> xof_md_len) {
4738-
const EVP_MD* md = EVP_get_digestbyname(hash_type);
4739-
if (md == nullptr)
4740-
return false;
4751+
bool Hash::HashInit(const EVP_MD* md, Maybe<unsigned int> xof_md_len) {
47414752
mdctx_.reset(EVP_MD_CTX_new());
47424753
if (!mdctx_ || EVP_DigestInit_ex(mdctx_.get(), md, nullptr) <= 0) {
47434754
mdctx_.reset();

src/node_crypto.h

+1-1
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ class Hash : public BaseObject {
591591
SET_MEMORY_INFO_NAME(Hash)
592592
SET_SELF_SIZE(Hash)
593593

594-
bool HashInit(const char* hash_type, v8::Maybe<unsigned int> xof_md_len);
594+
bool HashInit(const EVP_MD* md, v8::Maybe<unsigned int> xof_md_len);
595595
bool HashUpdate(const char* data, int len);
596596

597597
protected:

test/parallel/test-crypto-hash.js

+29
Original file line numberDiff line numberDiff line change
@@ -192,14 +192,27 @@ common.expectsError(
192192
assert.strictEqual(crypto.createHash('shake256').digest('hex'),
193193
'46b9dd2b0ba88d13233b3feb743eeb24' +
194194
'3fcd52ea62b81b82b50c27646ed5762f');
195+
assert.strictEqual(crypto.createHash('shake256', { outputLength: 0 })
196+
.copy() // Default outputLength.
197+
.digest('hex'),
198+
'46b9dd2b0ba88d13233b3feb743eeb24' +
199+
'3fcd52ea62b81b82b50c27646ed5762f');
195200

196201
// Short outputLengths.
197202
assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 })
198203
.digest('hex'),
199204
'');
205+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 })
206+
.copy({ outputLength: 0 })
207+
.digest('hex'),
208+
'');
200209
assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 })
201210
.digest('hex'),
202211
'7f9c2ba4e8');
212+
assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 })
213+
.copy({ outputLength: 5 })
214+
.digest('hex'),
215+
'7f9c2ba4e8');
203216
assert.strictEqual(crypto.createHash('shake128', { outputLength: 15 })
204217
.digest('hex'),
205218
'7f9c2ba4e88f827d61604550760585');
@@ -249,3 +262,19 @@ common.expectsError(
249262
{ code: 'ERR_OUT_OF_RANGE' });
250263
}
251264
}
265+
266+
{
267+
const h = crypto.createHash('sha512');
268+
h.digest();
269+
common.expectsError(() => h.copy(), { code: 'ERR_CRYPTO_HASH_FINALIZED' });
270+
common.expectsError(() => h.digest(), { code: 'ERR_CRYPTO_HASH_FINALIZED' });
271+
}
272+
273+
{
274+
const a = crypto.createHash('sha512').update('abc');
275+
const b = a.copy();
276+
const c = b.copy().update('def');
277+
const d = crypto.createHash('sha512').update('abcdef');
278+
assert.strictEqual(a.digest('hex'), b.digest('hex'));
279+
assert.strictEqual(c.digest('hex'), d.digest('hex'));
280+
}

0 commit comments

Comments
 (0)