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 support for AES-CCM #18138

Closed
wants to merge 5 commits 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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