When I first started reading about symmetric block ciphers, Twofish kept coming up as one of the finalists of the AES competition, and I think it deserves attention in its own right. Twofish is a symmetric-key block cipher, meaning I use the same key to encrypt and decrypt data. It operates on 128-bit blocks and supports key sizes of 128, 192, or 256 bits. What drew me to it is that it was designed with both security and flexibility in mind, and it remains unbroken in any practical sense even decades after its creation.
History and Background
I trace Twofish back to 1998, when it was designed by a team led by Bruce Schneier, along with John Kelsey, Doug Whiting, David Wagner, Chris Hall, and Niels Ferguson. The team built Twofish as a successor to their earlier cipher, Blowfish, and submitted it to the National Institute of Standards and Technology (NIST) as a candidate for the Advanced Encryption Standard (AES). Twofish made it into the final five candidates, alongside Rijndael, Serpent, RC6, and MARS. Rijndael was eventually chosen as AES, but I find it worth noting that Twofish was never cracked during the competition, and its performance and security margins were considered excellent. Because it is unpatented and placed in the public domain, I can use it freely without licensing concerns.
Problem Statement
The problem Twofish addresses is the same one every symmetric cipher tackles: I need a way to transform plaintext into ciphertext such that anyone without the secret key cannot feasibly recover the original message, while someone with the key can decrypt it quickly and reliably. Additionally, the cipher needed to run efficiently on the wide variety of hardware that existed in the late 1990s, from smart cards with very limited memory to powerful desktop processors, and it needed to resist the cryptanalytic attacks known at the time, including differential and linear cryptanalysis.
Core Concepts
Before I explain how Twofish works, I want to lay out a few terms I rely on throughout:
- Feistel network: a structure that splits data into halves and repeatedly applies a round function to one half, mixing it with the other, across many rounds.
- S-box (substitution box): a lookup table that takes an input and produces a substituted output, providing non-linearity that resists linear cryptanalysis.
- Key-dependent S-boxes: unlike fixed S-boxes, Twofish generates its S-boxes from the key itself, so the substitution behavior changes depending on the key I use.
- MDS matrix (Maximum Distance Separable matrix): a matrix used to diffuse bit changes across bytes, ensuring a single-bit change in the input spreads widely through the output.
- Pseudo-Hadamard Transform (PHT): a simple, fast mixing operation Twofish uses to combine two 32-bit words.
- Whitening: XORing the plaintext or ciphertext with key material before the first round and after the last round, which adds an extra layer of protection.
How It Works
I like to think of Twofish’s overall structure as a 16-round Feistel network operating on 128-bit blocks, split into four 32-bit words. Here is the flow I follow when tracing an encryption:
- I take the 128-bit plaintext block and split it into four 32-bit words.
- I apply input whitening, XORing each of the four words with a subkey.
- I run 16 rounds, where in each round two of the words pass through the “g function” (built from key-dependent S-boxes and the MDS matrix), the results are combined using the Pseudo-Hadamard Transform, and then mixed with round subkeys before being XORed into the other two words, followed by a swap of halves (except after the final round).
- After the 16 rounds, I undo the final swap and apply output whitening, XORing the words with more subkeys.
- The four words are recombined into the 128-bit ciphertext block.
Working Principle
The internal logic of Twofish hinges on its g function, which is the heart of each round. I feed a 32-bit word into the g function, and it splits that word into four bytes. Each byte passes through a key-dependent S-box (built specifically from the encryption key using Reed-Solomon-like code techniques), producing four substituted bytes. These four bytes are then multiplied by a fixed 4×4 MDS matrix over a finite field, producing a new 32-bit word with high diffusion. Two such g function outputs are combined using the Pseudo-Hadamard Transform, which simply adds the two words together in specific combinations (one addition and one addition after doubling), and then round keys are added. This mixture of key-dependent substitution and fixed linear diffusion is what gives Twofish resistance to both linear and differential cryptanalysis.
Mathematical Foundation
The Pseudo-Hadamard Transform I mentioned works on two 32-bit words $a$ and $b$ as follows:
$$a’ = a + b \pmod{2^{32}}$$
$$b’ = a + 2b \pmod{2^{32}}$$
The MDS matrix multiplication is performed over the finite field $GF(2^8)$, where each byte is treated as an element of the field, and the matrix $M$ (a fixed 4×4 matrix of field elements) transforms an input vector $v$ of four bytes into an output vector:
$$v’ = M \cdot v \pmod{p(x)}$$
where $p(x)$ is the irreducible polynomial defining the field. The key schedule itself relies on Reed-Solomon code properties to generate the S-box keys, expressed as:
$$S_i = \text{RS} \cdot M_i$$
where $M_i$ represents key material and RS is a fixed Reed-Solomon generator matrix. I don’t need to derive this from scratch to use Twofish, but understanding that the S-boxes are mathematically derived (not arbitrary) helps explain why they resist known attacks.
Diagrams
flowchart TD
A[128-bit Plaintext] --> B[Split into 4x 32-bit words]
B --> C[Input Whitening XOR]
C --> D[Round 1: g function + PHT + Key Mix]
D --> E[Round 2 ... Round 16]
E --> F[Undo Final Swap]
F --> G[Output Whitening XOR]
G --> H[128-bit Ciphertext]Pseudocode
function TWOFISH_ENCRYPT(plaintext[128 bits], key):
words = split_into_four_32bit_words(plaintext)
words = whiten(words, key.whitening_subkeys[0..3])
for round in 1 to 16:
T0 = g_function(words[0], key)
T1 = g_function(rotate_left(words[1], 8), key)
F0 = (T0 + T1 + key.round_subkeys[2*round]) mod 2^32
F1 = (T0 + 2*T1 + key.round_subkeys[2*round+1]) mod 2^32
words[2] = rotate_right(words[2] XOR F0, 1)
words[3] = (words[3] XOR (F1))
if round < 16:
swap(words[0], words[2])
swap(words[1], words[3])
words = whiten(words, key.whitening_subkeys[4..7])
ciphertext = combine_words(words)
return ciphertext
Step-by-Step Example
To keep this concrete, I will walk through the conceptual flow with a simplified illustration rather than raw hex output, since a full byte-level trace would span pages of S-box lookups.
Suppose I have a 128-bit plaintext block P and a 128-bit key K.
- I split
Pinto four words:P0, P1, P2, P3. - I XOR each with whitening subkeys derived from
K, givingA0, A1, A2, A3. - In round 1, I pass
A0through the g function, getting a diffused 32-bit valueT0. I rotateA1by 8 bits and pass it through g, gettingT1. - I combine
T0andT1with the PHT, add round subkeys, and XOR the results intoA2andA3. - I swap the word pairs and repeat this for 16 rounds total, each time deriving new round subkeys from
K‘s schedule. - After round 16, I skip the final swap, apply output whitening, and concatenate the four words into the final 128-bit ciphertext
C.
Decryption simply reverses this process, running the rounds in opposite order and using the inverse of each operation, since Twofish’s Feistel structure guarantees this reversal recovers P exactly.
Time Complexity
Twofish always performs a fixed 16 rounds regardless of input, so its time complexity for encrypting or decrypting a single block is $O(1)$ with respect to input size, since the block size is fixed at 128 bits. For encrypting a message of $n$ blocks, the complexity is $O(n)$, since each block requires the same constant amount of work. In practice, I have seen Twofish run at roughly comparable speeds to AES on general-purpose CPUs, sometimes slightly slower due to its more complex key schedule.
Space Complexity
The memory footprint depends mostly on the precomputed key-dependent S-boxes and subkeys, which together take a modest amount of memory, typically under a few kilobytes. Twofish requires $O(1)$ additional space per block being processed, since it operates in place on the four 32-bit words. The key schedule setup itself takes $O(1)$ space relative to message size, since it only depends on the key length, not on the amount of data being encrypted.
Correctness Analysis
I find the correctness of Twofish easiest to reason about through its Feistel structure. In a Feistel network, each round applies a function to one half of the data and XORs the result into the other half, then swaps the halves. Because XOR is its own inverse, and because I know the exact round function used in the forward direction, I can always reverse a Feistel round exactly by running the same function again and XORing it back out, then swapping. Applying this in reverse order across all 16 rounds guarantees decryption recovers the exact original plaintext, provided the same key and key schedule are used. This is a well-established property of Feistel ciphers in general, and Twofish’s designers built on this proven foundation rather than inventing a new paradigm.
Advantages
- I appreciate that Twofish is fast in both hardware and software implementations.
- It is free of any patent, so I can use it without licensing fees.
- Its key-dependent S-boxes add an extra layer of complexity for attackers compared to fixed S-box ciphers.
- It supports flexible key lengths of 128, 192, and 256 bits.
- It has withstood over two decades of cryptanalysis without a practical break.
Disadvantages
- The key setup phase is relatively expensive compared to simpler ciphers, which can matter in applications that change keys frequently.
- It never became as widely adopted as AES, so I find fewer optimized libraries and hardware accelerators for it compared to AES.
- Its more complex structure makes it harder to implement correctly from scratch compared to simpler ciphers like DES.
Applications
I have seen Twofish used in several practical contexts: it is one of the ciphers available in the OpenPGP standard for encrypting emails and files, it appears as an option in disk encryption tools like TrueCrypt and its successor VeraCrypt, and password managers such as KeePass support it as an alternative encryption backend to AES. It is also included in various SSH and VPN implementations as a selectable cipher.
Implementation in C
#include <stdio.h>
#include <stdint.h>
#include <string.h>
/* This is a simplified illustrative skeleton of Twofish's round
structure. A production implementation requires the full
key-dependent S-box generation and MDS matrix tables, which are
too large to reproduce here in full, so I focus on showing the
round mechanics clearly. */
#define ROUNDS 16
uint32_t rotl32(uint32_t x, int r) {
return (x << r) | (x >> (32 - r));
}
uint32_t rotr32(uint32_t x, int r) {
return (x >> r) | (x << (32 - r));
}
/* Placeholder g-function: in real Twofish this uses key-dependent
S-boxes and an MDS matrix multiply over GF(2^8). */
uint32_t g_function(uint32_t word, uint32_t *sbox_key) {
uint8_t b0 = (word >> 0) & 0xFF;
uint8_t b1 = (word >> 8) & 0xFF;
uint8_t b2 = (word >> 16) & 0xFF;
uint8_t b3 = (word >> 24) & 0xFF;
/* Substitute (placeholder XOR instead of real S-box lookup) */
b0 ^= (sbox_key[0] & 0xFF);
b1 ^= (sbox_key[1] & 0xFF);
b2 ^= (sbox_key[2] & 0xFF);
b3 ^= (sbox_key[3] & 0xFF);
/* Placeholder MDS-like mixing */
uint32_t result = ((uint32_t)b0) | ((uint32_t)b1 << 8) |
((uint32_t)b2 << 16) | ((uint32_t)b3 << 24);
return result;
}
void twofish_encrypt_block(uint32_t words[4], uint32_t round_keys[ROUNDS][2],
uint32_t whitening[8]) {
/* Input whitening */
words[0] ^= whitening[0];
words[1] ^= whitening[1];
words[2] ^= whitening[2];
words[3] ^= whitening[3];
for (int round = 0; round < ROUNDS; round++) {
uint32_t dummy_key[4] = {round_keys[round][0], round_keys[round][0],
round_keys[round][0], round_keys[round][0]};
uint32_t t0 = g_function(words[0], dummy_key);
uint32_t t1 = g_function(rotl32(words[1], 8), dummy_key);
uint32_t f0 = t0 + t1 + round_keys[round][0];
uint32_t f1 = t0 + 2 * t1 + round_keys[round][1];
words[2] = rotr32(words[2] ^ f0, 1);
words[3] = words[3] ^ f1;
if (round < ROUNDS - 1) {
uint32_t tmp;
tmp = words[0]; words[0] = words[2]; words[2] = tmp;
tmp = words[1]; words[1] = words[3]; words[3] = tmp;
}
}
/* Output whitening */
words[0] ^= whitening[4];
words[1] ^= whitening[5];
words[2] ^= whitening[6];
words[3] ^= whitening[7];
}
int main() {
uint32_t words[4] = {0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210};
uint32_t round_keys[ROUNDS][2];
uint32_t whitening[8];
for (int i = 0; i < ROUNDS; i++) {
round_keys[i][0] = 0xA5A5A5A5 + i;
round_keys[i][1] = 0x5A5A5A5A + i;
}
for (int i = 0; i < 8; i++) {
whitening[i] = 0x11111111 * (i + 1);
}
printf("Before: %08X %08X %08X %08X\n",
words[0], words[1], words[2], words[3]);
twofish_encrypt_block(words, round_keys, whitening);
printf("After: %08X %08X %08X %08X\n",
words[0], words[1], words[2], words[3]);
return 0;
}
Sample Input and Output
Given a plaintext block represented as the four 32-bit words 01234567 89ABCDEF FEDCBA98 76543210 and the illustrative key schedule in the code above, running the program produces transformed output words that differ substantially from the input, demonstrating the diffusion effect of just 16 rounds. In a full, correct implementation with real S-boxes and MDS tables, the ciphertext would be indistinguishable from random data without knowledge of the key, and decrypting with the same key would recover the exact original words.
Optimization Techniques
I have found a few techniques that help when implementing or using Twofish efficiently:
- Precomputing the key-dependent S-boxes once per key and caching them, rather than regenerating them for every block.
- Using table-based lookups combined with the MDS matrix to speed up the g function, similar to how AES implementations use T-tables.
- Taking advantage of SIMD instructions on modern CPUs to process multiple blocks in parallel when encrypting large amounts of data.
- Choosing the 128-bit key variant when maximum speed matters more than the largest possible security margin, since key setup and round key generation scale with key size.
Common Mistakes
From what I have read and seen discussed in cryptography communities, common mistakes include reusing the same key and initialization vector across multiple encryption operations in modes like CBC, which can leak information about plaintext patterns; implementing the MDS matrix or S-box generation incorrectly, which silently weakens the cipher without causing obvious errors; and confusing Twofish with Blowfish, given their similar names and shared design lineage, despite very different internal structures and security properties.
Further Reading
- Schneier, B., Kelsey, J., Whiting, D., Wagner, D., Hall, C., and Ferguson, N. “Twofish: A 128-Bit Block Cipher.” https://www.schneier.com/academic/twofish/
- NIST AES competition archive: https://csrc.nist.gov/projects/cryptographic-standard-and-guidelines/archived-crypto-projects/aes-development
- Schneier on Security, Twofish page: https://www.schneier.com/academic/twofish/
- “Applied Cryptography” by Bruce Schneier, published by Wiley.