Skip to content

Commit

Permalink
crypto: add support for AES-CCM
Browse files Browse the repository at this point in the history
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 <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Shigeki Ohtsu <ohtsu@ohtsu.org>
Reviewed-By: Rod Vagg <rod@vagg.org>
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
  • Loading branch information
tniessen committed Apr 6, 2018
1 parent 38a6929 commit 1e07acd
Show file tree
Hide file tree
Showing 5 changed files with 825 additions and 105 deletions.
105 changes: 97 additions & 8 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,17 +241,22 @@ Once the `cipher.final()` method has been called, the `Cipher` object can no
longer be used to encrypt data. Attempts to call `cipher.final()` more than
once will result in an error being thrown.

### cipher.setAAD(buffer)
### cipher.setAAD(buffer[, options])
<!-- YAML
added: v1.0.0
-->
- `buffer` {Buffer}
- `options` {object}
- Returns the {Cipher} for method chaining.

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

The `options` argument is optional for `GCM`. When using `CCM`, the
`plaintextLength` option must be specified and its value must match the length
of the plaintext in bytes. See [CCM mode][].

The `cipher.setAAD()` method must be called before [`cipher.update()`][].

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

Creates and returns a `Cipher` object that uses the given `algorithm` and
`password`. Optional `options` argument controls stream behavior.
`password`.

The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][].

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

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

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

Creates and returns a `Decipher` object that uses the given `algorithm` and
`password` (key). Optional `options` argument controls stream behavior.
`password` (key).

The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][].

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

Creates and returns a `Decipher` object that uses the given `algorithm`, `key`
and initialization vector (`iv`). Optional `options` argument controls stream
behavior.
and initialization vector (`iv`).

The `options` argument controls stream behavior and is optional except when a
cipher in CCM mode is used (e.g. `'aes-128-ccm'`). In that case, the
`authTagLength` option is required and specifies the length of the
authentication tag in bytes, see [CCM mode][].

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

See the reference for other recommendations and details.

### CCM mode

CCM is one of the two supported [AEAD algorithms][]. Applications which use this
mode must adhere to certain restrictions when using the cipher API:

- The authentication tag length must be specified during cipher creation by
setting the `authTagLength` option and must be one of 4, 6, 8, 10, 12, 14 or
16 bytes.
- The length of the initialization vector (nonce) `N` must be between 7 and 13
bytes (`7 ≤ N ≤ 13`).
- The length of the plaintext is limited to `2 ** (8 * (15 - N))` bytes.
- When decrypting, the authentication tag must be set via `setAuthTag()` before
specifying additional authenticated data and / or calling `update()`.
Otherwise, decryption will fail and `final()` will throw an error in
compliance with section 2.6 of [RFC 3610][].
- Using stream methods such as `write(data)`, `end(data)` or `pipe()` in CCM
mode might fail as CCM cannot handle more than one chunk of data per instance.
- When passing additional authenticated data (AAD), the length of the actual
message in bytes must be passed to `setAAD()` via the `plaintextLength`
option. This is not necessary if no AAD is used.
- As CCM processes the whole message at once, `update()` can only be called
once.
- Even though calling `update()` is sufficient to encrypt / decrypt the message,
applications *must* call `final()` to compute and / or verify the
authentication tag.

```js
const crypto = require('crypto');

const key = 'keykeykeykeykeykeykeykey';
const nonce = crypto.randomBytes(12);

const aad = Buffer.from('0123456789', 'hex');

const cipher = crypto.createCipheriv('aes-192-ccm', key, nonce, {
authTagLength: 16
});
const plaintext = 'Hello world';
cipher.setAAD(aad, {
plaintextLength: Buffer.byteLength(plaintext)
});
const ciphertext = cipher.update(plaintext, 'utf8');
cipher.final();
const tag = cipher.getAuthTag();

// Now transmit { ciphertext, tag }.

const decipher = crypto.createDecipheriv('aes-192-ccm', key, nonce, {
authTagLength: 16
});
decipher.setAuthTag(tag);
decipher.setAAD(aad, {
plaintextLength: ciphertext.length
});
const receivedPlaintext = decipher.update(ciphertext, null, 'utf8');

try {
decipher.final();
} catch (err) {
console.error('Authentication failed!');
}

console.log(receivedPlaintext);
```

## Crypto Constants

