From 624e516fb7275d993520aa1898b81645de601aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= Date: Thu, 13 Sep 2018 00:48:35 +0200 Subject: [PATCH] crypto: fix edge case in authenticated encryption Restricting the authentication tag length and calling update or setAAD before setAuthTag caused an incorrect authentication tag to be passed to OpenSSL: The auth_tag_len_ field was already set, so the implementation assumed that the tag itself was known as well. PR-URL: https://github.com/nodejs/node/pull/22828 Reviewed-By: Daniel Bevenius --- src/node_crypto.cc | 9 +++-- src/node_crypto.h | 9 +++-- test/parallel/test-crypto-authenticated.js | 40 +++++++++++++--------- 3 files changed, 38 insertions(+), 20 deletions(-) diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 75aedabf7eac5c..b4a6aaa87dd075 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2891,6 +2891,10 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { return args.GetReturnValue().Set(false); } + // TODO(tniessen): Throw if the authentication tag has already been set. + if (cipher->auth_tag_state_ == kAuthTagPassedToOpenSSL) + return args.GetReturnValue().Set(true); + unsigned int tag_len = Buffer::Length(args[0]); const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_.get()); if (mode == EVP_CIPH_GCM_MODE) { @@ -2923,6 +2927,7 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { // Note: we don't use std::min() here to work around a header conflict. cipher->auth_tag_len_ = tag_len; + cipher->auth_tag_state_ = kAuthTagKnown; if (cipher->auth_tag_len_ > sizeof(cipher->auth_tag_)) cipher->auth_tag_len_ = sizeof(cipher->auth_tag_); @@ -2934,14 +2939,14 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { bool CipherBase::MaybePassAuthTagToOpenSSL() { - if (!auth_tag_set_ && auth_tag_len_ != kNoAuthTagLength) { + if (auth_tag_state_ == kAuthTagKnown) { if (!EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_SET_TAG, auth_tag_len_, reinterpret_cast(auth_tag_))) { return false; } - auth_tag_set_ = true; + auth_tag_state_ = kAuthTagPassedToOpenSSL; } return true; } diff --git a/src/node_crypto.h b/src/node_crypto.h index 86aa3ba4ba8395..1a93ae7a47e2cf 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -363,6 +363,11 @@ class CipherBase : public BaseObject { kErrorMessageSize, kErrorState }; + enum AuthTagState { + kAuthTagUnknown, + kAuthTagKnown, + kAuthTagPassedToOpenSSL + }; static const unsigned kNoAuthTagLength = static_cast(-1); void Init(const char* cipher_type, @@ -404,7 +409,7 @@ class CipherBase : public BaseObject { : BaseObject(env, wrap), ctx_(nullptr), kind_(kind), - auth_tag_set_(false), + auth_tag_state_(kAuthTagUnknown), auth_tag_len_(kNoAuthTagLength), pending_auth_failed_(false) { MakeWeak(); @@ -413,7 +418,7 @@ class CipherBase : public BaseObject { private: DeleteFnPtr ctx_; const CipherKind kind_; - bool auth_tag_set_; + AuthTagState auth_tag_state_; unsigned int auth_tag_len_; char auth_tag_[EVP_GCM_TLS_TAG_LEN]; bool pending_auth_failed_; diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index 4b2d8526ddb46e..dc19a7b2ab81ef 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -579,27 +579,35 @@ for (const test of TEST_CASES) { } // Test that the authentication tag can be set at any point before calling -// final() in GCM mode. +// final() in GCM or OCB mode. { const plain = Buffer.from('Hello world', 'utf8'); const key = Buffer.from('0123456789abcdef', 'utf8'); const iv = Buffer.from('0123456789ab', 'utf8'); - const cipher = crypto.createCipheriv('aes-128-gcm', key, iv); - const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); - const authTag = cipher.getAuthTag(); - - for (const authTagBeforeUpdate of [true, false]) { - const decipher = crypto.createDecipheriv('aes-128-gcm', key, iv); - if (authTagBeforeUpdate) { - decipher.setAuthTag(authTag); - } - const resultUpdate = decipher.update(ciphertext); - if (!authTagBeforeUpdate) { - decipher.setAuthTag(authTag); + for (const mode of ['gcm', 'ocb']) { + for (const authTagLength of mode === 'gcm' ? [undefined, 8] : [8]) { + const cipher = crypto.createCipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + const ciphertext = Buffer.concat([cipher.update(plain), cipher.final()]); + const authTag = cipher.getAuthTag(); + + for (const authTagBeforeUpdate of [true, false]) { + const decipher = crypto.createDecipheriv(`aes-128-${mode}`, key, iv, { + authTagLength + }); + if (authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultUpdate = decipher.update(ciphertext); + if (!authTagBeforeUpdate) { + decipher.setAuthTag(authTag); + } + const resultFinal = decipher.final(); + const result = Buffer.concat([resultUpdate, resultFinal]); + assert(result.equals(plain)); + } } - const resultFinal = decipher.final(); - const result = Buffer.concat([resultUpdate, resultFinal]); - assert(result.equals(plain)); } }