Ciphers

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.
create
create("aes-ctr")
family
Substitution-permutation
options
--key --iv
self-inverse
no
keyspace
2^128, 2^192 or 2^256 keys
try it
ciphers aes-ctr "ATTACK AT DAWN" --key 2b7e151628aed2a6abf7158809cf4f3c --iv f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff

CTR is counter mode, from NIST SP 800-38A like CBC and CFB. It's the simplest way to turn AES into a stream cipher. Start with a 16-byte counter block, that's iv. AES encrypts it, and the result gets XORed into the first 16 bytes of text. Then the counter goes up by one, and AES encrypts that for the next 16 bytes. Nothing from the text ever goes back in.

The counter is the whole block read as one 128-bit number. Thirty-two fs plus one is thirty-two zeros, same as in OpenSSL. Some protocols split the block into a nonce and a shorter counter. Here the nonce is just whatever the high bytes of iv are.

No feedback means a few nice things. Encrypting and decrypting are the same XOR, so AES only runs forward. There's no padding, fourteen bytes of text give fourteen bytes of ciphertext. And any block can be computed on its own, you don't need the ones before it. GCM, the mode TLS uses, is CTR with an authentication tag on top.

The key is 32, 48 or 64 hex digits, iv is 32 and it's required. decode wants hex back, any whole number of bytes.

const ctr = create("aes-ctr");
const key = "2b7e151628aed2a6abf7158809cf4f3c";
const iv = "000102030405060708090a0b0c0d0e0f";
ctr.encode("ATTACK AT DAWN", { key, iv }).text; // "11aa338dda2612f78e2973a8cce1"
ctr.decode("11aa338dda2612f78e2973a8cce1", { key, iv }).text; // "ATTACK AT DAWN"

Looks familiar? It's byte for byte what CFB gives for this text. Both XOR the first block with AES over the IV. From the second block on they split, CFB encrypts the ciphertext and CTR encrypts the counter. OpenSSL agrees:

printf %s "ATTACK AT DAWN" | openssl enc -aes-128-ctr -K 2b7e151628aed2a6abf7158809cf4f3c -iv 000102030405060708090a0b0c0d0e0f | xxd -p

The mode is tested against the CTR-AES128, CTR-AES192 and CTR-AES256 vectors from NIST SP 800-38A, F.5.

Flipping bits

A flipped ciphertext bit flips the same plaintext bit, and that's all it does. DAWN and DUSK differ by 00140405:

ctr.decode("11aa338dda2612f78e2973bcc8e4", { key, iv }).text;
// "ATTACK AT DUSK"

In CFB this only worked cleanly in the last segment. Here it works anywhere. Flip the same bytes in ATTACK AT DAWN, RETREAT AT DUSK. and you get ATTACK AT DUSK, RETREAT AT DUSK., no garbage in the next block. CFB throws on the same edit, the second block comes out as bytes that aren't UTF-8.

Same IV twice

This is the one that actually hurts. Same key and same iv means the same keystream. XOR two ciphertexts and the keystream cancels out, what's left is the XOR of the two plaintexts:

ctr.encode("ATTACK AT DAWN", { key, iv }).text; // "11aa338dda2612f78e2973a8cce1"
ctr.encode("ATTACK AT DUSK", { key, iv }).text; // "11aa338dda2612f78e2973bcc8e4"
// XOR: "0000000000000000000000140405"

No key needed. Guess a bit of one message and you read the same bit of the other. It's the two-time pad, and plenty of real CTR and GCM code got broken exactly this way. Every message needs its own counter range.

A key that isn't 32, 48 or 64 hex digits is an InvalidOptionError, so is an iv that isn't 32 hex digits. A missing key or IV is a MissingOptionError. On decode, an odd number of hex digits is a CipherError. No padding means only the UTF-8 check catches a wrong key. Most wrong keys fail it, a short message can still come out as valid garbage.

No integrity check, plain TypeScript, not constant time. For puzzles and for seeing why nonces matter. Don't protect anything real with it.

@agntn/ciphers·MIT license· Classical ciphers, for lessons and puzzles. Not for protecting anything, ever.