-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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 crypto.createMac() #32477
Conversation
lib/internal/crypto/hash.js
Outdated
|
||
if (state[kFinalized]) { | ||
const buf = Buffer.from(''); | ||
return outputEncoding === 'buffer' ? buf : buf.toString(outputEncoding); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I am missing something, why does this return an empty buffer / string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I did not add that, it was the behavior that was implemented before... Hmac.prototype.digest returns either an empty Buffer or string when called multiple times, whereas Hash.prototype.digest throws on multiple calls. I did not add that behavior and found it surprising when I did the refactor but it was not the time to change it. I will note, however, that the implemented behavior (that's been there since at least the Node.js 0.12 days) does not match what is documented -- which states that an error would be thrown if digest is called more than once.
C:\Users\jasne>nvs use 0.12
PATH += %LOCALAPPDATA%\nvs\node\0.12.10\x64
C:\Users\jasne>node
> var h = crypto.createHmac('sha256', 'secret')
undefined
> h.update('foo')
{ _handle: {}, _options: undefined }
> h.digest('buffer')
<Buffer 77 3b a4 46 93 c7 55 3d 6e e2 0f 61 ea 5d 27 57 a9 a4 f4 a4 4d 28 41 ae 4e 95 b5 2e 4c d6 2d b4>
> h.digest('buffer')
<Buffer >
> h.digest('utf8')
''
> .exit
C:\Users\jasne>nvs use 13
PATH -= %LOCALAPPDATA%\nvs\node\0.12.10\x64
PATH += %LOCALAPPDATA%\nvs\node\13.11.0\x64
C:\Users\jasne>node
Welcome to Node.js v13.11.0.
Type ".help" for more information.
> var h = crypto.createHmac('sha256', 'secret')
undefined
> h.update('foo')
Hmac {
_options: undefined,
[Symbol(kHandle)]: {},
[Symbol(kState)]: { [Symbol(kFinalized)]: false }
}
> h.digest('buffer')
<Buffer 77 3b a4 46 93 c7 55 3d 6e e2 0f 61 ea 5d 27 57 a9 a4 f4 a4 4d 28 41 ae 4e 95 b5 2e 4c d6 2d b4>
> h.digest('utf8')
''
>
vs..
C:\Users\jasne>nvs use 0.12
PATH -= %LOCALAPPDATA%\nvs\node\13.11.0\x64
PATH += %LOCALAPPDATA%\nvs\node\0.12.10\x64
C:\Users\jasne>node
> var h = crypto.createHash('md5')
undefined
> h.update('foo')
{ _handle: {}, _options: undefined }
> h.digest()
<Buffer ac bd 18 db 4c c2 f8 5c ed ef 65 4f cc c4 a4 d8>
> h.digest()
Error: Not initialized
at Error (native)
at Hash.digest (crypto.js:126:23)
at repl:1:3
at REPLServer.defaultEval (repl.js:132:27)
at bound (domain.js:284:14)
at REPLServer.runBound [as eval] (domain.js:297:12)
at REPLServer.<anonymous> (repl.js:279:12)
at REPLServer.emit (events.js:107:17)
at REPLServer.Interface._onLine (readline.js:214:10)
at REPLServer.Interface._line (readline.js:553:8)
> .exit
C:\Users\jasne>nvs use 13
PATH -= %LOCALAPPDATA%\nvs\node\0.12.10\x64
PATH += %LOCALAPPDATA%\nvs\node\13.11.0\x64
C:\Users\jasne>node
Welcome to Node.js v13.11.0.
Type ".help" for more information.
> var h = crypto.createHash('md5')
undefined
> h.update('foo')
Hash {
_options: undefined,
[Symbol(kHandle)]: {},
[Symbol(kState)]: { [Symbol(kFinalized)]: false }
}
> h.digest()
<Buffer ac bd 18 db 4c c2 f8 5c ed ef 65 4f cc c4 a4 d8>
> h.digest()
Uncaught Error [ERR_CRYPTO_HASH_FINALIZED]: Digest already called
at Hash.digest (internal/crypto/hash.js:97:11)
at repl:1:3
at Script.runInThisContext (vm.js:131:20)
at REPLServer.defaultEval (repl.js:432:29)
at bound (domain.js:429:14)
at REPLServer.runBound [as eval] (domain.js:442:12)
at REPLServer.onLine (repl.js:759:10)
at REPLServer.emit (events.js:327:22)
at REPLServer.EventEmitter.emit (domain.js:485:12)
at REPLServer.Interface._onLine (readline.js:337:10) {
code: 'ERR_CRYPTO_HASH_FINALIZED'
}
>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I'd prefer an exception be thrown on multiple calls.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would agree. For the new API it should throw and I think we should go back and deprecate the current behavior on createHmac()
and eventually switch it to the throw behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I disagree. It's an artificial restriction for no good reason.
I think we could get around having to use prefixes by using OpenSSL's crypto.createMac('cmac', key, { cipher: 'aes-128-cbc' });
crypto.createMac('hmac', key, { digest: 'sha1' });
crypto.createMac('poly1305', key);
crypto.createMac('siphash', key); |
Also I think using |
lib/internal/crypto/hash.js
Outdated
} | ||
|
||
_flush(callback) { | ||
this.push(this[kHandle].digest()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.push(this[kHandle].digest()); | |
this.push(this[kHandle].final()); |
I still think moving all parameters (e.g. cipher name, digest name) to the object in the third parameter will keeps things neater signature-wise and will be simpler once |
Yes please.
These appear to be the options for the underlying LazyTransform yes? I wouldn't separate those out, I would just use a single crypto.createMac('cmac', key, { cipher: 'aes-128-cbc', highWaterMark: 10 }); then inside the const { highWaterMark } = options;
super({ highWaterMark }); |
It would probably be both, for the stream settings and for the computation parameters. I suggested changing the parameter name in the signature to something else because it may/may not be optional, depending on the type (e.g. at least
That's exactly what I was suggesting. Although you could probably just pass the object as-is to the |
I considered that when I reworked the first draft but I have a mild-to-serious dislike for that approach.
Not sure I follow, can you elaborate? Working with JS objects at the C++ level is a lot more tedious and error-prone than dealing with primitives. edit: not that it really matters. (Lack of) ease of use at the C++ layer should not inform the public API's design. |
I think it's ok to deviate from the other classes in this way because this function is encompassing different types of (MAC) computation with each type requiring different parameters. Also we don't have a precedent for passing context-specific values as separate parameters for any of these other classes. If anything, by keeping all of the context-specific values in the object, it's more like the existing classes by keeping the function signature consistent. Another minor reason to do it this way is it might appease V8 because parameter types wouldn't possibly change across multiple calls. |
334fd34
to
2b1cbe1
Compare
On your head be it. Updated, PTAL. |
Except for missing documentation, LGTM. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good so far but needs
- docs
- expanded tests (invalid arg type coverage, checking for proper
error.code
instead of error message)
Will review again once docs and tests are updated.
Fair point. No strong opinion on my side, but I am not a fan of Node.js allowing to use strings in cryptographic APIs such as |
8ae28ff
to
2935f72
Compare
@bnoordhuis ... is this stalled or still work in progress? |
Can we merge this in its current state? I'm trying to use CMAC in an application and building Node isn't an option. We could add a warning that this is unstable and untested? |
Tried to use this for an I-D implementation. const key = crypto.randomBytes(16)
const mac = crypto.createMac('cmac', key, { cipher: 'aes-128-cbc' })
mac.update(crypto.randomBytes(30))
console.log(mac.final())
// => <Buffer b9> This doesn't seem right to me. |
This issue/PR was marked as stalled, it will be automatically closed in 30 days. If it should remain open, please leave a comment explaining why it should remain open. |
Closing this because it has stalled. Feel free to reopen if this PR is still relevant, or to ping the collaborator who labelled it stalled if you have any questions. |
This should be reopened and support for keyed hashes (i.e. SHA3, BLAKE2) considered in scope. |
@paragonie-security ad blake2 advanced options see #40921. Bottom line, it's not even in openssl as per openssl/openssl#980
|
Per #32448 (comment).
cc @mscdex @tniessen - this doesn't implement the one-shot API yet but, on the flip side, it supports CMACs.