Ciphers

AES (LRW)

AES with a tweak. Every 16-byte block is masked by its own position, so equal blocks stop looking equal. UTF-8 text in, hex out.
create
create("aes-lrw")
family
Substitution-permutation
options
--key --tweak?
self-inverse
no
keyspace
2^256, 2^320 or 2^384 keys (the AES key plus a 128-bit tweak key)
try it
ciphers aes-lrw "ATTACK AT DAWN" --key 4562ac25f828176d4c268414b5680185258e2a05e73e9d03ee5a830ccc094c87

LRW is Liskov, Rivest and Wagner, the tweakable mode the IEEE P1619 drafts picked for disk sectors. It's AES with one extra step around every block. Block number i gets a mask T = K2 ⊗ i, a multiplication in GF(2^128). The block is XORed with T, encrypted, and XORed with T again. Same AES key, but a different mask at every position.

The key is hex and carries both parts. First the AES key, 32, 48 or 64 digits, then 32 more digits for the tweak key K2. So 64, 80 or 96 digits in total, the same layout Linux uses for lrw(aes). tweak is the index of the first block, up to 32 hex digits, default 1. The next block is one more, and after ff…ff it wraps to 0. Text and ciphertext work as in AES: UTF-8 with PKCS#7 padding in, lowercase hex out, and decode wants hex back.

const lrw = create("aes-lrw");
const key = "4562ac25f828176d4c268414b5680185258e2a05e73e9d03ee5a830ccc094c87";
lrw.encode("ATTACK AT DAWN", { key }).text; // "1b3e1004e52c450fda5b2bca03bca338"
lrw.encode("ATTACK AT DAWN", { key, tweak: "200000000" }).text; // "f319a072c39498a1e0c6c8213de2facc"
lrw.decode("1b3e1004e52c450fda5b2bca03bca338", { key }).text; // "ATTACK AT DAWN"

OpenSSL has no LRW, so these bytes come from the Linux kernel's own lrw(aes) through AF_ALG, with the padding added before it. The block function is tested against the IEEE P1619 LRW-AES vectors that the kernel's testmgr.h carries, the counter wrap at 2^128 included.

What the tweak fixes

Thirty-two As again, two equal blocks of plaintext. In ECB that's two equal blocks of ciphertext. Here:

lrw.encode("A".repeat(32), { key }).text;
// 709a0bafa03db466980baedc25c44ea7 c622af990436ec152db2ae69dc4d779d 52b4776b71f7dd0556849ba9297439ac

Spaces added here to show the blocks. Nothing repeats, because block 1 and block 2 got different masks. But move the same plaintext block to the same position under the same key and the ciphertext is the same again. That's by design, a disk sector encrypts the same way every time it's written. LRW hides which blocks repeat inside a message, not whether a message changed.

A key that isn't 64, 80 or 96 hex digits is an InvalidOptionError, and so is a tweak that isn't 1 to 32 hex digits. A missing key is a MissingOptionError. On decode, hex that isn't whole 16-byte blocks is a CipherError. So is a wrong key or a wrong tweak, since the padding almost never survives either.

IEEE dropped LRW from the final P1619 and went with XTS. LRW breaks when the tweak key ends up encrypted under itself, which is easy to do on a disk. It also has no integrity check, so flipped bits decrypt to garbage without a word. Same as the ECB ciphers, this is plain TypeScript, not constant time, for puzzles and for seeing what a tweak does. Don't protect anything real with it.

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