crypto: add getCipherInfo method

Simple method for retrieving basic information about a cipher
(such as block length, expected or default iv length, key length,
etc)

Signed-off-by: James M Snell <jasnell@gmail.com>
Fixes: https://github.com/nodejs/node/issues/22304

PR-URL: https://github.com/nodejs/node/pull/35368
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
This commit is contained in:
James M Snell 2020-09-26 19:20:03 -07:00
parent adf8f3d1fe
commit 095be6a01f
No known key found for this signature in database
GPG Key ID: 7341B15C070877AC
5 changed files with 274 additions and 1 deletions

View File

@ -2424,6 +2424,35 @@ const ciphers = crypto.getCiphers();
console.log(ciphers); // ['aes-128-cbc', 'aes-128-ccm', ...]
```
### `crypto.getCipherInfo(nameOrNid[, options])`
<!-- YAML
added: REPLACEME
-->
* `nameOrNid`: {string|number} The name or nid of the cipher to query.
* `options`: {Object}
* `keyLength`: {number} A test key length.
* `ivLength`: {number} A test IV length.
* Returns: {Object}
* `name` {string} The name of the cipher
* `nid` {number} The nid of the cipher
* `blockSize` {number} The block size of the cipher in bytes. This property
is omitted when `mode` is `'stream'`.
* `ivLength` {number} The expected or default initialization vector length in
bytes. This property is omitted if the cipher does not use an initialization
vector.
* `keyLength` {number} The expected or default key length in bytes.
* `mode` {string} The cipher mode. One of `'cbc'`, `'ccm'`, `'cfb'`, `'ctr'`,
`'ecb'`, `'gcm'`, `'ocb'`, `'ofb'`, `'stream'`, `'wrap'`, `'xts'`.
Returns information about a given cipher.
Some ciphers accept variable length keys and initialization vectors. By default,
the `crypto.getCipherInfo()` method will return the default values for these
ciphers. To test if a given key length or iv length is acceptable for given
cipher, use the `keyLenth` and `ivLenth` options. If the given values are
unacceptable, `undefined` will be returned.
### `crypto.getCurves()`
<!-- YAML
added: v2.3.0

View File

@ -93,7 +93,8 @@ const {
privateDecrypt,
privateEncrypt,
publicDecrypt,
publicEncrypt
publicEncrypt,
getCipherInfo,
} = require('internal/crypto/cipher');
const {
Sign,
@ -178,6 +179,7 @@ module.exports = {
createVerify,
diffieHellman,
getCiphers,
getCipherInfo,
getCurves,
getDiffieHellman: createDiffieHellmanGroup,
getHashes,

View File

@ -10,6 +10,7 @@ const {
privateEncrypt: _privateEncrypt,
publicDecrypt: _publicDecrypt,
publicEncrypt: _publicEncrypt,
getCipherInfo: _getCipherInfo,
} = internalBinding('crypto');
const {
@ -29,6 +30,8 @@ const {
const {
validateEncoding,
validateInt32,
validateObject,
validateString,
} = require('internal/validators');
@ -291,6 +294,33 @@ ObjectSetPrototypeOf(Decipheriv.prototype, LazyTransform.prototype);
ObjectSetPrototypeOf(Decipheriv, LazyTransform);
addCipherPrototypeFunctions(Decipheriv);
function getCipherInfo(nameOrNid, options) {
if (typeof nameOrNid !== 'string' && typeof nameOrNid !== 'number') {
throw new ERR_INVALID_ARG_TYPE(
'nameOrNid',
['string', 'number'],
nameOrNid);
}
if (typeof nameOrNid === 'number')
validateInt32(nameOrNid, 'nameOrNid');
let keyLength, ivLength;
if (options !== undefined) {
validateObject(options, 'options');
({ keyLength, ivLength } = options);
if (keyLength !== undefined)
validateInt32(keyLength, 'options.keyLength');
if (ivLength !== undefined)
validateInt32(ivLength, 'options.ivLength');
}
const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength);
if (ret !== undefined) {
if (ret.name) ret.name = ret.name.toLowerCase();
if (ret.type) ret.type = ret.type.toLowerCase();
}
return ret;
}
module.exports = {
Cipher,
Cipheriv,
@ -300,4 +330,5 @@ module.exports = {
privateEncrypt,
publicDecrypt,
publicEncrypt,
getCipherInfo,
};

View File

@ -47,6 +47,145 @@ bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) {
bool IsValidGCMTagLength(unsigned int tag_len) {
return tag_len == 4 || tag_len == 8 || (tag_len >= 12 && tag_len <= 16);
}
// Collects and returns information on the given cipher
void GetCipherInfo(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK(args[0]->IsObject());
Local<Object> info = args[0].As<Object>();
CHECK(args[1]->IsString() || args[1]->IsInt32());
const EVP_CIPHER* cipher;
if (args[1]->IsString()) {
Utf8Value name(env->isolate(), args[1]);
cipher = EVP_get_cipherbyname(*name);
} else {
int nid = args[1].As<Int32>()->Value();
cipher = EVP_get_cipherbyname(OBJ_nid2sn(nid));
}
if (cipher == nullptr)
return;
int mode = EVP_CIPHER_mode(cipher);
int iv_length = EVP_CIPHER_iv_length(cipher);
int key_length = EVP_CIPHER_key_length(cipher);
int block_length = EVP_CIPHER_block_size(cipher);
const char* mode_label = nullptr;
switch (mode) {
case EVP_CIPH_CBC_MODE: mode_label = "cbc"; break;
case EVP_CIPH_CCM_MODE: mode_label = "ccm"; break;
case EVP_CIPH_CFB_MODE: mode_label = "cfb"; break;
case EVP_CIPH_CTR_MODE: mode_label = "ctr"; break;
case EVP_CIPH_ECB_MODE: mode_label = "ecb"; break;
case EVP_CIPH_GCM_MODE: mode_label = "gcm"; break;
case EVP_CIPH_OCB_MODE: mode_label = "ocb"; break;
case EVP_CIPH_OFB_MODE: mode_label = "ofb"; break;
case EVP_CIPH_WRAP_MODE: mode_label = "wrap"; break;
case EVP_CIPH_XTS_MODE: mode_label = "xts"; break;
case EVP_CIPH_STREAM_CIPHER: mode_label = "stream"; break;
}
// If the testKeyLen and testIvLen arguments are specified,
// then we will make an attempt to see if they are usable for
// the cipher in question, returning undefined if they are not.
// If they are, the info object will be returned with the values
// given.
if (args[2]->IsInt32() || args[3]->IsInt32()) {
// Test and input IV or key length to determine if it's acceptable.
// If it is, then the getCipherInfo will succeed with the given
// values.
CipherCtxPointer ctx(EVP_CIPHER_CTX_new());
if (!EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, 1))
return;
if (args[2]->IsInt32()) {
int check_len = args[2].As<Int32>()->Value();
if (!EVP_CIPHER_CTX_set_key_length(ctx.get(), check_len))
return;
key_length = check_len;
}
if (args[3]->IsInt32()) {
int check_len = args[3].As<Int32>()->Value();
// For CCM modes, the IV may be between 7 and 13 bytes.
// For GCM and OCB modes, we'll check by attempting to
// set the value. For everything else, just check that
// check_len == iv_length.
switch (mode) {
case EVP_CIPH_CCM_MODE:
if (check_len < 7 || check_len > 13)
return;
break;
case EVP_CIPH_GCM_MODE:
// Fall through
case EVP_CIPH_OCB_MODE:
if (!EVP_CIPHER_CTX_ctrl(
ctx.get(),
EVP_CTRL_AEAD_SET_IVLEN,
check_len,
nullptr)) {
return;
}
break;
default:
if (check_len != iv_length)
return;
}
iv_length = check_len;
}
}
if (mode_label != nullptr &&
info->Set(
env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "mode"),
OneByteString(env->isolate(), mode_label)).IsNothing()) {
return;
}
if (info->Set(
env->context(),
env->name_string(),
OneByteString(env->isolate(), EVP_CIPHER_name(cipher))).IsNothing()) {
return;
}
if (info->Set(
env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "nid"),
Int32::New(env->isolate(), EVP_CIPHER_nid(cipher))).IsNothing()) {
return;
}
// Stream ciphers do not have a meaningful block size
if (mode != EVP_CIPH_STREAM_CIPHER &&
info->Set(
env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "blockSize"),
Int32::New(env->isolate(), block_length)).IsNothing()) {
return;
}
// Ciphers that do not use an IV shouldn't report a length
if (iv_length != 0 &&
info->Set(
env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "ivLength"),
Int32::New(env->isolate(), iv_length)).IsNothing()) {
return;
}
if (info->Set(
env->context(),
FIXED_ONE_BYTE_STRING(env->isolate(), "keyLength"),
Int32::New(env->isolate(), key_length)).IsNothing()) {
return;
}
args.GetReturnValue().Set(info);
}
} // namespace
void CipherBase::GetSSLCiphers(const FunctionCallbackInfo<Value>& args) {
@ -151,6 +290,8 @@ void CipherBase::Initialize(Environment* env, Local<Object> target) {
EVP_PKEY_verify_recover_init,
EVP_PKEY_verify_recover>);
env->SetMethodNoSideEffect(target, "getCipherInfo", GetCipherInfo);
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherEncrypt);
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherDecrypt);
}

View File

@ -0,0 +1,70 @@
'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const {
getCiphers,
getCipherInfo
} = require('crypto');
const assert = require('assert');
const ciphers = getCiphers();
assert.strictEqual(getCipherInfo(-1), undefined);
assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined);
ciphers.forEach((cipher) => {
const info = getCipherInfo(cipher);
assert(info);
const info2 = getCipherInfo(info.nid);
assert.deepStrictEqual(info, info2);
});
const info = getCipherInfo('aes-128-cbc');
assert.strictEqual(info.name, 'aes-128-cbc');
assert.strictEqual(info.nid, 419);
assert.strictEqual(info.blockSize, 16);
assert.strictEqual(info.ivLength, 16);
assert.strictEqual(info.keyLength, 16);
assert.strictEqual(info.mode, 'cbc');
[null, undefined, [], {}].forEach((arg) => {
assert.throws(() => getCipherInfo(arg), {
code: 'ERR_INVALID_ARG_TYPE'
});
});
[null, '', 1, true].forEach((options) => {
assert.throws(
() => getCipherInfo('aes-192-cbc', options), {
code: 'ERR_INVALID_ARG_TYPE'
});
});
[null, '', {}, [], true].forEach((len) => {
assert.throws(
() => getCipherInfo('aes-192-cbc', { keyLength: len }), {
code: 'ERR_INVALID_ARG_TYPE'
});
assert.throws(
() => getCipherInfo('aes-192-cbc', { ivLength: len }), {
code: 'ERR_INVALID_ARG_TYPE'
});
});
assert(!getCipherInfo('aes-128-cbc', { keyLength: 12 }));
assert(getCipherInfo('aes-128-cbc', { keyLength: 16 }));
assert(!getCipherInfo('aes-128-cbc', { ivLength: 12 }));
assert(getCipherInfo('aes-128-cbc', { ivLength: 16 }));
assert(!getCipherInfo('aes-128-ccm', { ivLength: 1 }));
assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 }));
for (let n = 7; n <= 13; n++)
assert(getCipherInfo('aes-128-ccm', { ivLength: n }));
assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 }));
for (let n = 1; n < 16; n++)
assert(getCipherInfo('aes-128-ocb', { ivLength: n }));