Ciphers

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

OFB is output feedback, the fourth mode in NIST SP 800-38A after ECB, CBC and CFB. It's a keystream like CFB and CTR, made the laziest way possible. AES encrypts the IV. That's the first 16 bytes of keystream. Then AES encrypts that result, and that's the next 16. And again, until the text runs out. Each keystream block gets XORed into its block of text.

What goes back in is AES's own output, never the text and never the ciphertext. So the keystream depends on the key and the IV and nothing else. You could compute a megabyte of it before the message even exists. The price is that block 5 needs blocks 1 to 4 first, so unlike CTR you can't jump straight to the middle.

Like CTR, encrypting and decrypting are the same XOR, AES only runs forward, 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. decode wants hex back, any whole number of bytes.

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

Same bytes as CFB and CTR give for this text. All three start with AES over the IV. The second block is where they part: CFB encrypts the ciphertext, CTR the IV plus one, OFB the first keystream block. OpenSSL agrees:

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

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

Flipping bits

The keystream never sees the ciphertext, so a flipped bit flips the same plaintext bit and nothing else, in any block. DAWN and DUSK differ by 00140405:

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

Works in a longer message too. The same flip turns ATTACK AT DAWN, RETREAT AT DUSK. into ATTACK AT DUSK, RETREAT AT DUSK., second block untouched. There's no integrity check to notice.

Same keystream twice

Same key and same iv, same keystream, and XOR of two ciphertexts is XOR of the two texts, same as in CTR:

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

OFB has one more way to get there. The keystream is a chain, and every link is a valid IV. The first keystream block for the IV above is 50fe67cc996d32b6da0937e99bafec60. Encrypt something with that as iv and you get the old keystream, one block later:

ofb.encode("A".repeat(32), { key, iv }).text.slice(32);
// "98e59b9b49d362de2aca7c37c1a01735"
ofb.encode("A".repeat(16), { key, iv: "50fe67cc996d32b6da0937e99bafec60" }).text;
// "98e59b9b49d362de2aca7c37c1a01735"

So two different IVs can still share keystream, if one sits somewhere in the other's chain. With 2^128 possible blocks that's bad luck, not something that happens to an honest counter or a random IV, and NIST is fine with either. It happens when somebody takes a block that came out of AES under the same key and uses it as the next IV. Hand-made IVs in puzzles do that more often than you'd think.

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 what keystream reuse looks like. Don't protect anything real with it.

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