The following constants exported by `crypto.constants` apply to various uses of
Expand Down Expand Up @@ -2525,7 +2611,9 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[`tls.createSecureContext()`]: tls.html#tls_tls_createsecurecontext_options
[`verify.update()`]: #crypto_verify_update_data_inputencoding
[`verify.verify()`]: #crypto_verify_verify_object_signature_signatureformat
[AEAD algorithms]: https://en.wikipedia.org/wiki/Authenticated_encryption
[Caveats]: #crypto_support_for_weak_or_compromised_algorithms
[CCM mode]: #crypto_ccm_mode
[Crypto Constants]: #crypto_crypto_constants_1
[HTML 5.2]: https://www.w3.org/TR/html52/changes.html#features-removed
[HTML5's `keygen` element]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/keygen
Expand All @@ -2536,6 +2624,7 @@ the `crypto`, `tls`, and `https` modules and are generally specific to OpenSSL.
[OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man1.0.2/apps/spkac.html
[RFC 2412]: https://www.rfc-editor.org/rfc/rfc2412.txt
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
[initialization vector]: https://en.wikipedia.org/wiki/Initialization_vector
[stream-writable-write]: stream.html#stream_writable_write_chunk_encoding_callback
Expand Down
35 changes: 28 additions & 7 deletions lib/internal/crypto/cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const {

const {
ERR_CRYPTO_INVALID_STATE,
ERR_INVALID_ARG_TYPE
ERR_INVALID_ARG_TYPE,
ERR_INVALID_OPT_VALUE
} = require('internal/errors').codes;

const {
Expand Down Expand Up @@ -62,6 +63,16 @@ function getDecoder(decoder, encoding) {
return decoder;
}

function getUIntOption(options, key) {
let value;
if (options && (value = options[key]) != null) {
if (value >>> 0 !== value)
throw new ERR_INVALID_OPT_VALUE(key, value);
return value;
}
return -1;
}

function Cipher(cipher, password, options) {
if (!(this instanceof Cipher))
return new Cipher(cipher, password, options);
Expand All @@ -78,9 +89,11 @@ function Cipher(cipher, password, options) {
);
}

const authTagLength = getUIntOption(options, 'authTagLength');

this._handle = new CipherBase(true);

this._handle.init(cipher, password);
this._handle.init(cipher, password, authTagLength);
this._decoder = null;

LazyTransform.call(this, options);
Expand Down Expand Up @@ -168,13 +181,15 @@ Cipher.prototype.setAuthTag = function setAuthTag(tagbuf) {
return this;
};

Cipher.prototype.setAAD = function setAAD(aadbuf) {
Cipher.prototype.setAAD = function setAAD(aadbuf, options) {
if (!isArrayBufferView(aadbuf)) {
throw new ERR_INVALID_ARG_TYPE('buffer',
['Buffer', 'TypedArray', 'DataView'],
aadbuf);
}
if (this._handle.setAAD(aadbuf) === false)

const plaintextLength = getUIntOption(options, 'plaintextLength');
if (this._handle.setAAD(aadbuf, plaintextLength) === false)
throw new ERR_CRYPTO_INVALID_STATE('setAAD');
return this;
};
Expand Down Expand Up @@ -204,8 +219,10 @@ function Cipheriv(cipher, key, iv, options) {
);
}

const authTagLength = getUIntOption(options, 'authTagLength');

this._handle = new CipherBase(true);
this._handle.initiv(cipher, key, iv);
this._handle.initiv(cipher, key, iv, authTagLength);
this._decoder = null;

LazyTransform.call(this, options);
Expand Down Expand Up @@ -243,8 +260,10 @@ function Decipher(cipher, password, options) {
);
}

const authTagLength = getUIntOption(options, 'authTagLength');

this._handle = new CipherBase(false);
this._handle.init(cipher, password);
this._handle.init(cipher, password, authTagLength);
this._decoder = null;

LazyTransform.call(this, options);
Expand Down Expand Up @@ -288,8 +307,10 @@ function Decipheriv(cipher, key, iv, options) {
);
}

const authTagLength = getUIntOption(options, 'authTagLength');

this._handle = new CipherBase(false);
this._handle.initiv(cipher, key, iv);
this._handle.initiv(cipher, key, iv, authTagLength);
this._decoder = null;

LazyTransform.call(this, options);
Expand Down
Loading

0 comments on commit 1e07acd

Please sign in to comment.