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:
parent
38a692963f
commit
1e07acd476
@ -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])
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
- `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
|
||||
|
@ -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);
|
||||
|
@ -2802,7 +2802,8 @@ void CipherBase::New(const FunctionCallbackInfo<Value>& 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<Value>& 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<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,
|
||||
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<Value>& 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<Value>& 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<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 {
|
||||
// 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<Value>& 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<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())
|
||||
return false;
|
||||
|
||||
int outlen;
|
||||
if (!EVP_CipherUpdate(ctx_,
|
||||
nullptr,
|
||||
&outlen,
|
||||
reinterpret_cast<const unsigned char*>(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<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;
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
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<unsigned char*>(auth_tag_));
|
||||
auth_tag_len_ = 0;
|
||||
auth_tag_set_ = true;
|
||||
}
|
||||
|
||||
*out_len = len + EVP_CIPHER_CTX_block_size(ctx_);
|
||||
*out = Malloc<unsigned char>(static_cast<size_t>(*out_len));
|
||||
return EVP_CipherUpdate(ctx_,
|
||||
*out,
|
||||
out_len,
|
||||
reinterpret_cast<const unsigned char*>(data),
|
||||
len);
|
||||
int r = EVP_CipherUpdate(ctx_,
|
||||
*out,
|
||||
out_len,
|
||||
reinterpret_cast<const unsigned char*>(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<Value>& 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<Value>& 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<unsigned char>(
|
||||
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()) {
|
||||
auth_tag_len_ = sizeof(auth_tag_);
|
||||
r = EVP_CIPHER_CTX_ctrl(ctx_, EVP_CTRL_GCM_GET_TAG, auth_tag_len_,
|
||||
reinterpret_cast<unsigned char*>(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<unsigned char*>(auth_tag_)));
|
||||
}
|
||||
}
|
||||
|
||||
EVP_CIPHER_CTX_free(ctx_);
|
||||
ctx_ = nullptr;
|
||||
|
||||
return r == 1;
|
||||
return ok;
|
||||
}
|
||||
|
||||
|
||||
|
@ -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<v8::Value>& args);
|
||||
static void Init(const v8::FunctionCallbackInfo<v8::Value>& 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<CipherBase>(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 {
|
||||
|
@ -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'));
|
||||
if (test.aad)
|
||||
encrypt.setAAD(Buffer.from(test.aad, 'hex'));
|
||||
Buffer.from(test.iv, 'hex'),
|
||||
options);
|
||||
|
||||
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');
|
||||
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'));
|
||||
|
||||
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);
|
||||
if (isCCM && common.hasFipsCrypto) {
|
||||
assert.throws(() => {
|
||||
crypto.createDecipheriv(test.algo,
|
||||
Buffer.from(test.key, 'hex'),
|
||||
Buffer.from(test.iv, 'hex'),
|
||||
options);
|
||||
}, errMessages.FIPS);
|
||||
} else {
|
||||
// assert that final throws if input data could not be verified!
|
||||
assert.throws(function() { decrypt.final('hex'); }, errMessages.auth);
|
||||
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';
|
||||
|
||||
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'), '');
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user