Skip to content

Commit b402609

Browse files
tniessenaddaleax
authored andcommitted
crypto: improve setAuthTag
This is an attempt to make the behavior of setAuthTag match the documentation: In GCM mode, it can be called at any time before invoking final, even after the last call to update. Fixes: nodejs#22421 PR-URL: nodejs#22538 Reviewed-By: Anna Henningsen <anna@addaleax.net>
1 parent 594dd42 commit b402609

File tree

3 files changed

+51
-16
lines changed

3 files changed

+51
-16
lines changed

src/node_crypto.cc

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2928,6 +2928,20 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
29282928
}
29292929

29302930

2931+
bool CipherBase::MaybePassAuthTagToOpenSSL() {
2932+
if (!auth_tag_set_ && auth_tag_len_ != kNoAuthTagLength) {
2933+
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(),
2934+
EVP_CTRL_AEAD_SET_TAG,
2935+
auth_tag_len_,
2936+
reinterpret_cast<unsigned char*>(auth_tag_))) {
2937+
return false;
2938+
}
2939+
auth_tag_set_ = true;
2940+
}
2941+
return true;
2942+
}
2943+
2944+
29312945
bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
29322946
if (!ctx_ || !IsAuthenticatedMode())
29332947
return false;
@@ -2947,15 +2961,9 @@ bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
29472961
if (!CheckCCMMessageLength(plaintext_len))
29482962
return false;
29492963

2950-
if (kind_ == kDecipher && !auth_tag_set_ && auth_tag_len_ > 0 &&
2951-
auth_tag_len_ != kNoAuthTagLength) {
2952-
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(),
2953-
EVP_CTRL_CCM_SET_TAG,
2954-
auth_tag_len_,
2955-
reinterpret_cast<unsigned char*>(auth_tag_))) {
2964+
if (kind_ == kDecipher) {
2965+
if (!MaybePassAuthTagToOpenSSL())
29562966
return false;
2957-
}
2958-
auth_tag_set_ = true;
29592967
}
29602968

29612969
// Specify the plaintext length.
@@ -3000,14 +3008,10 @@ CipherBase::UpdateResult CipherBase::Update(const char* data,
30003008
return kErrorMessageSize;
30013009
}
30023010

3003-
// on first update:
3004-
if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_len_ > 0 &&
3005-
auth_tag_len_ != kNoAuthTagLength && !auth_tag_set_) {
3006-
CHECK(EVP_CIPHER_CTX_ctrl(ctx_.get(),
3007-
EVP_CTRL_AEAD_SET_TAG,
3008-
auth_tag_len_,
3009-
reinterpret_cast<unsigned char*>(auth_tag_)));
3010-
auth_tag_set_ = true;
3011+
// Pass the authentication tag to OpenSSL if possible. This will only happen
3012+
// once, usually on the first update.
3013+
if (kind_ == kDecipher && IsAuthenticatedMode()) {
3014+
CHECK(MaybePassAuthTagToOpenSSL());
30113015
}
30123016

30133017
*out_len = 0;
@@ -3107,6 +3111,10 @@ bool CipherBase::Final(unsigned char** out, int* out_len) {
31073111
*out = Malloc<unsigned char>(
31083112
static_cast<size_t>(EVP_CIPHER_CTX_block_size(ctx_.get())));
31093113

3114+
if (kind_ == kDecipher && IsSupportedAuthenticatedMode(mode)) {
3115+
MaybePassAuthTagToOpenSSL();
3116+
}
3117+
31103118
// In CCM mode, final() only checks whether authentication failed in update().
31113119
// EVP_CipherFinal_ex must not be called and will fail.
31123120
bool ok;

src/node_crypto.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ class CipherBase : public BaseObject {
385385

386386
bool IsAuthenticatedMode() const;
387387
bool SetAAD(const char* data, unsigned int len, int plaintext_len);
388+
bool MaybePassAuthTagToOpenSSL();
388389

389390
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
390391
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);

test/parallel/test-crypto-authenticated.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,3 +555,29 @@ for (const test of TEST_CASES) {
555555
encrypt.update('boom'); // Should not throw 'Message exceeds maximum size'.
556556
encrypt.final();
557557
}
558+
559+
// Test that the authentication tag can be set at any point before calling
560+
// final() in GCM mode.
561+
{
562+
const plain = Buffer.from('Hello world', 'utf8');
563+
const key = Buffer.from('0123456789abcdef', 'utf8');
564+
const iv = Buffer.from('0123456789ab', 'utf8');
565+
566+
const cipher = crypto.createCipheriv('aes-128-gcm', key, iv);
567+
const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]);
568+
const authTag = cipher.getAuthTag();
569+
570+
for (const authTagBeforeUpdate of [true, false]) {
571+
const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv);
572+
if (authTagBeforeUpdate) {
573+
decipher.setAuthTag(authTag);
574+
}
575+
const resultUpdate = decipher.update(ciphertext);
576+
if (!authTagBeforeUpdate) {
577+
decipher.setAuthTag(authTag);
578+
}
579+
const resultFinal = decipher.final();
580+
const result = Buffer.concat([resultUpdate, resultFinal]);
581+
assert(result.equals(plain));
582+
}
583+
}

0 commit comments

Comments
 (0)