Skip to content

Commit

Permalink
crypto: add getCipherInfo method
Browse files Browse the repository at this point in the history
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 <[email protected]>
Fixes: nodejs#22304

PR-URL: nodejs#35368
Reviewed-By: Ben Noordhuis <[email protected]>
  • Loading branch information
jasnell authored and joesepi committed Oct 22, 2020
1 parent 30ba21c commit 8b7aea7
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 1 deletion.
29 changes: 29 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ const {
privateDecrypt,
privateEncrypt,
publicDecrypt,
publicEncrypt
publicEncrypt,
getCipherInfo,
} = require('internal/crypto/cipher');
const {
Sign,
Expand Down Expand Up @@ -178,6 +179,7 @@ module.exports = {
createVerify,
diffieHellman,
getCiphers,
getCipherInfo,
getCurves,
getDiffieHellman: createDiffieHellmanGroup,
getHashes,
Expand Down
31 changes: 31 additions & 0 deletions lib/internal/crypto/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
privateEncrypt: _privateEncrypt,
publicDecrypt: _publicDecrypt,
publicEncrypt: _publicEncrypt,
getCipherInfo: _getCipherInfo,
} = internalBinding('crypto');

const {
Expand All @@ -29,6 +30,8 @@ const {

const {
validateEncoding,
validateInt32,
validateObject,
validateString,
} = require('internal/validators');

Expand Down Expand Up @@ -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,
Expand All @@ -300,4 +330,5 @@ module.exports = {
privateEncrypt,
publicDecrypt,
publicEncrypt,
getCipherInfo,
};
141 changes: 141 additions & 0 deletions src/crypto/crypto_cipher.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
}
Expand Down
70 changes: 70 additions & 0 deletions test/parallel/test-crypto-getcipherinfo.js
Original file line number Diff line number Diff line change
@@ -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 }));

0 comments on commit 8b7aea7

Please sign in to comment.