diff --git a/doc/api/crypto.md b/doc/api/crypto.md index a614fc1d0fde37..5064d1a7b267b4 100644 --- a/doc/api/crypto.md +++ b/doc/api/crypto.md @@ -241,17 +241,22 @@ Once the `cipher.final()` method has been called, the `Cipher` object can no longer be used to encrypt data. Attempts to call `cipher.final()` more than once will result in an error being thrown. -### cipher.setAAD(buffer) +### cipher.setAAD(buffer[, options]) - `buffer` {Buffer} +- `options` {object} - Returns the {Cipher} for method chaining. -When using an authenticated encryption mode (only `GCM` is currently +When using an authenticated encryption mode (only `GCM` and `CCM` are currently supported), the `cipher.setAAD()` method sets the value used for the _additional authenticated data_ (AAD) input parameter. +The `options` argument is optional for `GCM`. When using `CCM`, the +`plaintextLength` option must be specified and its value must match the length +of the plaintext in bytes. See [CCM mode][]. + The `cipher.setAAD()` method must be called before [`cipher.update()`][]. ### cipher.getAuthTag() @@ -1312,7 +1317,12 @@ deprecated: REPLACEME - `options` {Object} [`stream.transform` options][] Creates and returns a `Cipher` object that uses the given `algorithm` and -`password`. Optional `options` argument controls stream behavior. +`password`. + +The `options` argument controls stream behavior and is optional except when a +cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the +`authTagLength` option is required and specifies the length of the +authentication tag in bytes, see [CCM mode][]. The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On recent OpenSSL releases, `openssl list-cipher-algorithms` will display the @@ -1353,8 +1363,10 @@ changes: - `options` {Object} [`stream.transform` options][] Creates and returns a `Cipher` object, with the given `algorithm`, `key` and -initialization vector (`iv`). Optional `options` argument controls stream -behavior. +The `options` argument controls stream behavior and is optional except when a +cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the +`authTagLength` option is required and specifies the length of the +authentication tag in bytes, see [CCM mode][]. The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On recent OpenSSL releases, `openssl list-cipher-algorithms` will display the @@ -1396,7 +1408,12 @@ deprecated: REPLACEME - `options` {Object} [`stream.transform` options][] Creates and returns a `Decipher` object that uses the given `algorithm` and -`password` (key). Optional `options` argument controls stream behavior. +`password` (key). + +The `options` argument controls stream behavior and is optional except when a +cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the +`authTagLength` option is required and specifies the length of the +authentication tag in bytes, see [CCM mode][]. The implementation of `crypto.createDecipher()` derives keys using the OpenSSL function [`EVP_BytesToKey`][] with the digest algorithm set to MD5, one @@ -1425,8 +1442,12 @@ changes: - `options` {Object} [`stream.transform` options][] Creates and returns a `Decipher` object that uses the given `algorithm`, `key` -and initialization vector (`iv`). Optional `options` argument controls stream -behavior. +and initialization vector (`iv`). + +The `options` argument controls stream behavior and is optional except when a +cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the +`authTagLength` option is required and specifies the length of the +authentication tag in bytes, see [CCM mode][]. The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On recent OpenSSL releases, `openssl list-cipher-algorithms` will display the @@ -2167,6 +2188,71 @@ Based on the recommendations of [NIST SP 800-131A][]: See the reference for other recommendations and details. +### CCM mode + +CCM is one of the two supported [AEAD algorithms][]. Applications which use this +mode must adhere to certain restrictions when using the cipher API: + +- The authentication tag length must be specified during cipher creation by + setting the `authTagLength` option and must be one of 4, 6, 8, 10, 12, 14 or + 16 bytes. +- The length of the initialization vector (nonce) `N` must be between 7 and 13 + bytes (`7 ≤ N ≤ 13`). +- The length of the plaintext is limited to `2 ** (8 * (15 - N))` bytes. +- When decrypting, the authentication tag must be set via `setAuthTag()` before + specifying additional authenticated data and / or calling `update()`. + Otherwise, decryption will fail and `final()` will throw an error in + compliance with section 2.6 of [RFC 3610][]. +- Using stream methods such as `write(data)`, `end(data)` or `pipe()` in CCM + mode might fail as CCM cannot handle more than one chunk of data per instance. +- When passing additional authenticated data (AAD), the length of the actual + message in bytes must be passed to `setAAD()` via the `plaintextLength` + option. This is not necessary if no AAD is used. +- As CCM processes the whole message at once, `update()` can only be called + once. +- Even though calling `update()` is sufficient to encrypt / decrypt the message, + applications *must* call `final()` to compute and / or verify the + authentication tag. + +```js +const crypto = require('crypto'); + +const key = 'keykeykeykeykeykeykeykey'; +const nonce = crypto.randomBytes(12); + +const aad = Buffer.from('0123456789', 'hex'); + +const cipher = crypto.createCipheriv('aes-192-ccm', key, nonce, { + authTagLength: 16 +}); +const plaintext = 'Hello world'; +cipher.setAAD(aad, { + plaintextLength: Buffer.byteLength(plaintext) +}); +const ciphertext = cipher.update(plaintext, 'utf8'); +cipher.final(); +const tag = cipher.getAuthTag(); + +// Now transmit { ciphertext, tag }. + +const decipher = crypto.createDecipheriv('aes-192-ccm', key, nonce, { + authTagLength: 16 +}); +decipher.setAuthTag(tag); +decipher.setAAD(aad, { + plaintextLength: ciphertext.length +}); +const receivedPlaintext = decipher.update(ciphertext, null, 'utf8'); + +try { + decipher.final(); +} catch (err) { + console.error('Authentication failed!'); +} + +console.log(receivedPlaintext); +``` + ## Crypto Constants The following constants exported by `crypto.constants` apply to various uses of @@ -2525,7 +2611,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [`tls.createSecureContext()`]: tls.html#tls_tls_createsecurecontext_options [`verify.update()`]: #crypto_verify_update_data_inputencoding [`verify.verify()`]: #crypto_verify_verify_object_signature_signatureformat +[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption [Caveats]: #crypto_support_for_weak_or_compromised_algorithms +[CCM mode]: #crypto_ccm_mode [Crypto Constants]: #crypto_crypto_constants_1 [HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed [HTML5's `keygen` element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen @@ -2536,6 +2624,7 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL. [OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.0.2/apps/spkac.html [RFC 2412]: https://www.rfc-editor.org/rfc/rfc2412.txt [RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt +[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt [RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt [initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector [stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback diff --git a/lib/internal/crypto/cipher.js b/lib/internal/crypto/cipher.js index cd8297245fe459..d33a970148f52a 100644 --- a/lib/internal/crypto/cipher.js +++ b/lib/internal/crypto/cipher.js @@ -7,7 +7,8 @@ const { const { ERR_CRYPTO_INVALID_STATE, - ERR_INVALID_ARG_TYPE + ERR_INVALID_ARG_TYPE, + ERR_INVALID_OPT_VALUE } = require('internal/errors').codes; const { @@ -62,6 +63,16 @@ function getDecoder(decoder, encoding) { return decoder; } +function getUIntOption(options, key) { + let value; + if (options && (value = options[key]) != null) { + if (value >>> 0 !== value) + throw new ERR_INVALID_OPT_VALUE(key, value); + return value; + } + return -1; +} + function Cipher(cipher, password, options) { if (!(this instanceof Cipher)) return new Cipher(cipher, password, options); @@ -78,9 +89,11 @@ function Cipher(cipher, password, options) { ); } + const authTagLength = getUIntOption(options, 'authTagLength'); + this._handle = new CipherBase(true); - this._handle.init(cipher, password); + this._handle.init(cipher, password, authTagLength); this._decoder = null; LazyTransform.call(this, options); @@ -168,13 +181,15 @@ Cipher.prototype.setAuthTag = function setAuthTag(tagbuf) { return this; }; -Cipher.prototype.setAAD = function setAAD(aadbuf) { +Cipher.prototype.setAAD = function setAAD(aadbuf, options) { if (!isArrayBufferView(aadbuf)) { throw new ERR_INVALID_ARG_TYPE('buffer', ['Buffer', 'TypedArray', 'DataView'], aadbuf); } - if (this._handle.setAAD(aadbuf) === false) + + const plaintextLength = getUIntOption(options, 'plaintextLength'); + if (this._handle.setAAD(aadbuf, plaintextLength) === false) throw new ERR_CRYPTO_INVALID_STATE('setAAD'); return this; }; @@ -204,8 +219,10 @@ function Cipheriv(cipher, key, iv, options) { ); } + const authTagLength = getUIntOption(options, 'authTagLength'); + this._handle = new CipherBase(true); - this._handle.initiv(cipher, key, iv); + this._handle.initiv(cipher, key, iv, authTagLength); this._decoder = null; LazyTransform.call(this, options); @@ -243,8 +260,10 @@ function Decipher(cipher, password, options) { ); } + const authTagLength = getUIntOption(options, 'authTagLength'); + this._handle = new CipherBase(false); - this._handle.init(cipher, password); + this._handle.init(cipher, password, authTagLength); this._decoder = null; LazyTransform.call(this, options); @@ -288,8 +307,10 @@ function Decipheriv(cipher, key, iv, options) { ); } + const authTagLength = getUIntOption(options, 'authTagLength'); + this._handle = new CipherBase(false); - this._handle.initiv(cipher, key, iv); + this._handle.initiv(cipher, key, iv, authTagLength); this._decoder = null; LazyTransform.call(this, options); diff --git a/src/node_crypto.cc b/src/node_crypto.cc index 57dbe6861dc9cd..52673b59da6f31 100644 --- a/src/node_crypto.cc +++ b/src/node_crypto.cc @@ -2802,7 +2802,8 @@ void CipherBase::New(const FunctionCallbackInfo& args) { void CipherBase::Init(const char* cipher_type, const char* key_buf, - int key_buf_len) { + int key_buf_len, + int auth_tag_len) { HandleScope scope(env()->isolate()); #ifdef NODE_FIPS_MODE @@ -2847,6 +2848,12 @@ void CipherBase::Init(const char* cipher_type, if (mode == EVP_CIPH_WRAP_MODE) EVP_CIPHER_CTX_set_flags(ctx_, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); + if (IsAuthenticatedMode()) { + if (!InitAuthenticated(cipher_type, EVP_CIPHER_iv_length(cipher), + auth_tag_len)) + return; + } + CHECK_EQ(1, EVP_CIPHER_CTX_set_key_length(ctx_, key_len)); EVP_CipherInit_ex(ctx_, @@ -2862,12 +2869,17 @@ void CipherBase::Init(const FunctionCallbackInfo& args) { CipherBase* cipher; ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - CHECK_GE(args.Length(), 2); + CHECK_GE(args.Length(), 3); const node::Utf8Value cipher_type(args.GetIsolate(), args[0]); const char* key_buf = Buffer::Data(args[1]); ssize_t key_buf_len = Buffer::Length(args[1]); - cipher->Init(*cipher_type, key_buf, key_buf_len); + CHECK(args[2]->IsInt32()); + // Don't assign to cipher->auth_tag_len_ directly; the value might not + // represent a valid length at this point. + int auth_tag_len = args[2].As()->Value(); + + cipher->Init(*cipher_type, key_buf, key_buf_len, auth_tag_len); } @@ -2875,7 +2887,8 @@ void CipherBase::InitIv(const char* cipher_type, const char* key, int key_len, const char* iv, - int iv_len) { + int iv_len, + int auth_tag_len) { HandleScope scope(env()->isolate()); const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); @@ -2886,6 +2899,7 @@ void CipherBase::InitIv(const char* cipher_type, const int expected_iv_len = EVP_CIPHER_iv_length(cipher); const int mode = EVP_CIPHER_mode(cipher); const bool is_gcm_mode = (EVP_CIPH_GCM_MODE == mode); + const bool is_ccm_mode = (EVP_CIPH_CCM_MODE == mode); const bool has_iv = iv_len >= 0; // Throw if no IV was passed and the cipher requires an IV @@ -2896,7 +2910,7 @@ void CipherBase::InitIv(const char* cipher_type, } // Throw if an IV was passed which does not match the cipher's fixed IV length - if (is_gcm_mode == false && has_iv && iv_len != expected_iv_len) { + if (!is_gcm_mode && !is_ccm_mode && has_iv && iv_len != expected_iv_len) { return env()->ThrowError("Invalid IV length"); } @@ -2908,13 +2922,10 @@ void CipherBase::InitIv(const char* cipher_type, const bool encrypt = (kind_ == kCipher); EVP_CipherInit_ex(ctx_, cipher, nullptr, nullptr, nullptr, encrypt); - if (is_gcm_mode) { + if (IsAuthenticatedMode()) { CHECK(has_iv); - if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) { - EVP_CIPHER_CTX_free(ctx_); - ctx_ = nullptr; - return env()->ThrowError("Invalid IV length"); - } + if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len)) + return; } if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { @@ -2937,7 +2948,7 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); Environment* env = cipher->env(); - CHECK_GE(args.Length(), 3); + CHECK_GE(args.Length(), 4); const node::Utf8Value cipher_type(env->isolate(), args[0]); ssize_t key_len = Buffer::Length(args[1]); @@ -2951,16 +2962,84 @@ void CipherBase::InitIv(const FunctionCallbackInfo& args) { iv_buf = Buffer::Data(args[2]); iv_len = Buffer::Length(args[2]); } - cipher->InitIv(*cipher_type, key_buf, key_len, iv_buf, iv_len); + CHECK(args[3]->IsInt32()); + // Don't assign to cipher->auth_tag_len_ directly; the value might not + // represent a valid length at this point. + int auth_tag_len = args[3].As()->Value(); + + cipher->InitIv(*cipher_type, key_buf, key_len, iv_buf, iv_len, auth_tag_len); +} + + +bool CipherBase::InitAuthenticated(const char *cipher_type, int iv_len, + int auth_tag_len) { + CHECK(IsAuthenticatedMode()); + + // TODO(tniessen) Use EVP_CTRL_AEAD_SET_IVLEN when migrating to OpenSSL 1.1.0 + static_assert(EVP_CTRL_CCM_SET_IVLEN == EVP_CTRL_GCM_SET_IVLEN, + "OpenSSL constants differ between GCM and CCM"); + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) { + env()->ThrowError("Invalid IV length"); + return false; + } + + if (EVP_CIPHER_CTX_mode(ctx_) == EVP_CIPH_CCM_MODE) { + if (auth_tag_len < 0) { + char msg[128]; + snprintf(msg, sizeof(msg), "authTagLength required for %s", cipher_type); + env()->ThrowError(msg); + return false; + } + +#ifdef NODE_FIPS_MODE + // TODO(tniessen) Support CCM decryption in FIPS mode + if (kind_ == kDecipher && FIPS_mode()) { + env()->ThrowError("CCM decryption not supported in FIPS mode"); + return false; + } +#endif + + if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_CCM_SET_TAG, auth_tag_len, + nullptr)) { + env()->ThrowError("Invalid authentication tag length"); + return false; + } + + // When decrypting in CCM mode, this field will be set in setAuthTag(). + if (kind_ == kCipher) + auth_tag_len_ = auth_tag_len; + + // The message length is restricted to 2 ^ (8 * (15 - iv_len)) - 1 bytes. + CHECK(iv_len >= 7 && iv_len <= 13); + if (iv_len >= static_cast(15.5 - log2(INT_MAX + 1.) / 8)) { + max_message_size_ = (1 << (8 * (15 - iv_len))) - 1; + } else { + max_message_size_ = INT_MAX; + } + } + + return true; +} + + +bool CipherBase::CheckCCMMessageLength(int message_len) { + CHECK_NE(ctx_, nullptr); + CHECK(EVP_CIPHER_CTX_mode(ctx_) == EVP_CIPH_CCM_MODE); + + if (message_len > max_message_size_) { + env()->ThrowError("Message exceeds maximum size"); + return false; + } + + return true; } bool CipherBase::IsAuthenticatedMode() const { // Check if this cipher operates in an AEAD mode that we support. CHECK_NE(ctx_, nullptr); - const EVP_CIPHER* const cipher = EVP_CIPHER_CTX_cipher(ctx_); - int mode = EVP_CIPHER_mode(cipher); - return mode == EVP_CIPH_GCM_MODE; + const int mode = EVP_CIPHER_CTX_mode(ctx_); + return mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_CCM_MODE; } @@ -2995,12 +3074,15 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { // Restrict GCM tag lengths according to NIST 800-38d, page 9. unsigned int tag_len = Buffer::Length(args[0]); - if (tag_len > 16 || (tag_len < 12 && tag_len != 8 && tag_len != 4)) { - char msg[125]; - snprintf(msg, sizeof(msg), - "Permitting authentication tag lengths of %u bytes is deprecated. " - "Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16.", tag_len); - ProcessEmitDeprecationWarning(cipher->env(), msg, "DEP0090"); + const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_); + if (mode == EVP_CIPH_GCM_MODE) { + if (tag_len > 16 || (tag_len < 12 && tag_len != 8 && tag_len != 4)) { + char msg[125]; + snprintf(msg, sizeof(msg), + "Permitting authentication tag lengths of %u bytes is deprecated. " + "Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16.", tag_len); + ProcessEmitDeprecationWarning(cipher->env(), msg, "DEP0090"); + } } // Note: we don't use std::max() here to work around a header conflict. @@ -3013,18 +3095,44 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo& args) { } -bool CipherBase::SetAAD(const char* data, unsigned int len) { +bool CipherBase::SetAAD(const char* data, unsigned int len, int plaintext_len) { if (ctx_ == nullptr || !IsAuthenticatedMode()) return false; + int outlen; - if (!EVP_CipherUpdate(ctx_, - nullptr, - &outlen, - reinterpret_cast(data), - len)) { - return false; + const int mode = EVP_CIPHER_CTX_mode(ctx_); + + // When in CCM mode, we need to set the authentication tag and the plaintext + // length in advance. + if (mode == EVP_CIPH_CCM_MODE) { + if (plaintext_len < 0) { + env()->ThrowError("plaintextLength required for CCM mode with AAD"); + return false; + } + + if (!CheckCCMMessageLength(plaintext_len)) + return false; + + if (kind_ == kDecipher && !auth_tag_set_ && auth_tag_len_ > 0) { + if (!EVP_CIPHER_CTX_ctrl(ctx_, + EVP_CTRL_CCM_SET_TAG, + auth_tag_len_, + reinterpret_cast(auth_tag_))) { + return false; + } + auth_tag_set_ = true; + } + + // Specify the plaintext length. + if (!EVP_CipherUpdate(ctx_, nullptr, &outlen, nullptr, plaintext_len)) + return false; } - return true; + + return 1 == EVP_CipherUpdate(ctx_, + nullptr, + &outlen, + reinterpret_cast(data), + len); } @@ -3032,34 +3140,55 @@ void CipherBase::SetAAD(const FunctionCallbackInfo& args) { CipherBase* cipher; ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); - if (!cipher->SetAAD(Buffer::Data(args[0]), Buffer::Length(args[0]))) + CHECK_EQ(args.Length(), 2); + CHECK(args[1]->IsInt32()); + int plaintext_len = args[1].As()->Value(); + + if (!cipher->SetAAD(Buffer::Data(args[0]), Buffer::Length(args[0]), + plaintext_len)) args.GetReturnValue().Set(false); // Report invalid state failure } -bool CipherBase::Update(const char* data, - int len, - unsigned char** out, - int* out_len) { +CipherBase::UpdateResult CipherBase::Update(const char* data, + int len, + unsigned char** out, + int* out_len) { if (ctx_ == nullptr) - return false; + return kErrorState; + + const int mode = EVP_CIPHER_CTX_mode(ctx_); + + if (mode == EVP_CIPH_CCM_MODE) { + if (!CheckCCMMessageLength(len)) + return kErrorMessageSize; + } // on first update: - if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_len_ > 0) { + if (kind_ == kDecipher && IsAuthenticatedMode() && auth_tag_len_ > 0 && + !auth_tag_set_) { EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_TAG, auth_tag_len_, reinterpret_cast(auth_tag_)); - auth_tag_len_ = 0; + auth_tag_set_ = true; } *out_len = len + EVP_CIPHER_CTX_block_size(ctx_); *out = Malloc(static_cast(*out_len)); - return EVP_CipherUpdate(ctx_, - *out, - out_len, - reinterpret_cast(data), - len); + int r = EVP_CipherUpdate(ctx_, + *out, + out_len, + reinterpret_cast(data), + len); + + // When in CCM mode, EVP_CipherUpdate will fail if the authentication tag is + // invalid. In that case, remember the error and throw in final(). + if (!r && kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { + pending_auth_failed_ = true; + return kSuccess; + } + return r == 1 ? kSuccess : kErrorState; } @@ -3070,7 +3199,7 @@ void CipherBase::Update(const FunctionCallbackInfo& args) { ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); unsigned char* out = nullptr; - bool r; + UpdateResult r; int out_len = 0; // Only copy the data if we have to, because it's a string @@ -3085,11 +3214,13 @@ void CipherBase::Update(const FunctionCallbackInfo& args) { r = cipher->Update(buf, buflen, &out, &out_len); } - if (!r) { + if (r != kSuccess) { free(out); - return ThrowCryptoError(env, - ERR_get_error(), - "Trying to add data in unsupported state"); + if (r == kErrorState) { + ThrowCryptoError(env, ERR_get_error(), + "Trying to add data in unsupported state"); + } + return; } CHECK(out != nullptr || out_len == 0); @@ -3120,21 +3251,36 @@ bool CipherBase::Final(unsigned char** out, int *out_len) { if (ctx_ == nullptr) return false; + const int mode = EVP_CIPHER_CTX_mode(ctx_); + *out = Malloc( static_cast(EVP_CIPHER_CTX_block_size(ctx_))); - int r = EVP_CipherFinal_ex(ctx_, *out, out_len); - if (r == 1 && kind_ == kCipher && IsAuthenticatedMode()) { - auth_tag_len_ = sizeof(auth_tag_); - r = EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_GET_TAG, auth_tag_len_, - reinterpret_cast(auth_tag_)); - CHECK_EQ(r, 1); + // In CCM mode, final() only checks whether authentication failed in update(). + // EVP_CipherFinal_ex must not be called and will fail. + bool ok; + if (kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) { + ok = !pending_auth_failed_; + } else { + ok = EVP_CipherFinal_ex(ctx_, *out, out_len) == 1; + + if (ok && kind_ == kCipher && IsAuthenticatedMode()) { + // For GCM, the tag length is static (16 bytes), while the CCM tag length + // must be specified in advance. + if (mode == EVP_CIPH_GCM_MODE) + auth_tag_len_ = sizeof(auth_tag_); + // TOOD(tniessen) Use EVP_CTRL_AEAP_GET_TAG in OpenSSL 1.1.0 + static_assert(EVP_CTRL_CCM_GET_TAG == EVP_CTRL_GCM_GET_TAG, + "OpenSSL constants differ between GCM and CCM"); + CHECK_EQ(1, EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_GET_TAG, auth_tag_len_, + reinterpret_cast(auth_tag_))); + } } EVP_CIPHER_CTX_free(ctx_); ctx_ = nullptr; - return r == 1; + return ok; } diff --git a/src/node_crypto.h b/src/node_crypto.h index c8cf558d6089c3..2f7c904ee9212d 100644 --- a/src/node_crypto.h +++ b/src/node_crypto.h @@ -354,19 +354,31 @@ class CipherBase : public BaseObject { kCipher, kDecipher }; + enum UpdateResult { + kSuccess, + kErrorMessageSize, + kErrorState + }; - void Init(const char* cipher_type, const char* key_buf, int key_buf_len); + void Init(const char* cipher_type, + const char* key_buf, + int key_buf_len, + int auth_tag_len); void InitIv(const char* cipher_type, const char* key, int key_len, const char* iv, - int iv_len); - bool Update(const char* data, int len, unsigned char** out, int* out_len); + int iv_len, + int auth_tag_len); + bool InitAuthenticated(const char *cipher_type, int iv_len, int auth_tag_len); + bool CheckCCMMessageLength(int message_len); + UpdateResult Update(const char* data, int len, unsigned char** out, + int* out_len); bool Final(unsigned char** out, int *out_len); bool SetAutoPadding(bool auto_padding); bool IsAuthenticatedMode() const; - bool SetAAD(const char* data, unsigned int len); + bool SetAAD(const char* data, unsigned int len, int plaintext_len); static void New(const v8::FunctionCallbackInfo& args); static void Init(const v8::FunctionCallbackInfo& args); @@ -385,15 +397,20 @@ class CipherBase : public BaseObject { : BaseObject(env, wrap), ctx_(nullptr), kind_(kind), - auth_tag_len_(0) { + auth_tag_set_(false), + auth_tag_len_(0), + pending_auth_failed_(false) { MakeWeak(this); } private: EVP_CIPHER_CTX* ctx_; const CipherKind kind_; + bool auth_tag_set_; unsigned int auth_tag_len_; char auth_tag_[EVP_GCM_TLS_TAG_LEN]; + bool pending_auth_failed_; + int max_message_size_; }; class Hmac : public BaseObject { diff --git a/test/parallel/test-crypto-authenticated.js b/test/parallel/test-crypto-authenticated.js index 1bc2a372a50bb3..73e3a23f72ac81 100644 --- a/test/parallel/test-crypto-authenticated.js +++ b/test/parallel/test-crypto-authenticated.js @@ -323,6 +323,182 @@ const TEST_CASES = [ '0fc0c3b780f244452da3ebf1c5d82cde' + 'a2418997200ef82e44ae7e3f', tag: 'a44a8266ee1c8eb0c8b5d4cf5ae9f19a', tampered: false }, + + // The following test cases for AES-CCM are from RFC3610 + + // Packet Vector #1 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000003020100a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e', + aad: '0001020304050607', + plainIsHex: true, + ct: '588c979a61c663d2f066d0c2c0f989806d5f6b61dac384', + tag: '17e8d12cfdf926e0' + }, + + // Packet Vector #2 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000004030201a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', + aad: '0001020304050607', + plainIsHex: true, + ct: '72c91a36e135f8cf291ca894085c87e3cc15c439c9e43a3b', + tag: 'a091d56e10400916' + }, + + // Packet Vector #3 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000005040302a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20', + aad: '0001020304050607', + plainIsHex: true, + ct: '51b1e5f44a197d1da46b0f8e2d282ae871e838bb64da859657', + tag: '4adaa76fbd9fb0c5' + }, + + // Packet Vector #4 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000006050403a0a1a2a3a4a5', + plain: '0c0d0e0f101112131415161718191a1b1c1d1e', + aad: '000102030405060708090a0b', + plainIsHex: true, + ct: 'a28c6865939a9a79faaa5c4c2a9d4a91cdac8c', + tag: '96c861b9c9e61ef1' + }, + + // Packet Vector #5 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000007060504a0a1a2a3a4a5', + plain: '0c0d0e0f101112131415161718191a1b1c1d1e1f', + aad: '000102030405060708090a0b', + plainIsHex: true, + ct: 'dcf1fb7b5d9e23fb9d4e131253658ad86ebdca3e', + tag: '51e83f077d9c2d93' + }, + + // Packet Vector #6 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000008070605a0a1a2a3a4a5', + plain: '0c0d0e0f101112131415161718191a1b1c1d1e1f20', + aad: '000102030405060708090a0b', + plainIsHex: true, + ct: '6fc1b011f006568b5171a42d953d469b2570a4bd87', + tag: '405a0443ac91cb94' + }, + + // Packet Vector #7 + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000009080706a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e', + aad: '0001020304050607', + plainIsHex: true, + ct: '0135d1b2c95f41d5d1d4fec185d166b8094e999dfed96c', + tag: '048c56602c97acbb7490' + }, + + // Packet Vector #7 with invalid authentication tag + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000009080706a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e', + aad: '0001020304050607', + plainIsHex: true, + ct: '0135d1b2c95f41d5d1d4fec185d166b8094e999dfed96c', + tag: '048c56602c97acbb7491', + tampered: true + }, + + // Packet Vector #7 with invalid ciphertext + { + algo: 'aes-128-ccm', + key: 'c0c1c2c3c4c5c6c7c8c9cacbcccdcecf', + iv: '00000009080706a0a1a2a3a4a5', + plain: '08090a0b0c0d0e0f101112131415161718191a1b1c1d1e', + aad: '0001020304050607', + plainIsHex: true, + ct: '0135d1b2c95f41d5d1d4fec185d166b8094e999dfed96d', + tag: '048c56602c97acbb7490', + tampered: true + }, + + // Test case for CCM with a password using create(C|Dec)ipher + { + algo: 'aes-192-ccm', + key: '1ed2233fa2223ef5d7df08546049406c7305220bca40d4c9', + iv: '0e1791e9db3bd21a9122c416', + plain: 'Hello node.js world!', + password: 'very bad password', + aad: '63616c76696e', + ct: '49d2c2bd4892703af2f25db04cbe00e703d6d5ac', + tag: '693c21ce212564fc3a6f', + tampered: false + }, + + // Test case for CCM with a password using create(C|Dec)ipher, invalid tag + { + algo: 'aes-192-ccm', + key: '1ed2233fa2223ef5d7df08546049406c7305220bca40d4c9', + iv: '0e1791e9db3bd21a9122c416', + plain: 'Hello node.js world!', + password: 'very bad password', + aad: '63616c76696e', + ct: '49d2c2bd4892703af2f25db04cbe00e703d6d5ac', + tag: '693c21ce212564fc3a6e', + tampered: true + }, + + // Same test with a 128-bit key + { + algo: 'aes-128-ccm', + key: '1ed2233fa2223ef5d7df08546049406c', + iv: '7305220bca40d4c90e1791e9', + plain: 'Hello node.js world!', + password: 'very bad password', + aad: '63616c76696e', + ct: '8beba09d4d4d861f957d51c0794f4abf8030848e', + tag: '0d9bcd142a94caf3d1dd', + tampered: false + }, + + // Test case for CCM without any AAD + { + algo: 'aes-128-ccm', + key: '1ed2233fa2223ef5d7df08546049406c', + iv: '7305220bca40d4c90e1791e9', + plain: 'Hello node.js world!', + password: 'very bad password', + ct: '8beba09d4d4d861f957d51c0794f4abf8030848e', + tag: '29d71a70bb58dae1425d', + tampered: false + }, + + // Test case for CCM with an empty message + { + algo: 'aes-128-ccm', + key: '1ed2233fa2223ef5d7df08546049406c', + iv: '7305220bca40d4c90e1791e9', + plain: '', + password: 'very bad password', + aad: '63616c76696e', + ct: '', + tag: '65a6002b2cdfe9f00027f839332ca6fc', + tampered: false + }, ]; const errMessages = { @@ -330,13 +506,33 @@ const errMessages = { state: / state/, FIPS: /not supported in FIPS mode/, length: /Invalid IV length/, + authTagLength: /Invalid authentication tag length/ }; const ciphers = crypto.getCiphers(); const expectedWarnings = common.hasFipsCrypto ? - [] : [['Use Cipheriv for counter mode of aes-192-gcm', - common.noWarnCode]]; + [] : [ + ['Use Cipheriv for counter mode of aes-192-gcm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-192-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-192-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-128-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-128-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-128-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode], + ['Use Cipheriv for counter mode of aes-256-ccm', common.noWarnCode] + ]; const expectedDeprecationWarnings = [0, 1, 2, 6, 9, 10, 11, 17] .map((i) => [`Permitting authentication tag lengths of ${i} bytes is ` + @@ -362,14 +558,30 @@ for (const test of TEST_CASES) { continue; } + const isCCM = /^aes-(128|192|256)-ccm$/.test(test.algo); + + let options; + if (isCCM) + options = { authTagLength: test.tag.length / 2 }; + + const inputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + + let aadOptions; + if (isCCM) { + aadOptions = { + plaintextLength: Buffer.from(test.plain, inputEncoding).length + }; + } + { const encrypt = crypto.createCipheriv(test.algo, Buffer.from(test.key, 'hex'), - Buffer.from(test.iv, 'hex')); + Buffer.from(test.iv, 'hex'), + options); + if (test.aad) - encrypt.setAAD(Buffer.from(test.aad, 'hex')); + encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); - const inputEncoding = test.plainIsHex ? 'hex' : 'ascii'; let hex = encrypt.update(test.plain, inputEncoding, 'hex'); hex += encrypt.final('hex'); @@ -382,22 +594,32 @@ for (const test of TEST_CASES) { } { - const decrypt = crypto.createDecipheriv(test.algo, - Buffer.from(test.key, 'hex'), - Buffer.from(test.iv, 'hex')); - decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); - if (test.aad) - decrypt.setAAD(Buffer.from(test.aad, 'hex')); + if (isCCM && common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + }, errMessages.FIPS); + } else { + const decrypt = crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); + if (test.aad) + decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); - const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; + const outputEncoding = test.plainIsHex ? 'hex' : 'ascii'; - let msg = decrypt.update(test.ct, 'hex', outputEncoding); - if (!test.tampered) { - msg += decrypt.final(outputEncoding); - assert.strictEqual(msg, test.plain); - } else { - // assert that final throws if input data could not be verified! - assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); + let msg = decrypt.update(test.ct, 'hex', outputEncoding); + if (!test.tampered) { + msg += decrypt.final(outputEncoding); + assert.strictEqual(msg, test.plain); + } else { + // assert that final throws if input data could not be verified! + assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); + } } } @@ -406,9 +628,9 @@ for (const test of TEST_CASES) { assert.throws(() => { crypto.createCipher(test.algo, test.password); }, errMessages.FIPS); } else { - const encrypt = crypto.createCipher(test.algo, test.password); + const encrypt = crypto.createCipher(test.algo, test.password, options); if (test.aad) - encrypt.setAAD(Buffer.from(test.aad, 'hex')); + encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); let hex = encrypt.update(test.plain, 'ascii', 'hex'); hex += encrypt.final('hex'); const auth_tag = encrypt.getAuthTag(); @@ -425,10 +647,10 @@ for (const test of TEST_CASES) { assert.throws(() => { crypto.createDecipher(test.algo, test.password); }, errMessages.FIPS); } else { - const decrypt = crypto.createDecipher(test.algo, test.password); + const decrypt = crypto.createDecipher(test.algo, test.password, options); decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); if (test.aad) - decrypt.setAAD(Buffer.from(test.aad, 'hex')); + decrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions); let msg = decrypt.update(test.ct, 'hex', 'ascii'); if (!test.tampered) { msg += decrypt.final('ascii'); @@ -444,7 +666,8 @@ for (const test of TEST_CASES) { // trying to get tag before inputting all data: const encrypt = crypto.createCipheriv(test.algo, Buffer.from(test.key, 'hex'), - Buffer.from(test.iv, 'hex')); + Buffer.from(test.iv, 'hex'), + options); encrypt.update('blah', 'ascii'); assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state); } @@ -453,17 +676,21 @@ for (const test of TEST_CASES) { // trying to set tag on encryption object: const encrypt = crypto.createCipheriv(test.algo, Buffer.from(test.key, 'hex'), - Buffer.from(test.iv, 'hex')); + Buffer.from(test.iv, 'hex'), + options); assert.throws(() => { encrypt.setAuthTag(Buffer.from(test.tag, 'hex')); }, errMessages.state); } { - // trying to read tag from decryption object: - const decrypt = crypto.createDecipheriv(test.algo, - Buffer.from(test.key, 'hex'), - Buffer.from(test.iv, 'hex')); - assert.throws(function() { decrypt.getAuthTag(); }, errMessages.state); + if (!isCCM || !common.hasFipsCrypto) { + // trying to read tag from decryption object: + const decrypt = crypto.createDecipheriv(test.algo, + Buffer.from(test.key, 'hex'), + Buffer.from(test.iv, 'hex'), + options); + assert.throws(function() { decrypt.getAuthTag(); }, errMessages.state); + } } { @@ -501,3 +728,223 @@ for (const test of TEST_CASES) { decrypt.setAuthTag(Buffer.from('1'.repeat(length))); } } + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM and an invalid +// authentication tag length has been specified. +{ + for (const authTagLength of [-1, true, false, NaN, 5.5]) { + common.expectsError(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${authTagLength}" is invalid for option ` + + '"authTagLength"' + }); + + common.expectsError(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${authTagLength}" is invalid for option ` + + '"authTagLength"' + }); + + if (!common.hasFipsCrypto) { + common.expectsError(() => { + crypto.createCipher('aes-256-ccm', 'bad password', { authTagLength }); + }, { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${authTagLength}" is invalid for option ` + + '"authTagLength"' + }); + + common.expectsError(() => { + crypto.createDecipher('aes-256-ccm', 'bad password', { authTagLength }); + }, { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${authTagLength}" is invalid for option ` + + '"authTagLength"' + }); + } + } + + // The following values will not be caught by the JS layer and thus will not + // use the default error codes. + for (const authTagLength of [0, 1, 2, 3, 5, 7, 9, 11, 13, 15, 17, 18]) { + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength + }); + }, errMessages.authTagLength); + + assert.throws(() => { + crypto.createCipher('aes-256-ccm', 'bad password', { authTagLength }); + }, errMessages.authTagLength); + + assert.throws(() => { + crypto.createDecipher('aes-256-ccm', 'bad password', { authTagLength }); + }, errMessages.authTagLength); + } + } +} + +// Test that create(De|C)ipher(iv)? throws if the mode is CCM and no +// authentication tag has been specified. +{ + assert.throws(() => { + crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, /^Error: authTagLength required for aes-256-ccm$/); + + // CCM decryption and create(De|C)ipher are unsupported in FIPS mode. + if (!common.hasFipsCrypto) { + assert.throws(() => { + crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S'); + }, /^Error: authTagLength required for aes-256-ccm$/); + + assert.throws(() => { + crypto.createCipher('aes-256-ccm', 'very bad password'); + }, /^Error: authTagLength required for aes-256-ccm$/); + + assert.throws(() => { + crypto.createDecipher('aes-256-ccm', 'very bad password'); + }, /^Error: authTagLength required for aes-256-ccm$/); + } +} + +// Test that setAAD throws if an invalid plaintext length has been specified. +{ + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + + for (const plaintextLength of [-1, true, false, NaN, 5.5]) { + common.expectsError(() => { + cipher.setAAD(Buffer.from('0123456789', 'hex'), { plaintextLength }); + }, { + type: TypeError, + code: 'ERR_INVALID_OPT_VALUE', + message: `The value "${plaintextLength}" is invalid for option ` + + '"plaintextLength"' + }); + } +} + +// Test that setAAD and update throw if the plaintext is too long. +{ + for (const ivLength of [13, 12]) { + const maxMessageSize = (1 << (8 * (15 - ivLength))) - 1; + const key = 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8'; + const cipher = () => crypto.createCipheriv('aes-256-ccm', key, + '0'.repeat(ivLength), + { + authTagLength: 10 + }); + + assert.throws(() => { + cipher().setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + 1 + }); + }, /^Error: Message exceeds maximum size$/); + + const msg = Buffer.alloc(maxMessageSize + 1); + assert.throws(() => { + cipher().update(msg); + }, /^Error: Message exceeds maximum size$/); + + const c = cipher(); + c.setAAD(Buffer.alloc(0), { + plaintextLength: maxMessageSize + }); + c.update(msg.slice(1)); + } +} + +// Test that setAAD throws if the mode is CCM and the plaintext length has not +// been specified. +{ + assert.throws(() => { + const cipher = crypto.createCipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /^Error: plaintextLength required for CCM mode with AAD$/); + + if (!common.hasFipsCrypto) { + assert.throws(() => { + const cipher = crypto.createDecipheriv('aes-256-ccm', + 'FxLKsqdmv0E9xrQhp0b1ZgI0K7JFZJM8', + 'qkuZpJWCewa6S', + { + authTagLength: 10 + }); + cipher.setAAD(Buffer.from('0123456789', 'hex')); + }, /^Error: plaintextLength required for CCM mode with AAD$/); + } +} + +// Test that setAAD throws in CCM mode when no authentication tag is provided. +{ + if (!common.hasFipsCrypto) { + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('7305220bca40d4c90e1791e9', 'hex'); + const ct = Buffer.from('8beba09d4d4d861f957d51c0794f4abf8030848e', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-ccm', key, iv, { + authTagLength: 10 + }); + // Normally, we would do this: + // decrypt.setAuthTag(Buffer.from('0d9bcd142a94caf3d1dd', 'hex')); + assert.throws(() => { + decrypt.setAAD(Buffer.from('63616c76696e', 'hex'), { + plaintextLength: ct.length + }); + }, errMessages.state); + } +} + +// Test that setAuthTag does not throw in GCM mode when called after setAAD. +{ + const key = Buffer.from('1ed2233fa2223ef5d7df08546049406c', 'hex'); + const iv = Buffer.from('579d9dfde9cd93d743da1ceaeebb86e4', 'hex'); + const decrypt = crypto.createDecipheriv('aes-128-gcm', key, iv); + decrypt.setAAD(Buffer.from('0123456789', 'hex')); + decrypt.setAuthTag(Buffer.from('1bb9253e250b8069cde97151d7ef32d9', 'hex')); + assert.strictEqual(decrypt.update('807022', 'hex', 'hex'), 'abcdef'); + assert.strictEqual(decrypt.final('hex'), ''); +}