Blowfish (ECB)
- create
- create("blowfish")
- family
- Feistel
- options
- --key
- self-inverse
- no
- keyspace
- 2^32 to 2^448 keys
- try it
- ciphers blowfish "ATTACK AT DAWN" --key 0123456789abcdeff0e1d2c3b4a59687
Bruce Schneier published Blowfish in 1993 as a free replacement for DES, no patent and no license. The block is 64 bits, like DES. The key is anything from 32 to 448 bits, here as hex: an even number of digits from 8 to 112. 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, same as Triple DES.
const blowfish = create("blowfish");
const key = "0123456789abcdeff0e1d2c3b4a59687";
blowfish.encode("ATTACK AT DAWN", { key }).text; // "9e16058420b1546315051882f350a136"
blowfish.decode("9e16058420b1546315051882f350a136", { key }).text; // "ATTACK AT DAWN"
OpenSSL agrees, but since 3.0 Blowfish lives in the legacy provider, so ask for it:
printf %s "ATTACK AT DAWN" | openssl enc -bf-ecb -provider legacy -provider default -K 0123456789abcdeff0e1d2c3b4a59687 | xxd -p
The tests run Eric Young's vectors, the ones Schneier ships with the reference code. 34 blocks under 8-byte keys, then one block under keys from 4 to 24 bytes. The 448-bit key is checked against PyCryptodome, because OpenSSL's command line cuts -K to 16 bytes.
Where the S-boxes come from
DES has eight fixed S-boxes, and for years people asked who picked them and why. Blowfish has no such question. It starts from the hex digits of pi: 18 words for the round keys, 1024 for four S-boxes of 256 words. Nobody chose them, so nobody could have hidden anything in them.
Then the key goes in. It gets XORed into the 18 round keys, repeated as many times as it takes. After that Blowfish encrypts a block of zeros and writes the result over the first two round keys. Then it encrypts that result and overwrites the next two. And so on, through the round keys and all four S-boxes, 521 encryptions in total. At the end every key has its own S-boxes.
That's also why a new key is expensive. Each one costs those 521 encryptions before the first real block. bcrypt took this on purpose and made it slower still, so guessing passwords gets expensive.
The pi words aren't pasted into the source. The library computes them once, on the first key, with Machin's formula in BigInt. It takes a few dozen milliseconds, and after that they sit in memory.
Why ECB leaks
Sixteen As are two equal blocks, and they come out as two equal blocks:
blowfish.encode("A".repeat(16), { key }).text;
// 8e9fdf91ed9fbd73 8e9fdf91ed9fbd73 10c9d9248e4c6405
Spaces added to show the blocks. The last one is only padding.
A key that isn't 8 to 112 hex digits, or has an odd number of them, is an InvalidOptionError. A missing one is a MissingOptionError. On decode, ciphertext that isn't whole 8-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.
Nobody has broken full 16-round Blowfish. The block is the problem. Around 2^32 blocks under one key, two ciphertext blocks start to collide, and a collision leaks plaintext. Sweet32 did exactly that in 2016, to 3DES in HTTPS and to Blowfish in OpenVPN. Schneier himself has been telling people to use Twofish instead since 2007. This one is plain TypeScript, not constant time. Puzzles and learning, not secrets.