Ciphers

Serpent (ECB)

Anderson, Biham and Knudsen's AES finalist from 1998. Thirty-two rounds of 4-bit S-boxes on 16-byte blocks, keys of 128, 192 or 256 bits. UTF-8 text in, hex out.
create
create("serpent")
family
Substitution-permutation
options
--key
self-inverse
no
keyspace
2^128, 2^192 or 2^256 keys
try it
ciphers serpent "ATTACK AT DAWN" --key 0123456789abcdeffedcba9876543210

Serpent is what Ross Anderson, Eli Biham and Lars Knudsen sent to the AES competition in 1998. It made the final five and came second in the vote at the last AES conference in 2000. Rijndael was faster and won. Serpent was slower and had the bigger safety margin, and that trade is pretty much the whole story of the choice.

The block is 16 bytes, like AES, and it's a substitution-permutation network, like AES too. But the pieces are much smaller. Every round:

  • XORs in a 128-bit round key,
  • runs 32 copies of one 4-bit S-box side by side over the block, a different S-box every round, eight of them in turn,
  • mixes the four 32-bit words with rotations, shifts and XORs.

That's 32 rounds. The last one swaps the mixing for one more round key, so there are 33 of them. The designers said 16 rounds would already stop every attack they knew about, and doubled it anyway.

Bitslice

The trick is how the S-boxes run. Take bit j of each of the four words and you have a nibble. Push it through the S-box and put the four bits back at position j. Do that for all 32 positions and you've done 32 S-boxes at once, with nothing more than bitwise operations on whole words. No table lookups that depend on secret data, no bit permutation at the start or the end. The paper describes a standard mode with those permutations too, and the two give the same ciphertext.

This package does the lookups one nibble at a time instead of with the boolean formulas, because it's easier to read. So it's a teaching implementation, not constant time.

The key schedule stretches the key into 132 words with the golden ratio 0x9e3779b9 and a rotation, then runs every four of them through an S-box, starting from S3 and counting down. A key shorter than 256 bits gets a single 1 bit after it and zeros up to the end.

Keys and bytes

The key is 32, 48 or 64 hex digits, a 128, 192 or 256-bit key. Case doesn't matter and spaces are ignored. Text goes in as UTF-8 with PKCS#7 padding, ciphertext comes out as lowercase hex, and decode wants hex back. The mode is ECB.

Words are little-endian, in the key and in the block, the byte order libgcrypt and Botan use. Some code out there reads them the other way round, and then the same key gives different ciphertext. If a vector doesn't match, check that first.

const serpent = create("serpent");
const key = "0123456789abcdeffedcba9876543210";
serpent.encode("ATTACK AT DAWN", { key }).text; // "33bd9b4c6955d0e186249aeca8b19dbf"
serpent.decode("33bd9b4c6955d0e186249aeca8b19dbf", { key }).text; // "ATTACK AT DAWN"

Checked against

Botan's serpent.vec. The tests run all 960 of its single-bit vectors, one bit set in the key or in the block, for 128, 192 and 256-bit keys, plus a handful of the others both ways. Whole texts with padding are checked against libgcrypt's GCRY_CIPHER_SERPENT in ECB.

How strong is it

Nobody broke the full thing. The best attacks in print reach 12 of the 32 rounds, and even those need absurd amounts of data and time. VeraCrypt still offers it next to AES, for people who want that safety margin.

The block is 128 bits, so the Sweet32 collisions that hit Blowfish and Triple DES aren't a problem here.

Why ECB leaks

Same as every ECB here. Thirty-two As are two equal blocks, and they come out as two equal blocks:

serpent.encode("A".repeat(32), { key }).text;
// 9f4cec54f67f8b775b9dbf076814dd75 9f4cec54f67f8b775b9dbf076814dd75 70f039bebc4127e475704e5e8a1826a7

Spaces added to show the blocks. The last one is only padding.

A key that isn't 32, 48 or 64 hex digits is an InvalidOptionError, a missing one a MissingOptionError. On decode, ciphertext that isn't whole 16-byte blocks of hex is a CipherError. A wrong key almost always breaks the padding, and that's a CipherError too. So are decrypted bytes that aren't UTF-8. Puzzles and learning, not secrets.

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