summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Noordhuis <info@bnoordhuis.nl>2019-10-10 00:33:15 +0200
committerRich Trott <rtrott@gmail.com>2019-10-16 10:00:00 -0700
commit9f203f927c732a1f2f707ecce5e8656e3e4c2459 (patch)
treeade2a25aa62778f0e215fc124ef8deed9ea85b08
parente22efba812b2a6c2ee6d35f4e11af5b08afd881d (diff)
downloadandroid-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.md37
-rw-r--r--lib/internal/crypto/hash.js11
-rw-r--r--src/node_crypto.cc23
-rw-r--r--src/node_crypto.h2
-rw-r--r--test/parallel/test-crypto-hash.js29
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'));
+}