AES (CFB)
- create
- create("aes-cfb")
- family
- Substitution-permutation
- options
- --key --iv --segment?
- self-inverse
- no
- keyspace
- 2^128, 2^192 or 2^256 keys
- try it
- ciphers aes-cfb "ATTACK AT DAWN" --key 2b7e151628aed2a6abf7158809cf4f3c --iv 000102030405060708090a0b0c0d0e0f
CFB is cipher feedback, another mode from NIST SP 800-38A. It turns AES into something that works like a stream cipher. There's a register, 16 bytes, and it starts as the IV. AES encrypts the register, and the leftmost bits of the result get XORed into the plaintext. What comes out is the ciphertext, and that ciphertext gets shifted into the register from the right. Then again, until the text runs out.
How many bits per step is segment. 128 is the default, a whole block at a time, and it's what openssl enc -aes-128-cfb means. 8 is one byte per step, 1 is one bit. Those two cost a full AES call per byte or per bit. Slow, but some protocols use them anyway.
Two things differ from CBC. AES only runs forward, decryption too, because the keystream is made the same way on both sides. And there's no padding. Fourteen bytes of text give fourteen bytes of ciphertext.
The key is 32, 48 or 64 hex digits, iv is 32 and it's required, same as in CBC. decode wants hex back, any whole number of bytes.
const cfb = create("aes-cfb");
const key = "2b7e151628aed2a6abf7158809cf4f3c";
const iv = "000102030405060708090a0b0c0d0e0f";
cfb.encode("ATTACK AT DAWN", { key, iv }).text; // "11aa338dda2612f78e2973a8cce1"
cfb.encode("ATTACK AT DAWN", { key, iv, segment: 8 }).text; // "11585d087981d10c0863f5b2c8dd"
cfb.decode("11aa338dda2612f78e2973a8cce1", { key, iv }).text; // "ATTACK AT DAWN"
The first byte is 11 in both. No accident, both XOR it with the first byte of AES over the IV. After that the register differs and so does everything else. OpenSSL gives the same bytes, with -aes-128-cfb8 or -aes-128-cfb1 for the smaller segments:
printf %s "ATTACK AT DAWN" | openssl enc -aes-128-cfb -K 2b7e151628aed2a6abf7158809cf4f3c -iv 000102030405060708090a0b0c0d0e0f | xxd -p
The mode is tested against the CFB1, CFB8 and CFB128 vectors from NIST SP 800-38A, F.3.
Flipping bits
The plaintext is XORed with a keystream, so a flipped ciphertext bit flips the same plaintext bit. Nobody needs the key for that. DAWN and DUSK differ by 00140405, XOR that into the last four bytes:
cfb.decode("11aa338dda2612f78e2973bcc8e4", { key, iv }).text;
// "ATTACK AT DUSK"
It worked cleanly because those bytes are the last segment. A flip anywhere earlier also lands in the register, and the next 16 bytes come out as garbage. In CBC the flip goes to the next block and the garbage stays in its own. Here it's the other way around.
Zerologon
CFB-8 with an all-zero IV has one famous hole. Take a key where AES of the zero block starts with a zero byte, that's 1 key in 256. Encrypt zero bytes and the keystream byte is zero, so the ciphertext byte is zero. That zero goes into the register, and the register stays all zeros. Forever.
cfb.encode("\0".repeat(8), { key: "0000000000000000000000000000005f", iv: "0".repeat(32), segment: 8 }).text;
// "0000000000000000"
That's CVE-2020-1472. Netlogon used AES-CFB8 with a zero IV, so sending zeros as the client credential worked for one session key in 256. A few hundred tries and you could log in as any machine account, the domain controller's included. CFB-128 doesn't do this, the whole register is replaced every block.
A key that isn't 32, 48 or 64 hex digits is an InvalidOptionError. So is an iv that isn't 32 hex digits, and a segment that isn't 1, 8 or 128. A missing key or IV is a MissingOptionError. On decode, an odd number of hex digits is a CipherError. With no padding, nothing tells a wrong key apart except the UTF-8 check. Most wrong keys fail it, but a short message can come out as valid garbage. A wrong IV garbles the first 16 bytes and leaves the rest alone.
No integrity check, plain TypeScript, not constant time. For puzzles and for Zerologon on a whiteboard. Don't protect anything real with it.
AES (CBC)
AES with the blocks chained. Every 16-byte block is mixed with the ciphertext before it, so equal blocks stop looking equal. UTF-8 text in, hex out.
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.