Skip to content

Commit 095be6a

Browse files
committed
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: #22304 PR-URL: #35368 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
1 parent adf8f3d commit 095be6a

File tree

5 files changed

+274
-1
lines changed

5 files changed

+274
-1
lines changed

doc/api/crypto.md

+29
Original file line numberDiff line numberDiff line change
@@ -2424,6 +2424,35 @@ const ciphers = crypto.getCiphers();
24242424
console.log(ciphers); // ['aes-128-cbc', 'aes-128-ccm', ...]
24252425
```
24262426

2427+
### `crypto.getCipherInfo(nameOrNid[, options])`
2428+
<!-- YAML
2429+
added: REPLACEME
2430+
-->
2431+
2432+
* `nameOrNid`: {string|number} The name or nid of the cipher to query.
2433+
* `options`: {Object}
2434+
* `keyLength`: {number} A test key length.
2435+
* `ivLength`: {number} A test IV length.
2436+
* Returns: {Object}
2437+
* `name` {string} The name of the cipher
2438+
* `nid` {number} The nid of the cipher
2439+
* `blockSize` {number} The block size of the cipher in bytes. This property
2440+
is omitted when `mode` is `'stream'`.
2441+
* `ivLength` {number} The expected or default initialization vector length in
2442+
bytes. This property is omitted if the cipher does not use an initialization
2443+
vector.
2444+
* `keyLength` {number} The expected or default key length in bytes.
2445+
* `mode` {string} The cipher mode. One of `'cbc'`, `'ccm'`, `'cfb'`, `'ctr'`,
2446+
`'ecb'`, `'gcm'`, `'ocb'`, `'ofb'`, `'stream'`, `'wrap'`, `'xts'`.
2447+
2448+
Returns information about a given cipher.
2449+
2450+
Some ciphers accept variable length keys and initialization vectors. By default,
2451+
the `crypto.getCipherInfo()` method will return the default values for these
2452+
ciphers. To test if a given key length or iv length is acceptable for given
2453+
cipher, use the `keyLenth` and `ivLenth` options. If the given values are
2454+
unacceptable, `undefined` will be returned.
2455+
24272456
### `crypto.getCurves()`
24282457
<!-- YAML
24292458
added: v2.3.0

lib/crypto.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ const {
9393
privateDecrypt,
9494
privateEncrypt,
9595
publicDecrypt,
96-
publicEncrypt
96+
publicEncrypt,
97+
getCipherInfo,
9798
} = require('internal/crypto/cipher');
9899
const {
99100
Sign,
@@ -178,6 +179,7 @@ module.exports = {
178179
createVerify,
179180
diffieHellman,
180181
getCiphers,
182+
getCipherInfo,
181183
getCurves,
182184
getDiffieHellman: createDiffieHellmanGroup,
183185
getHashes,

lib/internal/crypto/cipher.js

+31
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
privateEncrypt: _privateEncrypt,
1111
publicDecrypt: _publicDecrypt,
1212
publicEncrypt: _publicEncrypt,
13+
getCipherInfo: _getCipherInfo,
1314
} = internalBinding('crypto');
1415

1516
const {
@@ -29,6 +30,8 @@ const {
2930

3031
const {
3132
validateEncoding,
33+
validateInt32,
34+
validateObject,
3235
validateString,
3336
} = require('internal/validators');
3437

@@ -291,6 +294,33 @@ ObjectSetPrototypeOf(Decipheriv.prototype, LazyTransform.prototype);
291294
ObjectSetPrototypeOf(Decipheriv, LazyTransform);
292295
addCipherPrototypeFunctions(Decipheriv);
293296

297+
function getCipherInfo(nameOrNid, options) {
298+
if (typeof nameOrNid !== 'string' && typeof nameOrNid !== 'number') {
299+
throw new ERR_INVALID_ARG_TYPE(
300+
'nameOrNid',
301+
['string', 'number'],
302+
nameOrNid);
303+
}
304+
if (typeof nameOrNid === 'number')
305+
validateInt32(nameOrNid, 'nameOrNid');
306+
let keyLength, ivLength;
307+
if (options !== undefined) {
308+
validateObject(options, 'options');
309+
({ keyLength, ivLength } = options);
310+
if (keyLength !== undefined)
311+
validateInt32(keyLength, 'options.keyLength');
312+
if (ivLength !== undefined)
313+
validateInt32(ivLength, 'options.ivLength');
314+
}
315+
316+
const ret = _getCipherInfo({}, nameOrNid, keyLength, ivLength);
317+
if (ret !== undefined) {
318+
if (ret.name) ret.name = ret.name.toLowerCase();
319+
if (ret.type) ret.type = ret.type.toLowerCase();
320+
}
321+
return ret;
322+
}
323+
294324
module.exports = {
295325
Cipher,
296326
Cipheriv,
@@ -300,4 +330,5 @@ module.exports = {
300330
privateEncrypt,
301331
publicDecrypt,
302332
publicEncrypt,
333+
getCipherInfo,
303334
};

src/crypto/crypto_cipher.cc

+141
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,145 @@ bool IsSupportedAuthenticatedMode(const EVP_CIPHER_CTX* ctx) {
4747
bool IsValidGCMTagLength(unsigned int tag_len) {
4848
return tag_len == 4 || tag_len == 8 || (tag_len >= 12 && tag_len <= 16);
4949
}
50+
51+
// Collects and returns information on the given cipher
52+
void GetCipherInfo(const FunctionCallbackInfo<Value>& args) {
53+
Environment* env = Environment::GetCurrent(args);
54+
CHECK(args[0]->IsObject());
55+
Local<Object> info = args[0].As<Object>();
56+
57+
CHECK(args[1]->IsString() || args[1]->IsInt32());
58+
59+
const EVP_CIPHER* cipher;
60+
if (args[1]->IsString()) {
61+
Utf8Value name(env->isolate(), args[1]);
62+
cipher = EVP_get_cipherbyname(*name);
63+
} else {
64+
int nid = args[1].As<Int32>()->Value();
65+
cipher = EVP_get_cipherbyname(OBJ_nid2sn(nid));
66+
}
67+
68+
if (cipher == nullptr)
69+
return;
70+
71+
int mode = EVP_CIPHER_mode(cipher);
72+
int iv_length = EVP_CIPHER_iv_length(cipher);
73+
int key_length = EVP_CIPHER_key_length(cipher);
74+
int block_length = EVP_CIPHER_block_size(cipher);
75+
const char* mode_label = nullptr;
76+
switch (mode) {
77+
case EVP_CIPH_CBC_MODE: mode_label = "cbc"; break;
78+
case EVP_CIPH_CCM_MODE: mode_label = "ccm"; break;
79+
case EVP_CIPH_CFB_MODE: mode_label = "cfb"; break;
80+
case EVP_CIPH_CTR_MODE: mode_label = "ctr"; break;
81+
case EVP_CIPH_ECB_MODE: mode_label = "ecb"; break;
82+
case EVP_CIPH_GCM_MODE: mode_label = "gcm"; break;
83+
case EVP_CIPH_OCB_MODE: mode_label = "ocb"; break;
84+
case EVP_CIPH_OFB_MODE: mode_label = "ofb"; break;
85+
case EVP_CIPH_WRAP_MODE: mode_label = "wrap"; break;
86+
case EVP_CIPH_XTS_MODE: mode_label = "xts"; break;
87+
case EVP_CIPH_STREAM_CIPHER: mode_label = "stream"; break;
88+
}
89+
90+
// If the testKeyLen and testIvLen arguments are specified,
91+
// then we will make an attempt to see if they are usable for
92+
// the cipher in question, returning undefined if they are not.
93+
// If they are, the info object will be returned with the values
94+
// given.
95+
if (args[2]->IsInt32() || args[3]->IsInt32()) {
96+
// Test and input IV or key length to determine if it's acceptable.
97+
// If it is, then the getCipherInfo will succeed with the given
98+
// values.
99+
CipherCtxPointer ctx(EVP_CIPHER_CTX_new());
100+
if (!EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, 1))
101+
return;
102+
103+
if (args[2]->IsInt32()) {
104+
int check_len = args[2].As<Int32>()->Value();
105+
if (!EVP_CIPHER_CTX_set_key_length(ctx.get(), check_len))
106+
return;
107+
key_length = check_len;
108+
}
109+
110+
if (args[3]->IsInt32()) {
111+
int check_len = args[3].As<Int32>()->Value();
112+
// For CCM modes, the IV may be between 7 and 13 bytes.
113+
// For GCM and OCB modes, we'll check by attempting to
114+
// set the value. For everything else, just check that
115+
// check_len == iv_length.
116+
switch (mode) {
117+
case EVP_CIPH_CCM_MODE:
118+
if (check_len < 7 || check_len > 13)
119+
return;
120+
break;
121+
case EVP_CIPH_GCM_MODE:
122+
// Fall through
123+
case EVP_CIPH_OCB_MODE:
124+
if (!EVP_CIPHER_CTX_ctrl(
125+
ctx.get(),
126+
EVP_CTRL_AEAD_SET_IVLEN,
127+
check_len,
128+
nullptr)) {
129+
return;
130+
}
131+
break;
132+
default:
133+
if (check_len != iv_length)
134+
return;
135+
}
136+
iv_length = check_len;
137+
}
138+
}
139+
140+
if (mode_label != nullptr &&
141+
info->Set(
142+
env->context(),
143+
FIXED_ONE_BYTE_STRING(env->isolate(), "mode"),
144+
OneByteString(env->isolate(), mode_label)).IsNothing()) {
145+
return;
146+
}
147+
148+
if (info->Set(
149+
env->context(),
150+
env->name_string(),
151+
OneByteString(env->isolate(), EVP_CIPHER_name(cipher))).IsNothing()) {
152+
return;
153+
}
154+
155+
if (info->Set(
156+
env->context(),
157+
FIXED_ONE_BYTE_STRING(env->isolate(), "nid"),
158+
Int32::New(env->isolate(), EVP_CIPHER_nid(cipher))).IsNothing()) {
159+
return;
160+
}
161+
162+
// Stream ciphers do not have a meaningful block size
163+
if (mode != EVP_CIPH_STREAM_CIPHER &&
164+
info->Set(
165+
env->context(),
166+
FIXED_ONE_BYTE_STRING(env->isolate(), "blockSize"),
167+
Int32::New(env->isolate(), block_length)).IsNothing()) {
168+
return;
169+
}
170+
171+
// Ciphers that do not use an IV shouldn't report a length
172+
if (iv_length != 0 &&
173+
info->Set(
174+
env->context(),
175+
FIXED_ONE_BYTE_STRING(env->isolate(), "ivLength"),
176+
Int32::New(env->isolate(), iv_length)).IsNothing()) {
177+
return;
178+
}
179+
180+
if (info->Set(
181+
env->context(),
182+
FIXED_ONE_BYTE_STRING(env->isolate(), "keyLength"),
183+
Int32::New(env->isolate(), key_length)).IsNothing()) {
184+
return;
185+
}
186+
187+
args.GetReturnValue().Set(info);
188+
}
50189
} // namespace
51190

52191
void CipherBase::GetSSLCiphers(const FunctionCallbackInfo<Value>& args) {
@@ -151,6 +290,8 @@ void CipherBase::Initialize(Environment* env, Local<Object> target) {
151290
EVP_PKEY_verify_recover_init,
152291
EVP_PKEY_verify_recover>);
153292

293+
env->SetMethodNoSideEffect(target, "getCipherInfo", GetCipherInfo);
294+
154295
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherEncrypt);
155296
NODE_DEFINE_CONSTANT(target, kWebCryptoCipherDecrypt);
156297
}
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const {
8+
getCiphers,
9+
getCipherInfo
10+
} = require('crypto');
11+
12+
const assert = require('assert');
13+
14+
const ciphers = getCiphers();
15+
16+
assert.strictEqual(getCipherInfo(-1), undefined);
17+
assert.strictEqual(getCipherInfo('cipher that does not exist'), undefined);
18+
19+
ciphers.forEach((cipher) => {
20+
const info = getCipherInfo(cipher);
21+
assert(info);
22+
const info2 = getCipherInfo(info.nid);
23+
assert.deepStrictEqual(info, info2);
24+
});
25+
26+
const info = getCipherInfo('aes-128-cbc');
27+
assert.strictEqual(info.name, 'aes-128-cbc');
28+
assert.strictEqual(info.nid, 419);
29+
assert.strictEqual(info.blockSize, 16);
30+
assert.strictEqual(info.ivLength, 16);
31+
assert.strictEqual(info.keyLength, 16);
32+
assert.strictEqual(info.mode, 'cbc');
33+
34+
[null, undefined, [], {}].forEach((arg) => {
35+
assert.throws(() => getCipherInfo(arg), {
36+
code: 'ERR_INVALID_ARG_TYPE'
37+
});
38+
});
39+
40+
[null, '', 1, true].forEach((options) => {
41+
assert.throws(
42+
() => getCipherInfo('aes-192-cbc', options), {
43+
code: 'ERR_INVALID_ARG_TYPE'
44+
});
45+
});
46+
47+
[null, '', {}, [], true].forEach((len) => {
48+
assert.throws(
49+
() => getCipherInfo('aes-192-cbc', { keyLength: len }), {
50+
code: 'ERR_INVALID_ARG_TYPE'
51+
});
52+
assert.throws(
53+
() => getCipherInfo('aes-192-cbc', { ivLength: len }), {
54+
code: 'ERR_INVALID_ARG_TYPE'
55+
});
56+
});
57+
58+
assert(!getCipherInfo('aes-128-cbc', { keyLength: 12 }));
59+
assert(getCipherInfo('aes-128-cbc', { keyLength: 16 }));
60+
assert(!getCipherInfo('aes-128-cbc', { ivLength: 12 }));
61+
assert(getCipherInfo('aes-128-cbc', { ivLength: 16 }));
62+
63+
assert(!getCipherInfo('aes-128-ccm', { ivLength: 1 }));
64+
assert(!getCipherInfo('aes-128-ccm', { ivLength: 14 }));
65+
for (let n = 7; n <= 13; n++)
66+
assert(getCipherInfo('aes-128-ccm', { ivLength: n }));
67+
68+
assert(!getCipherInfo('aes-128-ocb', { ivLength: 16 }));
69+
for (let n = 1; n < 16; n++)
70+
assert(getCipherInfo('aes-128-ocb', { ivLength: n }));

0 commit comments

Comments
 (0)