Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add Hash.prototype.copy() method #29910

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://nodejs.org/api/stream.html#stream_new_stream_transform_options doesn't have an outputLength option!

It looks like the options are "whatever crypto.createHash() accepts", and that the crypto.createHash docs are wrong, https://nodejs.org/api/crypto.html#crypto_crypto_createhash_algorithm_options, and got copied here.

You could fix the createHash docs, but that's unrelated to this feature, perhaps just change the docs here to link to them and say "same options as over there", so when the createHash docs get fixed this will be fixed, too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally, I don't think it's too confusing but I can split it off into a separate issue if you like.

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.
jasnell marked this conversation as resolved.
Show resolved Hide resolved

```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 @@ -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)
Expand All @@ -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();
Expand Down
23 changes: 17 additions & 6 deletions src/node_crypto.cc
Expand Up @@ -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()) {
Expand All @@ -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();
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'));
}