AES (CBC-MAC)
- create
- create("aes-cbc-mac")
- family
- Substitution-permutation
- options
- --key
- self-inverse
- no
- keyspace
- 2^128, 2^192 or 2^256 keys
- try it
- ciphers aes-cbc-mac "ATTACK AT DAWN" --key 2b7e151628aed2a6abf7158809cf4f3c
This one doesn't hide anything. The text goes out as plain bytes, and the only new thing is a 16-byte tag on the end that proves whoever wrote it had the key. It's the oldest MAC built from a block cipher, FIPS 113 from 1985 with DES inside, and ISO/IEC 9797-1 calls it MAC Algorithm 1.
The recipe is CBC with the IV set to zeros. Every block gets XORed with the previous result and encrypted, and at the end you throw away everything except the last block. That block is the tag. A text that doesn't fill its last block gets zeros up to 16 bytes, and an empty one becomes one block of zeros.
const mac = create("aes-cbc-mac");
const key = "2b7e151628aed2a6abf7158809cf4f3c";
mac.encode("ATTACK AT DAWN", { key }).text;
// "41545441434b204154204441574e" + "1efa905609cc69e415825c40f80501e8"
mac.decode("41545441434b204154204441574e1efa905609cc69e415825c40f80501e8", { key }).text;
// "ATTACK AT DAWN"
The first fourteen bytes are just ATTACK AT DAWN in hex. Anyone can read them. decode runs the MAC again over those bytes and compares, and only then hands the text back.
Take a text of exactly one block and the tag is the first block that aes-cbc gives with a zero IV:
create("aes-cbc").encode("ATTACK AT DAWN!!", { key, iv: "00".repeat(16) }).text;
// "541ff0c92b9251ae06c624c4a8ab2a85" + "c4c37a672a17ac138199e2ad092b90ae"
mac.encode("ATTACK AT DAWN!!", { key }).text;
// "41545441434b204154204441574e2121" + "541ff0c92b9251ae06c624c4a8ab2a85"
The second block from aes-cbc is its PKCS#7 padding, which the MAC doesn't have.
The options
Only key, 32, 48 or 64 hex digits. No IV, no nonce. The IV has to be zero. An IV that travels with the message lets anyone change the first block and fix the tag by changing the IV the same way. And since nothing is random, the same text under the same key always gets the same tag.
openssl enc has no MAC mode, so the reference is Node's createCipheriv("aes-128-cbc") with a zero IV and padding off, over the zero-padded text, and taking the last block. It matches. The tests also check the last block of the NIST SP 800-38A CBC example and the tag of empty text, which is AES of a zero block, the L from RFC 4493.
What the tag catches
DAWN to DUSK, same as in CCM:
mac.decode("41545441434b204154204455534b1efa905609cc69e415825c40f80501e8", { key });
// CipherError: [aes-cbc-mac] Tag does not match: wrong key, or the text was changed
A new tag for DUSK needs the key. A wrong key ends the same way.
What it doesn't
Zero padding has a hole. ATTACK AT DAWN and ATTACK AT DAWN with two zero bytes on the end pad to the same block, so they get the same tag, and decode takes both:
mac.decode("41545441434b204154204441574e00001efa905609cc69e415825c40f80501e8", { key }).text;
// "ATTACK AT DAWN\0\0"
The bigger one is length. CBC-MAC is only safe when every message has the same length. Know the tag t of a one-block message m, and m followed by m XOR t has the same tag t, no key needed. That's why CMAC exists, and why CCM puts the length into its first block. A lot of CTFs are built on exactly this.
A key that isn't 32, 48 or 64 hex digits is an InvalidOptionError, a missing one is a MissingOptionError. On decode, hex that isn't whole bytes, input shorter than the tag, or a tag that doesn't match is a CipherError.
Plain TypeScript, not constant time. It's here for the forgeries, not for signing anything real.
AES (XTS)
The disk mode that replaced LRW. Every block masked by its sector and its position, a short last block steals from the one before, and nothing is padded. UTF-8 text in, hex out.
Blowfish (ECB)
Schneier's 1993 Feistel cipher, with S-boxes cooked from the key and the digits of pi. Keys from 32 to 448 bits, UTF-8 text in, hex out.