Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: add simple getCipherInfo #35368

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions doc/api/crypto.md
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
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
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
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;
}
jasnell marked this conversation as resolved.
Show resolved Hide resolved

// 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
@@ -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 }));