crypto: add support for AES-CCM

This commit adds support for another AEAD algorithm and introduces
required API changes and extensions. Due to the design of CCM itself and
the way OpenSSL implements it, there are some restrictions when using
this mode as outlined in the updated documentation.

PR-URL: https://github.com/nodejs/node/pull/18138
Fixes: https://github.com/nodejs/node/issues/2383
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Shigeki Ohtsu <ohtsu@ohtsu.org>
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
This commit is contained in:
Tobias Nießen 2017-12-18 13:22:08 +01:00
parent 38a692963f
commit 1e07acd476
No known key found for this signature in database
GPG Key ID: 718207F8FD156B70
5 changed files with 827 additions and 107 deletions

View File

@ -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 longer be used to encrypt data. Attempts to call `cipher.final()` more than
once will result in an error being thrown. once will result in an error being thrown.
### cipher.setAAD(buffer) ### cipher.setAAD(buffer[, options])
<!-- YAML <!-- YAML
added: v1.0.0 added: v1.0.0
--> -->
- `buffer` {Buffer} - `buffer` {Buffer}
- `options` {object}
- Returns the {Cipher} for method chaining. - 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 supported), the `cipher.setAAD()` method sets the value used for the
_additional authenticated data_ (AAD) input parameter. _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()`][]. The `cipher.setAAD()` method must be called before [`cipher.update()`][].
### cipher.getAuthTag() ### cipher.getAuthTag()
@ -1312,7 +1317,12 @@ deprecated: REPLACEME
- `options` {Object} [`stream.transform` options][] - `options` {Object} [`stream.transform` options][]
Creates and returns a `Cipher` object that uses the given `algorithm` and 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 The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the recent OpenSSL releases, `openssl list-cipher-algorithms` will display the
@ -1353,8 +1363,10 @@ changes:
- `options` {Object} [`stream.transform` options][] - `options` {Object} [`stream.transform` options][]
Creates and returns a `Cipher` object, with the given `algorithm`, `key` and Creates and returns a `Cipher` object, with the given `algorithm`, `key` and
initialization vector (`iv`). Optional `options` argument controls stream The `options` argument controls stream behavior and is optional except when a
behavior. 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 The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the recent OpenSSL releases, `openssl list-cipher-algorithms` will display the
@ -1396,7 +1408,12 @@ deprecated: REPLACEME
- `options` {Object} [`stream.transform` options][] - `options` {Object} [`stream.transform` options][]
Creates and returns a `Decipher` object that uses the given `algorithm` and 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 The implementation of `crypto.createDecipher()` derives keys using the OpenSSL
function [`EVP_BytesToKey`][] with the digest algorithm set to MD5, one function [`EVP_BytesToKey`][] with the digest algorithm set to MD5, one
@ -1425,8 +1442,12 @@ changes:
- `options` {Object} [`stream.transform` options][] - `options` {Object} [`stream.transform` options][]
Creates and returns a `Decipher` object that uses the given `algorithm`, `key` Creates and returns a `Decipher` object that uses the given `algorithm`, `key`
and initialization vector (`iv`). Optional `options` argument controls stream and initialization vector (`iv`).
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 The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the 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. 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 ## Crypto Constants
The following constants exported by `crypto.constants` apply to various uses of 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 [`tls.createSecureContext()`]: tls.html#tls_tls_createsecurecontext_options
[`verify.update()`]: #crypto_verify_update_data_inputencoding [`verify.update()`]: #crypto_verify_update_data_inputencoding
[`verify.verify()`]: #crypto_verify_verify_object_signature_signatureformat [`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 [Caveats]: #crypto_support_for_weak_or_compromised_algorithms
[CCM mode]: #crypto_ccm_mode
[Crypto Constants]: #crypto_crypto_constants_1 [Crypto Constants]: #crypto_crypto_constants_1
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed [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 [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 [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 2412]: https://www.rfc-editor.org/rfc/rfc2412.txt
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.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 [RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
[initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector [initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector
[stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback [stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback

View File

@ -7,7 +7,8 @@ const {
const { const {
ERR_CRYPTO_INVALID_STATE, ERR_CRYPTO_INVALID_STATE,
ERR_INVALID_ARG_TYPE ERR_INVALID_ARG_TYPE,
ERR_INVALID_OPT_VALUE
} = require('internal/errors').codes; } = require('internal/errors').codes;
const { const {
@ -62,6 +63,16 @@ function getDecoder(decoder, encoding) {
return decoder; 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) { function Cipher(cipher, password, options) {
if (!(this instanceof Cipher)) if (!(this instanceof Cipher))
return new Cipher(cipher, password, options); 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 = new CipherBase(true);
this._handle.init(cipher, password); this._handle.init(cipher, password, authTagLength);
this._decoder = null; this._decoder = null;
LazyTransform.call(this, options); LazyTransform.call(this, options);
@ -168,13 +181,15 @@ Cipher.prototype.setAuthTag = function setAuthTag(tagbuf) {
return this; return this;
}; };
Cipher.prototype.setAAD = function setAAD(aadbuf) { Cipher.prototype.setAAD = function setAAD(aadbuf, options) {
if (!isArrayBufferView(aadbuf)) { if (!isArrayBufferView(aadbuf)) {
throw new ERR_INVALID_ARG_TYPE('buffer', throw new ERR_INVALID_ARG_TYPE('buffer',
['Buffer', 'TypedArray', 'DataView'], ['Buffer', 'TypedArray', 'DataView'],
aadbuf); 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'); throw new ERR_CRYPTO_INVALID_STATE('setAAD');
return this; return this;
}; };
@ -204,8 +219,10 @@ function Cipheriv(cipher, key, iv, options) {
); );
} }
const authTagLength = getUIntOption(options, 'authTagLength');
this._handle = new CipherBase(true); this._handle = new CipherBase(true);
this._handle.initiv(cipher, key, iv); this._handle.initiv(cipher, key, iv, authTagLength);
this._decoder = null; this._decoder = null;
LazyTransform.call(this, options); 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 = new CipherBase(false);
this._handle.init(cipher, password); this._handle.init(cipher, password, authTagLength);
this._decoder = null; this._decoder = null;
LazyTransform.call(this, options); 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 = new CipherBase(false);
this._handle.initiv(cipher, key, iv); this._handle.initiv(cipher, key, iv, authTagLength);
this._decoder = null; this._decoder = null;
LazyTransform.call(this, options); LazyTransform.call(this, options);

View File

@ -2802,7 +2802,8 @@ void CipherBase::New(const FunctionCallbackInfo<Value>& args) {
void CipherBase::Init(const char* cipher_type, void CipherBase::Init(const char* cipher_type,
const char* key_buf, const char* key_buf,
int key_buf_len) { int key_buf_len,
int auth_tag_len) {
HandleScope scope(env()->isolate()); HandleScope scope(env()->isolate());
#ifdef NODE_FIPS_MODE #ifdef NODE_FIPS_MODE
@ -2847,6 +2848,12 @@ void CipherBase::Init(const char* cipher_type,
if (mode == EVP_CIPH_WRAP_MODE) if (mode == EVP_CIPH_WRAP_MODE)
EVP_CIPHER_CTX_set_flags(ctx_, EVP_CIPHER_CTX_FLAG_WRAP_ALLOW); 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)); CHECK_EQ(1, EVP_CIPHER_CTX_set_key_length(ctx_, key_len));
EVP_CipherInit_ex(ctx_, EVP_CipherInit_ex(ctx_,
@ -2862,12 +2869,17 @@ void CipherBase::Init(const FunctionCallbackInfo<Value>& args) {
CipherBase* cipher; CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); 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 node::Utf8Value cipher_type(args.GetIsolate(), args[0]);
const char* key_buf = Buffer::Data(args[1]); const char* key_buf = Buffer::Data(args[1]);
ssize_t key_buf_len = Buffer::Length(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<v8::Int32>()->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, const char* key,
int key_len, int key_len,
const char* iv, const char* iv,
int iv_len) { int iv_len,
int auth_tag_len) {
HandleScope scope(env()->isolate()); HandleScope scope(env()->isolate());
const EVP_CIPHER* const cipher = EVP_get_cipherbyname(cipher_type); 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 expected_iv_len = EVP_CIPHER_iv_length(cipher);
const int mode = EVP_CIPHER_mode(cipher); const int mode = EVP_CIPHER_mode(cipher);
const bool is_gcm_mode = (EVP_CIPH_GCM_MODE == mode); 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; const bool has_iv = iv_len >= 0;
// Throw if no IV was passed and the cipher requires an IV // 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 // 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"); return env()->ThrowError("Invalid IV length");
} }
@ -2908,13 +2922,10 @@ void CipherBase::InitIv(const char* cipher_type,
const bool encrypt = (kind_ == kCipher); const bool encrypt = (kind_ == kCipher);
EVP_CipherInit_ex(ctx_, cipher, nullptr, nullptr, nullptr, encrypt); EVP_CipherInit_ex(ctx_, cipher, nullptr, nullptr, nullptr, encrypt);
if (is_gcm_mode) { if (IsAuthenticatedMode()) {
CHECK(has_iv); CHECK(has_iv);
if (!EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_SET_IVLEN, iv_len, nullptr)) { if (!InitAuthenticated(cipher_type, iv_len, auth_tag_len))
EVP_CIPHER_CTX_free(ctx_); return;
ctx_ = nullptr;
return env()->ThrowError("Invalid IV length");
}
} }
if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) { if (!EVP_CIPHER_CTX_set_key_length(ctx_, key_len)) {
@ -2937,7 +2948,7 @@ void CipherBase::InitIv(const FunctionCallbackInfo<Value>& args) {
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder());
Environment* env = cipher->env(); Environment* env = cipher->env();
CHECK_GE(args.Length(), 3); CHECK_GE(args.Length(), 4);
const node::Utf8Value cipher_type(env->isolate(), args[0]); const node::Utf8Value cipher_type(env->isolate(), args[0]);
ssize_t key_len = Buffer::Length(args[1]); ssize_t key_len = Buffer::Length(args[1]);
@ -2951,16 +2962,84 @@ void CipherBase::InitIv(const FunctionCallbackInfo<Value>& args) {
iv_buf = Buffer::Data(args[2]); iv_buf = Buffer::Data(args[2]);
iv_len = Buffer::Length(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<v8::Int32>()->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<int>(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 { bool CipherBase::IsAuthenticatedMode() const {
// Check if this cipher operates in an AEAD mode that we support. // Check if this cipher operates in an AEAD mode that we support.
CHECK_NE(ctx_, nullptr); CHECK_NE(ctx_, nullptr);
const EVP_CIPHER* const cipher = EVP_CIPHER_CTX_cipher(ctx_); const int mode = EVP_CIPHER_CTX_mode(ctx_);
int mode = EVP_CIPHER_mode(cipher); return mode == EVP_CIPH_GCM_MODE || mode == EVP_CIPH_CCM_MODE;
return mode == EVP_CIPH_GCM_MODE;
} }
@ -2995,12 +3074,15 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& args) {
// Restrict GCM tag lengths according to NIST 800-38d, page 9. // Restrict GCM tag lengths according to NIST 800-38d, page 9.
unsigned int tag_len = Buffer::Length(args[0]); unsigned int tag_len = Buffer::Length(args[0]);
if (tag_len > 16 || (tag_len < 12 && tag_len != 8 && tag_len != 4)) { const int mode = EVP_CIPHER_CTX_mode(cipher->ctx_);
char msg[125]; if (mode == EVP_CIPH_GCM_MODE) {
snprintf(msg, sizeof(msg), if (tag_len > 16 || (tag_len < 12 && tag_len != 8 && tag_len != 4)) {
"Permitting authentication tag lengths of %u bytes is deprecated. " char msg[125];
"Valid GCM tag lengths are 4, 8, 12, 13, 14, 15, 16.", tag_len); snprintf(msg, sizeof(msg),
ProcessEmitDeprecationWarning(cipher->env(), msg, "DEP0090"); "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. // Note: we don't use std::max() here to work around a header conflict.
@ -3013,18 +3095,44 @@ void CipherBase::SetAuthTag(const FunctionCallbackInfo<Value>& 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()) if (ctx_ == nullptr || !IsAuthenticatedMode())
return false; return false;
int outlen; int outlen;
if (!EVP_CipherUpdate(ctx_, const int mode = EVP_CIPHER_CTX_mode(ctx_);
nullptr,
&outlen, // When in CCM mode, we need to set the authentication tag and the plaintext
reinterpret_cast<const unsigned char*>(data), // length in advance.
len)) { if (mode == EVP_CIPH_CCM_MODE) {
return false; 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<unsigned char*>(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<const unsigned char*>(data),
len);
} }
@ -3032,34 +3140,55 @@ void CipherBase::SetAAD(const FunctionCallbackInfo<Value>& args) {
CipherBase* cipher; CipherBase* cipher;
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); 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<v8::Int32>()->Value();
if (!cipher->SetAAD(Buffer::Data(args[0]), Buffer::Length(args[0]),
plaintext_len))
args.GetReturnValue().Set(false); // Report invalid state failure args.GetReturnValue().Set(false); // Report invalid state failure
} }
bool CipherBase::Update(const char* data, CipherBase::UpdateResult CipherBase::Update(const char* data,
int len, int len,
unsigned char** out, unsigned char** out,
int* out_len) { int* out_len) {
if (ctx_ == nullptr) 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: // 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_CIPHER_CTX_ctrl(ctx_,
EVP_CTRL_GCM_SET_TAG, EVP_CTRL_GCM_SET_TAG,
auth_tag_len_, auth_tag_len_,
reinterpret_cast<unsigned char*>(auth_tag_)); reinterpret_cast<unsigned char*>(auth_tag_));
auth_tag_len_ = 0; auth_tag_set_ = true;
} }
*out_len = len + EVP_CIPHER_CTX_block_size(ctx_); *out_len = len + EVP_CIPHER_CTX_block_size(ctx_);
*out = Malloc<unsigned char>(static_cast<size_t>(*out_len)); *out = Malloc<unsigned char>(static_cast<size_t>(*out_len));
return EVP_CipherUpdate(ctx_, int r = EVP_CipherUpdate(ctx_,
*out, *out,
out_len, out_len,
reinterpret_cast<const unsigned char*>(data), reinterpret_cast<const unsigned char*>(data),
len); 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<Value>& args) {
ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder()); ASSIGN_OR_RETURN_UNWRAP(&cipher, args.Holder());
unsigned char* out = nullptr; unsigned char* out = nullptr;
bool r; UpdateResult r;
int out_len = 0; int out_len = 0;
// Only copy the data if we have to, because it's a string // Only copy the data if we have to, because it's a string
@ -3085,11 +3214,13 @@ void CipherBase::Update(const FunctionCallbackInfo<Value>& args) {
r = cipher->Update(buf, buflen, &out, &out_len); r = cipher->Update(buf, buflen, &out, &out_len);
} }
if (!r) { if (r != kSuccess) {
free(out); free(out);
return ThrowCryptoError(env, if (r == kErrorState) {
ERR_get_error(), ThrowCryptoError(env, ERR_get_error(),
"Trying to add data in unsupported state"); "Trying to add data in unsupported state");
}
return;
} }
CHECK(out != nullptr || out_len == 0); CHECK(out != nullptr || out_len == 0);
@ -3120,21 +3251,36 @@ bool CipherBase::Final(unsigned char** out, int *out_len) {
if (ctx_ == nullptr) if (ctx_ == nullptr)
return false; return false;
const int mode = EVP_CIPHER_CTX_mode(ctx_);
*out = Malloc<unsigned char>( *out = Malloc<unsigned char>(
static_cast<size_t>(EVP_CIPHER_CTX_block_size(ctx_))); static_cast<size_t>(EVP_CIPHER_CTX_block_size(ctx_)));
int r = EVP_CipherFinal_ex(ctx_, *out, out_len);
if (r == 1 && kind_ == kCipher && IsAuthenticatedMode()) { // In CCM mode, final() only checks whether authentication failed in update().
auth_tag_len_ = sizeof(auth_tag_); // EVP_CipherFinal_ex must not be called and will fail.
r = EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_GET_TAG, auth_tag_len_, bool ok;
reinterpret_cast<unsigned char*>(auth_tag_)); if (kind_ == kDecipher && mode == EVP_CIPH_CCM_MODE) {
CHECK_EQ(r, 1); 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<unsigned char*>(auth_tag_)));
}
} }
EVP_CIPHER_CTX_free(ctx_); EVP_CIPHER_CTX_free(ctx_);
ctx_ = nullptr; ctx_ = nullptr;
return r == 1; return ok;
} }

View File

@ -354,19 +354,31 @@ class CipherBase : public BaseObject {
kCipher, kCipher,
kDecipher 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, void InitIv(const char* cipher_type,
const char* key, const char* key,
int key_len, int key_len,
const char* iv, const char* iv,
int iv_len); int iv_len,
bool Update(const char* data, int len, unsigned char** out, int* out_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 Final(unsigned char** out, int *out_len);
bool SetAutoPadding(bool auto_padding); bool SetAutoPadding(bool auto_padding);
bool IsAuthenticatedMode() const; 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<v8::Value>& args); static void New(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Init(const v8::FunctionCallbackInfo<v8::Value>& args); static void Init(const v8::FunctionCallbackInfo<v8::Value>& args);
@ -385,15 +397,20 @@ class CipherBase : public BaseObject {
: BaseObject(env, wrap), : BaseObject(env, wrap),
ctx_(nullptr), ctx_(nullptr),
kind_(kind), kind_(kind),
auth_tag_len_(0) { auth_tag_set_(false),
auth_tag_len_(0),
pending_auth_failed_(false) {
MakeWeak<CipherBase>(this); MakeWeak<CipherBase>(this);
} }
private: private:
EVP_CIPHER_CTX* ctx_; EVP_CIPHER_CTX* ctx_;
const CipherKind kind_; const CipherKind kind_;
bool auth_tag_set_;
unsigned int auth_tag_len_; unsigned int auth_tag_len_;
char auth_tag_[EVP_GCM_TLS_TAG_LEN]; char auth_tag_[EVP_GCM_TLS_TAG_LEN];
bool pending_auth_failed_;
int max_message_size_;
}; };
class Hmac : public BaseObject { class Hmac : public BaseObject {

View File

@ -323,6 +323,182 @@ const TEST_CASES = [
'0fc0c3b780f244452da3ebf1c5d82cde' + '0fc0c3b780f244452da3ebf1c5d82cde' +
'a2418997200ef82e44ae7e3f', 'a2418997200ef82e44ae7e3f',
tag: 'a44a8266ee1c8eb0c8b5d4cf5ae9f19a', tampered: false }, 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 = { const errMessages = {
@ -330,13 +506,33 @@ const errMessages = {
state: / state/, state: / state/,
FIPS: /not supported in FIPS mode/, FIPS: /not supported in FIPS mode/,
length: /Invalid IV length/, length: /Invalid IV length/,
authTagLength: /Invalid authentication tag length/
}; };
const ciphers = crypto.getCiphers(); const ciphers = crypto.getCiphers();
const expectedWarnings = common.hasFipsCrypto ? 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] const expectedDeprecationWarnings = [0, 1, 2, 6, 9, 10, 11, 17]
.map((i) => [`Permitting authentication tag lengths of ${i} bytes is ` + .map((i) => [`Permitting authentication tag lengths of ${i} bytes is ` +
@ -362,14 +558,30 @@ for (const test of TEST_CASES) {
continue; 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, const encrypt = crypto.createCipheriv(test.algo,
Buffer.from(test.key, 'hex'), Buffer.from(test.key, 'hex'),
Buffer.from(test.iv, 'hex')); Buffer.from(test.iv, 'hex'),
if (test.aad) options);
encrypt.setAAD(Buffer.from(test.aad, 'hex'));
if (test.aad)
encrypt.setAAD(Buffer.from(test.aad, 'hex'), aadOptions);
const inputEncoding = test.plainIsHex ? 'hex' : 'ascii';
let hex = encrypt.update(test.plain, inputEncoding, 'hex'); let hex = encrypt.update(test.plain, inputEncoding, 'hex');
hex += encrypt.final('hex'); hex += encrypt.final('hex');
@ -382,22 +594,32 @@ for (const test of TEST_CASES) {
} }
{ {
const decrypt = crypto.createDecipheriv(test.algo, if (isCCM && common.hasFipsCrypto) {
Buffer.from(test.key, 'hex'), assert.throws(() => {
Buffer.from(test.iv, 'hex')); crypto.createDecipheriv(test.algo,
decrypt.setAuthTag(Buffer.from(test.tag, 'hex')); Buffer.from(test.key, 'hex'),
if (test.aad) Buffer.from(test.iv, 'hex'),
decrypt.setAAD(Buffer.from(test.aad, 'hex')); options);
}, errMessages.FIPS);
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 { } else {
// assert that final throws if input data could not be verified! const decrypt = crypto.createDecipheriv(test.algo,
assert.throws(function() { decrypt.final('hex'); }, errMessages.auth); 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';
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); }, assert.throws(() => { crypto.createCipher(test.algo, test.password); },
errMessages.FIPS); errMessages.FIPS);
} else { } else {
const encrypt = crypto.createCipher(test.algo, test.password); const encrypt = crypto.createCipher(test.algo, test.password, options);
if (test.aad) 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'); let hex = encrypt.update(test.plain, 'ascii', 'hex');
hex += encrypt.final('hex'); hex += encrypt.final('hex');
const auth_tag = encrypt.getAuthTag(); const auth_tag = encrypt.getAuthTag();
@ -425,10 +647,10 @@ for (const test of TEST_CASES) {
assert.throws(() => { crypto.createDecipher(test.algo, test.password); }, assert.throws(() => { crypto.createDecipher(test.algo, test.password); },
errMessages.FIPS); errMessages.FIPS);
} else { } 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')); decrypt.setAuthTag(Buffer.from(test.tag, 'hex'));
if (test.aad) 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'); let msg = decrypt.update(test.ct, 'hex', 'ascii');
if (!test.tampered) { if (!test.tampered) {
msg += decrypt.final('ascii'); msg += decrypt.final('ascii');
@ -444,7 +666,8 @@ for (const test of TEST_CASES) {
// trying to get tag before inputting all data: // trying to get tag before inputting all data:
const encrypt = crypto.createCipheriv(test.algo, const encrypt = crypto.createCipheriv(test.algo,
Buffer.from(test.key, 'hex'), Buffer.from(test.key, 'hex'),
Buffer.from(test.iv, 'hex')); Buffer.from(test.iv, 'hex'),
options);
encrypt.update('blah', 'ascii'); encrypt.update('blah', 'ascii');
assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state); assert.throws(function() { encrypt.getAuthTag(); }, errMessages.state);
} }
@ -453,17 +676,21 @@ for (const test of TEST_CASES) {
// trying to set tag on encryption object: // trying to set tag on encryption object:
const encrypt = crypto.createCipheriv(test.algo, const encrypt = crypto.createCipheriv(test.algo,
Buffer.from(test.key, 'hex'), 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')); }, assert.throws(() => { encrypt.setAuthTag(Buffer.from(test.tag, 'hex')); },
errMessages.state); errMessages.state);
} }
{ {
// trying to read tag from decryption object: if (!isCCM || !common.hasFipsCrypto) {
const decrypt = crypto.createDecipheriv(test.algo, // trying to read tag from decryption object:
Buffer.from(test.key, 'hex'), const decrypt = crypto.createDecipheriv(test.algo,
Buffer.from(test.iv, 'hex')); Buffer.from(test.key, 'hex'),
assert.throws(function() { decrypt.getAuthTag(); }, errMessages.state); 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))); 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'), '');
}