Serpent (ECB)
- 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.