Navigation Menu

Skip to content

Commit

Permalink
crypto: add Hash.prototype.copy() method
Browse files Browse the repository at this point in the history
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 <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>
  • Loading branch information
bnoordhuis authored and BethGriggs committed Feb 6, 2020
1 parent cab905f commit 5dd72a6
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 8 deletions.
37 changes: 37 additions & 0 deletions doc/api/crypto.md
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion lib/internal/crypto/hash.js
Expand Up @@ -36,7 +36,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)
Expand All @@ -51,6 +52,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();
Expand Down
23 changes: 17 additions & 6 deletions src/node_crypto.cc
Expand Up @@ -4721,7 +4721,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()) {
Expand All @@ -4730,17 +4739,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();
Expand Down
2 changes: 1 addition & 1 deletion src/node_crypto.h
Expand Up @@ -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:
Expand Down
29 changes: 29 additions & 0 deletions test/parallel/test-crypto-hash.js
Expand Up @@ -192,14 +192,27 @@ 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 })
.digest('hex'),
'7f9c2ba4e88f827d61604550760585');
Expand Down Expand Up @@ -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'));
}

0 comments on commit 5dd72a6

Please sign in to comment.