Skip to content

Commit 1e07acd

Browse files
committed
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: #18138 Fixes: #2383 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Shigeki Ohtsu <[email protected]> Reviewed-By: Rod Vagg <[email protected]> Reviewed-By: Daniel Bevenius <[email protected]>
1 parent 38a6929 commit 1e07acd

File tree

5 files changed

+825
-105
lines changed

5 files changed

+825
-105
lines changed

doc/api/crypto.md

+97-8
Original file line numberDiff line numberDiff line change
@@ -241,17 +241,22 @@ Once the `cipher.final()` method has been called, the `Cipher` object can no
241241
longer be used to encrypt data. Attempts to call `cipher.final()` more than
242242
once will result in an error being thrown.
243243

244-
### cipher.setAAD(buffer)
244+
### cipher.setAAD(buffer[, options])
245245
<!-- YAML
246246
added: v1.0.0
247247
-->
248248
- `buffer` {Buffer}
249+
- `options` {object}
249250
- Returns the {Cipher} for method chaining.
250251

251-
When using an authenticated encryption mode (only `GCM` is currently
252+
When using an authenticated encryption mode (only `GCM` and `CCM` are currently
252253
supported), the `cipher.setAAD()` method sets the value used for the
253254
_additional authenticated data_ (AAD) input parameter.
254255

256+
The `options` argument is optional for `GCM`. When using `CCM`, the
257+
`plaintextLength` option must be specified and its value must match the length
258+
of the plaintext in bytes. See [CCM mode][].
259+
255260
The `cipher.setAAD()` method must be called before [`cipher.update()`][].
256261

257262
### cipher.getAuthTag()
@@ -1312,7 +1317,12 @@ deprecated: REPLACEME
13121317
- `options` {Object} [`stream.transform` options][]
13131318

13141319
Creates and returns a `Cipher` object that uses the given `algorithm` and
1315-
`password`. Optional `options` argument controls stream behavior.
1320+
`password`.
1321+
1322+
The `options` argument controls stream behavior and is optional except when a
1323+
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
1324+
`authTagLength` option is required and specifies the length of the
1325+
authentication tag in bytes, see [CCM mode][].
13161326

13171327
The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
13181328
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the
@@ -1353,8 +1363,10 @@ changes:
13531363
- `options` {Object} [`stream.transform` options][]
13541364

13551365
Creates and returns a `Cipher` object, with the given `algorithm`, `key` and
1356-
initialization vector (`iv`). Optional `options` argument controls stream
1357-
behavior.
1366+
The `options` argument controls stream behavior and is optional except when a
1367+
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
1368+
`authTagLength` option is required and specifies the length of the
1369+
authentication tag in bytes, see [CCM mode][].
13581370

13591371
The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
13601372
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the
@@ -1396,7 +1408,12 @@ deprecated: REPLACEME
13961408
- `options` {Object} [`stream.transform` options][]
13971409

13981410
Creates and returns a `Decipher` object that uses the given `algorithm` and
1399-
`password` (key). Optional `options` argument controls stream behavior.
1411+
`password` (key).
1412+
1413+
The `options` argument controls stream behavior and is optional except when a
1414+
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
1415+
`authTagLength` option is required and specifies the length of the
1416+
authentication tag in bytes, see [CCM mode][].
14001417

14011418
The implementation of `crypto.createDecipher()` derives keys using the OpenSSL
14021419
function [`EVP_BytesToKey`][] with the digest algorithm set to MD5, one
@@ -1425,8 +1442,12 @@ changes:
14251442
- `options` {Object} [`stream.transform` options][]
14261443

14271444
Creates and returns a `Decipher` object that uses the given `algorithm`, `key`
1428-
and initialization vector (`iv`). Optional `options` argument controls stream
1429-
behavior.
1445+
and initialization vector (`iv`).
1446+
1447+
The `options` argument controls stream behavior and is optional except when a
1448+
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
1449+
`authTagLength` option is required and specifies the length of the
1450+
authentication tag in bytes, see [CCM mode][].
14301451

14311452
The `algorithm` is dependent on OpenSSL, examples are `'aes192'`, etc. On
14321453
recent OpenSSL releases, `openssl list-cipher-algorithms` will display the
@@ -2167,6 +2188,71 @@ Based on the recommendations of [NIST SP 800-131A][]:
21672188

21682189
See the reference for other recommendations and details.
21692190

2191+
### CCM mode
2192+
2193+
CCM is one of the two supported [AEAD algorithms][]. Applications which use this
2194+
mode must adhere to certain restrictions when using the cipher API:
2195+
2196+
- The authentication tag length must be specified during cipher creation by
2197+
setting the `authTagLength` option and must be one of 4, 6, 8, 10, 12, 14 or
2198+
16 bytes.
2199+
- The length of the initialization vector (nonce) `N` must be between 7 and 13
2200+
bytes (`7 ≤ N ≤ 13`).
2201+
- The length of the plaintext is limited to `2 ** (8 * (15 - N))` bytes.
2202+
- When decrypting, the authentication tag must be set via `setAuthTag()` before
2203+
specifying additional authenticated data and / or calling `update()`.
2204+
Otherwise, decryption will fail and `final()` will throw an error in
2205+
compliance with section 2.6 of [RFC 3610][].
2206+
- Using stream methods such as `write(data)`, `end(data)` or `pipe()` in CCM
2207+
mode might fail as CCM cannot handle more than one chunk of data per instance.
2208+
- When passing additional authenticated data (AAD), the length of the actual
2209+
message in bytes must be passed to `setAAD()` via the `plaintextLength`
2210+
option. This is not necessary if no AAD is used.
2211+
- As CCM processes the whole message at once, `update()` can only be called
2212+
once.
2213+
- Even though calling `update()` is sufficient to encrypt / decrypt the message,
2214+
applications *must* call `final()` to compute and / or verify the
2215+
authentication tag.
2216+
2217+
```js
2218+
const crypto = require('crypto');
2219+
2220+
const key = 'keykeykeykeykeykeykeykey';
2221+
const nonce = crypto.randomBytes(12);
2222+
2223+
const aad = Buffer.from('0123456789', 'hex');
2224+
2225+
const cipher = crypto.createCipheriv('aes-192-ccm', key, nonce, {
2226+
authTagLength: 16
2227+
});
2228+
const plaintext = 'Hello world';
2229+
cipher.setAAD(aad, {
2230+
plaintextLength: Buffer.byteLength(plaintext)
2231+
});
2232+
const ciphertext = cipher.update(plaintext, 'utf8');
2233+
cipher.final();
2234+
const tag = cipher.getAuthTag();
2235+
2236+
// Now transmit { ciphertext, tag }.
2237+
2238+
const decipher = crypto.createDecipheriv('aes-192-ccm', key, nonce, {
2239+
authTagLength: 16
2240+
});
2241+
decipher.setAuthTag(tag);
2242+
decipher.setAAD(aad, {
2243+
plaintextLength: ciphertext.length
2244+
});
2245+
const receivedPlaintext = decipher.update(ciphertext, null, 'utf8');
2246+
2247+
try {
2248+
decipher.final();
2249+
} catch (err) {
2250+
console.error('Authentication failed!');
2251+
}
2252+
2253+
console.log(receivedPlaintext);
2254+
```
2255+
21702256
## Crypto Constants
21712257

21722258
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.
25252611
[`tls.createSecureContext()`]: tls.html#tls_tls_createsecurecontext_options
25262612
[`verify.update()`]: #crypto_verify_update_data_inputencoding
25272613
[`verify.verify()`]: #crypto_verify_verify_object_signature_signatureformat
2614+
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
25282615
[Caveats]: #crypto_support_for_weak_or_compromised_algorithms
2616+
[CCM mode]: #crypto_ccm_mode
25292617
[Crypto Constants]: #crypto_crypto_constants_1
25302618
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
25312619
[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.
25362624
[OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.0.2/apps/spkac.html
25372625
[RFC 2412]: https://www.rfc-editor.org/rfc/rfc2412.txt
25382626
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
2627+
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
25392628
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
25402629
[initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector
25412630
[stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback

lib/internal/crypto/cipher.js

+28-7
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const {
77

88
const {
99
ERR_CRYPTO_INVALID_STATE,
10-
ERR_INVALID_ARG_TYPE
10+
ERR_INVALID_ARG_TYPE,
11+
ERR_INVALID_OPT_VALUE
1112
} = require('internal/errors').codes;
1213

1314
const {
@@ -62,6 +63,16 @@ function getDecoder(decoder, encoding) {
6263
return decoder;
6364
}
6465

66+
function getUIntOption(options, key) {
67+
let value;
68+
if (options && (value = options[key]) != null) {
69+
if (value >>> 0 !== value)
70+
throw new ERR_INVALID_OPT_VALUE(key, value);
71+
return value;
72+
}
73+
return -1;
74+
}
75+
6576
function Cipher(cipher, password, options) {
6677
if (!(this instanceof Cipher))
6778
return new Cipher(cipher, password, options);
@@ -78,9 +89,11 @@ function Cipher(cipher, password, options) {
7889
);
7990
}
8091

92+
const authTagLength = getUIntOption(options, 'authTagLength');
93+
8194
this._handle = new CipherBase(true);
8295

83-
this._handle.init(cipher, password);
96+
this._handle.init(cipher, password, authTagLength);
8497
this._decoder = null;
8598

8699
LazyTransform.call(this, options);
@@ -168,13 +181,15 @@ Cipher.prototype.setAuthTag = function setAuthTag(tagbuf) {
168181
return this;
169182
};
170183

171-
Cipher.prototype.setAAD = function setAAD(aadbuf) {
184+
Cipher.prototype.setAAD = function setAAD(aadbuf, options) {
172185
if (!isArrayBufferView(aadbuf)) {
173186
throw new ERR_INVALID_ARG_TYPE('buffer',
174187
['Buffer', 'TypedArray', 'DataView'],
175188
aadbuf);
176189
}
177-
if (this._handle.setAAD(aadbuf) === false)
190+
191+
const plaintextLength = getUIntOption(options, 'plaintextLength');
192+
if (this._handle.setAAD(aadbuf, plaintextLength) === false)
178193
throw new ERR_CRYPTO_INVALID_STATE('setAAD');
179194
return this;
180195
};
@@ -204,8 +219,10 @@ function Cipheriv(cipher, key, iv, options) {
204219
);
205220
}
206221

222+
const authTagLength = getUIntOption(options, 'authTagLength');
223+
207224
this._handle = new CipherBase(true);
208-
this._handle.initiv(cipher, key, iv);
225+
this._handle.initiv(cipher, key, iv, authTagLength);
209226
this._decoder = null;
210227

211228
LazyTransform.call(this, options);
@@ -243,8 +260,10 @@ function Decipher(cipher, password, options) {
243260
);
244261
}
245262

263+
const authTagLength = getUIntOption(options, 'authTagLength');
264+
246265
this._handle = new CipherBase(false);
247-
this._handle.init(cipher, password);
266+
this._handle.init(cipher, password, authTagLength);
248267
this._decoder = null;
249268

250269
LazyTransform.call(this, options);
@@ -288,8 +307,10 @@ function Decipheriv(cipher, key, iv, options) {
288307
);
289308
}
290309

310+
const authTagLength = getUIntOption(options, 'authTagLength');
311+
291312
this._handle = new CipherBase(false);
292-
this._handle.initiv(cipher, key, iv);
313+
this._handle.initiv(cipher, key, iv, authTagLength);
293314
this._decoder = null;
294315

295316
LazyTransform.call(this, options);

0 commit comments

Comments
 (0)