Ciphers

Rabbit

The eSTREAM stream cipher from RFC 4503. A 128-bit key and an optional 64-bit IV give 16 bytes of keystream per step. UTF-8 text in and hex out with no padding.
IDrabbit41 / 41stream · arx
Cipher / ARX

Rabbit

The eSTREAM stream cipher from RFC 4503, a squaring where the S-boxes would be.

Keyspace
2^128
Decode
same options back
Works on
UTF-8 bytes, hex out
Family
1 in arx
required 1 / 3

Options

keystring
required
32 hex digits, a 128-bit key
ivstring
optional
Initialization vector, 16 hex digits; without it the IV setup is skipped
endianstring
default big
Byte order of key, IV and keystream: big as in RFC 4503 and CyberChef, little as in Crypto++

Access

Createcreate("rabbit")
CLIciphers encode rabbit 'ATTACK AT DAWN' --key 912813292e3d36fe3bfc62f1dc51c3ac
Tryplayground with the sample above

Rabbit comes from Denmark. Martin Boesgaard, Mette Vesterager, Thomas Pedersen, Jesper Christiansen and Ove Scavenius showed it at FSE in 2003. It went into eSTREAM in 2005 and made the final portfolio of software ciphers in 2008. RFC 4503 has described it since 2006. For years you needed a license to use it commercially, until the designers put it in the public domain in October 2008.

The name fits. It was built to be fast in software, a few cycles per byte on a Pentium 3, back when that mattered to anyone.

How it runs

The state is 513 bits. Eight 32-bit state words, eight 32-bit counter words, and one carry bit that links the counters together.

Every step does two things:

  • adds fixed constants to the counters, 0x4D34D34D, 0xD34D34D3 and 0x34D34D34 in turn, carrying from one counter to the next,
  • runs every state word plus its counter through g and mixes the eight results back into the state with additions and rotations by 8 and 16 bits.

g is the whole trick. Add two words, square the sum into 64 bits, XOR the high half with the low half. No S-boxes, no tables. Then 16 bytes of keystream get pulled out of the state, each 16-bit piece the XOR of halves from two different words.

The key fills the state and the counters, and four steps shake it up. The IV is optional. With one, it gets XORed into the counters and four more steps run. Without it you skip that part, and then you must never start the cipher twice with the same key, RFC 4503 says so in §3.2.

Keys and bytes

The key is 32 hex digits and iv is 16. Case doesn't matter and spaces are ignored. Text goes in as UTF-8, ciphertext comes out as lowercase hex, and decode wants hex back.

ts
const rabbit = create("rabbit");
const key = "912813292e3d36fe3bfc62f1dc51c3ac";
rabbit.encode("ATTACK AT DAWN", { key }).text; // "b29c6ab764eac93e97a4c3a306d2"
rabbit.decode("b29c6ab764eac93e97a4c3a306d2", { key }).text; // "ATTACK AT DAWN"

That key is the second one from Appendix A of RFC 4503, and this ciphertext can be checked by hand. The last 14 bytes of its S[0], F3 C8 3E F6 ..., XORed with ATTACK AT DAWN. Why the last 14 and not the first, that's the next section.

With an IV, the same check works against A.2:

ts
rabbit.encode("ATTACK AT DAWN", { key: "0".repeat(32), iv: "c373f575c1267e59" }).text;
// "0fed0c4151a9c09d98b266402a23"

Which end is first

This is where Rabbit code out there disagrees, and it's not a bug on either side. RFC 4503 treats the key, the IV and every keystream block as one big number, and writes it most significant byte first. The eSTREAM reference code reads the same bytes least significant first. Crypto++, wolfSSL and libtomcrypt follow the reference. Same key bytes, different ciphertext.

endian picks the order. big is the RFC's and the default, and it's also what CyberChef does by default. little gives what Crypto++ gives:

ts
rabbit.encode("ATTACK AT DAWN", { key, endian: "little" }).text; // "32214a7fa92d752007004367e552"

rabbit.encode("Rabbit stream cipher test", {
  key: "23c2731e8b5469fd8dabb5bc592a0f3a",
  iv: "712906405ef03201",
  endian: "little",
}).text;
// "1ae2d4edcf9b6063b00fd6fda0b223aded157e77031cf0440b", the first example on the Crypto++ wiki

A short last block takes the least significant bytes of its keystream, as §2.8 says. With little those are the first bytes of the block, with big the last. That's why a 14-byte text above lines up with the end of S[0], and why CyberChef's own test encrypts eight zero bytes to f56b45261c4af702, the back half of the first block.

If a vector doesn't match, flip endian before anything else.

Checked against

All six keystreams in Appendix A of RFC 4503, three keys without an IV and three IVs under the zero key, both ways. Little-endian output against Crypto++ 8.9 Rabbit and RabbitWithIV, and CyberChef's Rabbit tests for the short last block and the Crypto++ wiki example.

One trap if you check the RFC yourself. Appendix B prints the key as 91 28 13 29 2E ED ..., while Appendix A has 2E 3D there and the state words in B, X1 = 0x13292E3D, agree with A. The ED is a typo.

Same IV twice

Same key and same IV means the same keystream. XOR two ciphertexts and it cancels out:

ts
rabbit.encode("ATTACK AT DAWN", { key }).text; // "b29c6ab764eac93e97a4c3a306d2"
rabbit.encode("ATTACK AT DUSK", { key }).text; // "b29c6ab764eac93e97a4c3b702d7"
// XOR: "0000000000000000000000140405"

That's DAWN XOR DUSK, no key involved. The two-time pad, the same leak as in CTR. Every message wants its own IV. And with 64 bits of IV there are only so many of them, so someone attacking lots of keys at once gets down to about 96 bits of work instead of 128.

How strong is it

Nobody broke it. Aumasson found a small bias in the output in 2006, a distinguisher at 2^247. Lu, Wang and Ling pushed it down to 2^158 in 2008, still far above just trying all 2^128 keys.

A key that isn't 32 hex digits is an InvalidOptionError, so is an iv that isn't 16 hex digits or an endian that isn't big or little. A missing key is a MissingOptionError, a missing IV just skips the IV setup. On decode, an odd number of hex digits is a CipherError. No padding means only the UTF-8 check catches a wrong key or the wrong endian, and a short message can still slip through as valid garbage.

No integrity check, plain TypeScript, BigInt in the squaring, not constant time. It's here for the puzzle that uses Rabbit and for seeing how a stream cipher works. Real data wants your platform's crypto.