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: https://github.com/nodejs/node/issues/22421

PR-URL: https://github.com/nodejs/node/pull/22538
Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
Tobias Nießen 2018-08-26 19:17:09 +02:00 committed by Anna Henningsen
parent 594dd4242b
commit b4026099c3
No known key found for this signature in database
GPG Key ID: 9C63F3A6CD2AD8F9
3 changed files with 51 additions and 16 deletions

View File

@ -2928,6 +2928,20 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
}
bool CipherBase::MaybePassAuthTagToOpenSSL() {
if (!auth_tag_set_ && auth_tag_len_ != kNoAuthTagLength) {
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(),
EVP_CTRL_AEAD_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_))) {
return false;
}
auth_tag_set_ = true;
}
return true;
}
bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
if (!ctx_ || !IsAuthenticatedMode())
return false;
@ -2947,15 +2961,9 @@ bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) {
if (!CheckCCMMessageLength(plaintext_len))
return false;
if (kind_ == kDecipher && !auth_tag_set_ && auth_tag_len_ > 0 &&
auth_tag_len_ != kNoAuthTagLength) {
if (!EVP_CIPHER_CTX_ctrl(ctx_.get(),
EVP_CTRL_CCM_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_))) {
if (kind_ == kDecipher) {
if (!MaybePassAuthTagToOpenSSL())
return false;
}
auth_tag_set_ = true;
}
// Specify the plaintext length.
@ -3000,14 +3008,10 @@ CipherBase::UpdateResult CipherBase::Update(const char* data,
return kErrorMessageSize;
}
// on first update:
if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_len_ > 0 &&
auth_tag_len_ != kNoAuthTagLength && !auth_tag_set_) {
CHECK(EVP_CIPHER_CTX_ctrl(ctx_.get(),
EVP_CTRL_AEAD_SET_TAG,
auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_)));
auth_tag_set_ = true;
// Pass the authentication tag to OpenSSL if possible. This will only happen
// once, usually on the first update.
if (kind_ == kDecipher && IsAuthenticatedMode()) {
CHECK(MaybePassAuthTagToOpenSSL());
}
*out_len = 0;
@ -3107,6 +3111,10 @@ bool CipherBase::Final(unsigned char** out, int* out_len) {
*out = Malloc<unsigned char>(
static_cast<size_t>(EVP_CIPHER_CTX_block_size(ctx_.get())));
if (kind_ == kDecipher && IsSupportedAuthenticatedMode(mode)) {
MaybePassAuthTagToOpenSSL();
}
// In CCM mode, final() only checks whether authentication failed in update().
// EVP_CipherFinal_ex must not be called and will fail.
bool ok;

View File

@ -385,6 +385,7 @@ class CipherBase : public BaseObject {
bool IsAuthenticatedMode() const;
bool SetAAD(const char* data, unsigned int len, int plaintext_len);
bool MaybePassAuthTagToOpenSSL();
static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);

View File

@ -555,3 +555,29 @@ for (const test of TEST_CASES) {
encrypt.update('boom'); // Should not throw 'Message exceeds maximum size'.
encrypt.final();
}
// Test that the authentication tag can be set at any point before calling
// final() in GCM 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);
}
const resultFinal = decipher.final();
const result = Buffer.concat([resultUpdate, resultFinal]);
assert(result.equals(plain));
}
}