diff options
author | Ben Noordhuis <info@bnoordhuis.nl> | 2019-10-10 00:33:15 +0200 |
---|---|---|
committer | Rich Trott <rtrott@gmail.com> | 2019-10-16 10:00:00 -0700 |
commit | 9f203f927c732a1f2f707ecce5e8656e3e4c2459 (patch) | |
tree | ade2a25aa62778f0e215fc124ef8deed9ea85b08 | |
parent | e22efba812b2a6c2ee6d35f4e11af5b08afd881d (diff) | |
download | android-node-v8-9f203f927c732a1f2f707ecce5e8656e3e4c2459.tar.gz android-node-v8-9f203f927c732a1f2f707ecce5e8656e3e4c2459.tar.bz2 android-node-v8-9f203f927c732a1f2f707ecce5e8656e3e4c2459.zip |
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: https://github.com/nodejs/node/issues/29903
PR-URL: https://github.com/nodejs/node/pull/29910
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: David Carlier <devnexen@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
-rw-r--r-- | doc/api/crypto.md | 37 | ||||
-rw-r--r-- | lib/internal/crypto/hash.js | 11 | ||||
-rw-r--r-- | src/node_crypto.cc | 23 | ||||
-rw-r--r-- | src/node_crypto.h | 2 | ||||
-rw-r--r-- | test/parallel/test-crypto-hash.js | 29 |
5 files changed, 94 insertions, 8 deletions
diff --git a/doc/api/crypto.md b/doc/api/crypto.md index 345ce54296..31fa812f91 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -1041,6 +1041,43 @@ console.log(hash.digest('hex')); // 6a2da20943931e9834fc12cfe5bb47bbd9ae43489a30726962b576f4e3993e50 ``` +### hash.copy(\[options\]) +<!-- YAML +added: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/29910 +--> + +* `options` {Object} [`stream.transform` options][] +* Returns: {Hash} + +Creates a new `Hash` object that contains a deep copy of the internal state +of the current `Hash` object. + +The 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. + +An error is thrown when an attempt is made to copy the `Hash` object after +its [`hash.digest()`][] method has been called. + +```js +// Calculate a rolling hash. +const crypto = require('crypto'); +const hash = crypto.createHash('sha256'); + +hash.update('one'); +console.log(hash.copy().digest('hex')); + +hash.update('two'); +console.log(hash.copy().digest('hex')); + +hash.update('three'); +console.log(hash.copy().digest('hex')); + +// Etc. +``` + ### hash.digest(\[encoding\]) <!-- YAML added: v0.1.92 diff --git a/lib/internal/crypto/hash.js b/lib/internal/crypto/hash.js index eccfb44569..b28f9fca13 100644 --- a/lib/internal/crypto/hash.js +++ b/lib/internal/crypto/hash.js @@ -34,7 +34,8 @@ const kFinalized = Symbol('kFinalized'); function Hash(algorithm, options) { if (!(this instanceof Hash)) return new Hash(algorithm, options); - validateString(algorithm, 'algorithm'); + if (!(algorithm instanceof _Hash)) + validateString(algorithm, 'algorithm'); const xofLen = typeof options === 'object' && options !== null ? options.outputLength : undefined; if (xofLen !== undefined) @@ -49,6 +50,14 @@ function Hash(algorithm, options) { Object.setPrototypeOf(Hash.prototype, LazyTransform.prototype); Object.setPrototypeOf(Hash, LazyTransform); +Hash.prototype.copy = function copy(options) { + const state = this[kState]; + if (state[kFinalized]) + throw new ERR_CRYPTO_HASH_FINALIZED(); + + return new Hash(this[kHandle], options); +}; + Hash.prototype._transform = function _transform(chunk, encoding, callback) { this[kHandle].update(chunk, encoding); callback(); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 3405fbb5b4..a8d26ffa0a 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -4720,7 +4720,16 @@ void Hash::Initialize(Environment* env, Local<Object> target) { void Hash::New(const FunctionCallbackInfo<Value>& args) { Environment* env = Environment::GetCurrent(args); - const node::Utf8Value hash_type(env->isolate(), args[0]); + const Hash* orig = nullptr; + const EVP_MD* md = nullptr; + + if (args[0]->IsObject()) { + ASSIGN_OR_RETURN_UNWRAP(&orig, args[0].As<Object>()); + md = EVP_MD_CTX_md(orig->mdctx_.get()); + } else { + const node::Utf8Value hash_type(env->isolate(), args[0]); + md = EVP_get_digestbyname(*hash_type); + } Maybe<unsigned int> xof_md_len = Nothing<unsigned int>(); if (!args[1]->IsUndefined()) { @@ -4729,17 +4738,19 @@ void Hash::New(const FunctionCallbackInfo<Value>& args) { } Hash* hash = new Hash(env, args.This()); - if (!hash->HashInit(*hash_type, xof_md_len)) { + if (md == nullptr || !hash->HashInit(md, xof_md_len)) { return ThrowCryptoError(env, ERR_get_error(), "Digest method not supported"); } + + if (orig != nullptr && + 0 >= EVP_MD_CTX_copy(hash->mdctx_.get(), orig->mdctx_.get())) { + return ThrowCryptoError(env, ERR_get_error(), "Digest copy error"); + } } -bool Hash::HashInit(const char* hash_type, Maybe<unsigned int> xof_md_len) { - const EVP_MD* md = EVP_get_digestbyname(hash_type); - if (md == nullptr) - return false; +bool Hash::HashInit(const EVP_MD* md, Maybe<unsigned int> xof_md_len) { mdctx_.reset(EVP_MD_CTX_new()); if (!mdctx_ || EVP_DigestInit_ex(mdctx_.get(), md, nullptr) <= 0) { mdctx_.reset(); diff --git a/src/node_crypto.h b/src/node_crypto.h index 206a19119a..777ba5d302 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -591,7 +591,7 @@ class Hash : public BaseObject { SET_MEMORY_INFO_NAME(Hash) SET_SELF_SIZE(Hash) - bool HashInit(const char* hash_type, v8::Maybe<unsigned int> xof_md_len); + bool HashInit(const EVP_MD* md, v8::Maybe<unsigned int> xof_md_len); bool HashUpdate(const char* data, int len); protected: diff --git a/test/parallel/test-crypto-hash.js b/test/parallel/test-crypto-hash.js index 7f752f898a..a0c3ffeb20 100644 --- a/test/parallel/test-crypto-hash.js +++ b/test/parallel/test-crypto-hash.js @@ -192,12 +192,25 @@ common.expectsError( assert.strictEqual(crypto.createHash('shake256').digest('hex'), '46b9dd2b0ba88d13233b3feb743eeb24' + '3fcd52ea62b81b82b50c27646ed5762f'); + assert.strictEqual(crypto.createHash('shake256', { outputLength: 0 }) + .copy() // Default outputLength. + .digest('hex'), + '46b9dd2b0ba88d13233b3feb743eeb24' + + '3fcd52ea62b81b82b50c27646ed5762f'); // Short outputLengths. assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 }) .digest('hex'), ''); assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 }) + .copy({ outputLength: 0 }) + .digest('hex'), + ''); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 5 }) + .digest('hex'), + '7f9c2ba4e8'); + assert.strictEqual(crypto.createHash('shake128', { outputLength: 0 }) + .copy({ outputLength: 5 }) .digest('hex'), '7f9c2ba4e8'); assert.strictEqual(crypto.createHash('shake128', { outputLength: 15 }) @@ -249,3 +262,19 @@ common.expectsError( { code: 'ERR_OUT_OF_RANGE' }); } } + +{ + const h = crypto.createHash('sha512'); + h.digest(); + common.expectsError(() => h.copy(), { code: 'ERR_CRYPTO_HASH_FINALIZED' }); + common.expectsError(() => h.digest(), { code: 'ERR_CRYPTO_HASH_FINALIZED' }); +} + +{ + const a = crypto.createHash('sha512').update('abc'); + const b = a.copy(); + const c = b.copy().update('def'); + const d = crypto.createHash('sha512').update('abcdef'); + assert.strictEqual(a.digest('hex'), b.digest('hex')); + assert.strictEqual(c.digest('hex'), d.digest('hex')); +} |