AES (CCM)
- create
- create("aes-ccm")
- family
- Substitution-permutation
- options
- --key --nonce --aad? --tagLength?
- self-inverse
- no
- keyspace
- 2^128, 2^192 or 2^256 keys
- try it
- ciphers aes-ccm "ATTACK AT DAWN" --key 2b7e151628aed2a6abf7158809cf4f3c --nonce 000102030405060708090a0b
Every other AES mode here decrypts whatever you hand it. Flip a bit and you get a flipped bit back, or garbage, and nobody tells you. CCM is the first one that says no. It's Counter with CBC-MAC, from NIST SP 800-38C and RFC 3610, and it's what Wi-Fi (WPA2) and Bluetooth LE encrypt with.
It's two AES passes under one key. The first is a CBC-MAC. AES runs over a header block with the nonce and the length, then over the associated data, then over the text, each block XORed with the last result. What's left at the end is the tag. The second pass is plain CTR. The counter block holds the nonce, counter 1 encrypts the text and counter 0 encrypts the tag. Out comes the ciphertext with the tag glued to its end.
decode goes the other way. It decrypts, runs the CBC-MAC again and compares. If the tag doesn't match, you get an error and no text at all.
const ccm = create("aes-ccm");
const key = "2b7e151628aed2a6abf7158809cf4f3c";
const nonce = "000102030405060708090a0b";
ccm.encode("ATTACK AT DAWN", { key, nonce }).text;
// "9038dc3aa03594330d2d4dca3cb9" + "f3d0bc51521e4e075cf5099b1b92fa10"
ccm.decode("9038dc3aa03594330d2d4dca3cb9f3d0bc51521e4e075cf5099b1b92fa10", { key, nonce }).text;
// "ATTACK AT DAWN"
Fourteen bytes of text, fourteen bytes of ciphertext, sixteen of tag. There's no padding, CTR doesn't need any.
The options
The key is 32, 48 or 64 hex digits. nonce is required, 7 to 13 bytes, so 14 to 26 hex digits. Its length is a trade. The block has 15 bytes for the nonce and the text length together, so a 13-byte nonce leaves two bytes for the length and caps the text at 65535 bytes. A 7-byte one leaves eight.
tagLength is in bits, 32 to 128 in steps of 16, default 128. A shorter tag is cheaper to forge. With 32 bits a random guess passes once in about four billion tries, which is fine for a sensor on a radio and not much else.
aad is associated data, in hex. It's covered by the tag but not encrypted and not in the output. A packet header is the usual case. The router has to read it, but nobody should be able to change it. The receiver needs the same aad, or the tag fails:
const aad = "46524f4d3a2048512e"; // "FROM: HQ." in hex
ccm.encode("ATTACK AT DAWN", { key, nonce, aad }).text;
// "9038dc3aa03594330d2d4dca3cb9a5038633d04da4a01f58149f30e75d5b"
The first fourteen bytes didn't change. Only the tag did. The text still goes through the same keystream, and aad only feeds the MAC.
openssl enc doesn't do AEAD modes, so the reference here is Node's createCipheriv("aes-128-ccm"), which is OpenSSL underneath. It gives the same bytes. The mode is tested against the three examples from NIST SP 800-38C Appendix C and packet vector #1 from RFC 3610.
Flipping bits, again
In CTR, DAWN became DUSK by XORing 00140405 into the right place. Same trick here:
ccm.decode("9038dc3aa03594330d2d4dde38bcf3d0bc51521e4e075cf5099b1b92fa10", { key, nonce });
// CipherError: [aes-ccm] Tag does not match: wrong key, nonce, aad or tagLength, or the ciphertext was changed
The keystream part still decrypts to ATTACK AT DUSK, but decode never shows it. The tag was computed over DAWN, and faking a new one needs the key. A wrong key, a wrong nonce, a different aad or tagLength all end the same way. That's the whole point.
Same nonce twice
The tag doesn't save you from this one. CCM encrypts with CTR, so one nonce used for two messages gives one keystream, and XOR of the ciphertexts is XOR of the texts:
ccm.encode("ATTACK AT DUSK", { key, nonce }).text;
// "9038dc3aa03594330d2d4dde38bc" + "b5b40a703a9773be7cec69f7c601bfe6"
The first eleven bytes match DAWN exactly, ATTACK AT D, and the next three carry the DUSK difference. Every message needs its own nonce. A counter works fine, the nonce only has to be unique, not random.
A key that isn't 32, 48 or 64 hex digits is an InvalidOptionError, so is a nonce outside 14 to 26 hex digits, an aad that isn't whole bytes of hex, or a tagLength off the list. A missing key or nonce is a MissingOptionError. On decode, a ciphertext shorter than the tag or a tag that doesn't match is a CipherError.
Plain TypeScript, not constant time. It's here to show what an authentication tag buys you and what it doesn't. Not for anything real.
AES (CTR)
AES over a counter. Each block of the keystream is the next counter value run through AES, XORed into the text. UTF-8 text in, hex out, no padding, and decrypting is the same step.
AES (OFB)
AES encrypting its own output. The IV goes in, every result goes back in for the next block, and the whole chain gets XORed into the text. UTF-8 text in, hex out, no padding, and decrypting is the same step.