Skip to content

Commit 1d03df4

Browse files
bnoordhuisMylesBorins
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 a1c524d commit 1d03df4

File tree

5 files changed

+94
-8
lines changed

5 files changed

+94
-8
lines changed

‎doc/api/crypto.md‎

Lines changed: 37 additions & 0 deletions
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+
constcrypto=require('crypto');
1067+
consthash=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‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const kFinalized = Symbol('kFinalized');
3434
functionHash(algorithm,options){
3535
if(!(thisinstanceofHash))
3636
returnnewHash(algorithm,options);
37-
validateString(algorithm,'algorithm');
37+
if(!(algorithminstanceof_Hash))
38+
validateString(algorithm,'algorithm');
3839
constxofLen=typeofoptions==='object'&&options!==null ?
3940
options.outputLength : undefined;
4041
if(xofLen!==undefined)
@@ -49,6 +50,14 @@ function Hash(algorithm, options){
4950
Object.setPrototypeOf(Hash.prototype,LazyTransform.prototype);
5051
Object.setPrototypeOf(Hash,LazyTransform);
5152

53+
Hash.prototype.copy=functioncopy(options){
54+
conststate=this[kState];
55+
if(state[kFinalized])
56+
thrownewERR_CRYPTO_HASH_FINALIZED();
57+
58+
returnnewHash(this[kHandle],options);
59+
};
60+
5261
Hash.prototype._transform=function_transform(chunk,encoding,callback){
5362
this[kHandle].update(chunk,encoding);
5463
callback();

‎src/node_crypto.cc‎

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4720,7 +4720,16 @@ void Hash::Initialize(Environment* env, Local<Object> target){
47204720
voidHash::New(const FunctionCallbackInfo<Value>& args){
47214721
Environment* env = Environment::GetCurrent(args);
47224722

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

47254734
Maybe<unsignedint> xof_md_len = Nothing<unsignedint>();
47264735
if (!args[1]->IsUndefined()){
@@ -4729,17 +4738,19 @@ void Hash::New(const FunctionCallbackInfo<Value>& args){
47294738
}
47304739

47314740
Hash* hash = newHash(env, args.This());
4732-
if (!hash->HashInit(*hash_type, xof_md_len)){
4741+
if (md == nullptr || !hash->HashInit(md, xof_md_len)){
47334742
returnThrowCryptoError(env, ERR_get_error(),
47344743
"Digest method not supported");
47354744
}
4745+
4746+
if (orig != nullptr &&
4747+
0 >= EVP_MD_CTX_copy(hash->mdctx_.get(), orig->mdctx_.get())){
4748+
returnThrowCryptoError(env, ERR_get_error(), "Digest copy error");
4749+
}
47364750
}
47374751

47384752

4739-
boolHash::HashInit(constchar* hash_type, Maybe<unsignedint> xof_md_len){
4740-
const EVP_MD* md = EVP_get_digestbyname(hash_type);
4741-
if (md == nullptr)
4742-
returnfalse;
4753+
boolHash::HashInit(const EVP_MD* md, Maybe<unsignedint> xof_md_len){
47434754
mdctx_.reset(EVP_MD_CTX_new());
47444755
if (!mdctx_ || EVP_DigestInit_ex(mdctx_.get(), md, nullptr) <= 0){
47454756
mdctx_.reset();

‎src/node_crypto.h‎

Lines changed: 1 addition & 1 deletion
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-
boolHashInit(constchar* hash_type, v8::Maybe<unsignedint> xof_md_len);
594+
boolHashInit(constEVP_MD* md, v8::Maybe<unsignedint> xof_md_len);
595595
boolHashUpdate(constchar* data, int len);
596596

597597
protected:

‎test/parallel/test-crypto-hash.js‎

Lines changed: 29 additions & 0 deletions
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+
consth=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+
consta=crypto.createHash('sha512').update('abc');
275+
constb=a.copy();
276+
constc=b.copy().update('def');
277+
constd=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)