Block Ciphers and the Data Encryption Standard (DES)
You're sending a 1000-character message, but your cipher only works on 8 bytes at a time. Split the message into chunks, encrypt each chunk the same way, and you've just built a block cipher. This is the foundation almost every modern encryption standard is built on, including the one that ran the world's financial systems for over two decades.
A block cipher encrypts a fixed-size block of plaintext (commonly 64 or 128 bits) into a block of ciphertext of the same size, using a key. Contrast this with a stream cipher, which encrypts data one bit or byte at a time.
flowchart TD
A["Plaintext Data"] --> B{"Encryption Approach"}
B -->|"Block Cipher"| C["Split into Fixed-Size Blocks"]
C --> D["Encrypt Each Block with Key + Round Function"]
D --> E["Ciphertext Blocks"]
B -->|"Stream Cipher"| F["Generate Keystream Bit by Bit"]
F --> G["XOR Keystream with Plaintext Bit by Bit"]
G --> H["Continuous Ciphertext Stream"]
Block Cipher vs Stream Cipher
| Feature | Block Cipher | Stream Cipher |
|---|---|---|
| Unit of encryption | Fixed-size blocks (e.g. 64 or 128 bits) | One bit/byte at a time |
| Speed | Slower, more computation per block | Faster, lightweight |
| Error propagation | Errors can spread across a block | Errors stay localized |
| Memory requirement | Higher, needs to buffer a full block | Lower, processes as data arrives |
| Typical use case | File encryption, disk encryption, database fields | Real-time audio/video, network streams |
| Example | DES, AES, Blowfish | RC4, Salsa20 |
Block Cipher Principles
Before DES existed, cipher designers had to agree on what actually makes a block cipher secure. A few principles guide every block cipher design since:
- Block size: larger blocks give more security but cost more computation. 64 bits was the historical standard; 128 bits is now common in AES.
- Key size: longer keys resist brute force better. DES's 56-bit key is now considered too short; modern ciphers use 128 bits or more.
- Number of rounds: more rounds increase diffusion, but each round adds computational cost. DES settled on 16 as a security/performance tradeoff.
- Subkey generation algorithm: the algorithm that derives round keys from the main key should itself be hard to reverse, so recovering one subkey shouldn't reveal the main key.
- Round function complexity: a more complex round function is harder to analyze, but slower to compute. There's a constant tension between speed and resistance to cryptanalysis.
[!NOTE]
These principles aren't unique to DES. AES, Blowfish, and every modern block cipher get evaluated against the same tradeoffs: block size, key size, rounds, and round function strength.
Confusion and Diffusion
Claude Shannon defined two properties that any strong cipher must have, and every block cipher design traces back to satisfying them.
Confusion: makes the relationship between the key and the ciphertext as complex as possible, so an attacker can't reverse-engineer the key from statistical patterns in the ciphertext. Achieved mainly through substitution (S-boxes).
Diffusion: spreads the influence of a single plaintext bit across many ciphertext bits, so a small change in input produces a scattered, unpredictable change in output. Achieved mainly through permutation and repeated rounds.
Without confusion: the key-ciphertext relationship stays close to linear, and an attacker can solve for the key using algebraic methods
With confusion: the relationship becomes nonlinear, and simple algebraic attacks fail outright
flowchart LR
K["Secret Key"] --> S["Substitution (S-boxes)"]
S --> Conf["Confusion:\nComplex Key-Ciphertext Relationship"]
P["Plaintext Bit"] --> Perm["Permutation + Repeated Rounds"]
Perm --> Diff["Diffusion:\nOne Bit Change Affects Many Output Bits"]
Conf --> Strong["Cryptographically Strong Cipher"]
Diff --> Strong
The Feistel Structure
Designing a cipher that is both easy to encrypt and easy to decrypt, while still being hard to break, is tricky. Horst Feistel solved this with a structure that splits each block in half and processes it through multiple rounds.
- Left half (L) and right half (R): the block is split down the middle
- Round function F: takes the right half and a subkey, produces an output that gets XORed with the left half
- Swap: the halves swap positions before the next round
The elegance of a Feistel cipher is that decryption uses the exact same structure as encryption, just with the subkeys applied in reverse order. You don't need a separate decryption algorithm, which halves the implementation work in hardware.
flowchart TD
P["64-bit Plaintext Block"] --> IP["Initial Permutation (IP)"]
IP --> L0["L0 (32 bits)"]
IP --> R0["R0 (32 bits)"]
R0 --> F1["Round Function F, Key K1"]
L0 --> X1["XOR"]
F1 --> X1
X1 --> R1["R1 = L0 XOR F(R0, K1)"]
R0 --> L1["L1 = R0"]
R1 --> F2["Round Function F, Key K2"]
L1 --> X2["XOR"]
F2 --> X2
X2 --> R2["R2"]
R1 --> L2["L2"]
R2 -.->|"14 more rounds using K3...K16"| R16["R16"]
L2 -.-> L16["L16"]
R16 --> FP["Final Permutation (IP inverse)"]
L16 --> FP
FP --> C["64-bit Ciphertext Block"]
Internal Structure of a Single DES Round
The Data Encryption Standard (DES) applies the Feistel structure to a 64-bit block using a 56-bit key, running 16 rounds. Each round's function F does four things to the right half before it gets folded back in.
flowchart TD
R["R(i-1): 32 bits"] --> Exp["Expansion Permutation (E):\nExpand 32 to 48 bits"]
K["Round Subkey Ki: 48 bits"] --> XOR1["XOR"]
Exp --> XOR1
XOR1 --> SB["8 S-boxes:\nCompress 48 bits back to 32 bits"]
SB --> Perm["Permutation (P)"]
Perm --> XOR2["XOR"]
L["L(i-1): 32 bits"] --> XOR2
XOR2 --> RNew["R(i) = L(i-1) XOR F(R(i-1), Ki)"]
R --> LNew["L(i) = R(i-1)"]
The Key Schedule: Where the 16 Subkeys Come From
DES doesn't reuse the same 56-bit key for every round. It generates 16 different 48-bit subkeys, one per round, through a process called the key schedule.
flowchart TD
K64["64-bit Key\n(56 key bits + 8 parity bits)"] --> PC1["Permuted Choice 1 (PC-1):\nDrop parity bits, output 56 bits"]
PC1 --> C0["C0: left 28 bits"]
PC1 --> D0["D0: right 28 bits"]
C0 --> LS1["Left Circular Shift"]
D0 --> LS2["Left Circular Shift"]
LS1 --> C1["C1"]
LS2 --> D1["D1"]
C1 --> PC2["Permuted Choice 2 (PC-2):\nSelect and permute 48 bits"]
D1 --> PC2
PC2 --> K1["Subkey K1"]
C1 -.->|"repeat left-shift + PC-2\n15 more times"| C16["C16"]
D1 -.-> D16["D16"]
C16 --> PC2b["PC-2"]
D16 --> PC2b
PC2b --> K16["Subkey K16"]
The shift amount varies per round (mostly 2 bits, sometimes 1), and because each round's C and D values only shift, they never lose information; by round 16, the halves return to their original position, which is what allows the same key schedule logic to work forwards for encryption and backwards for decryption.
What S-Boxes Actually Do
You take a 48-bit expanded half and need to compress it back to 32 bits, while also making the output unpredictable. That's the job of the eight S-boxes (Substitution boxes) in DES.
Each S-box takes a 6-bit input and produces a 4-bit output, using a fixed lookup table. The outer two bits of the 6-bit input select a row, the inner four bits select a column. This is the only non-linear step in DES, and it's what gives the cipher its confusion property. Without S-boxes, DES would just be a series of linear operations, trivially breakable with linear algebra.
Here's the actual lookup table for S-box 1 (S1), one of the eight used in every round:
| Row \ Column | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 14 | 4 | 13 | 1 | 2 | 15 | 11 | 8 | 3 | 10 | 6 | 12 | 5 | 9 | 0 | 7 |
| 1 | 0 | 15 | 7 | 4 | 14 | 2 | 13 | 1 | 10 | 6 | 12 | 11 | 9 | 5 | 3 | 8 |
| 2 | 4 | 1 | 14 | 8 | 13 | 6 | 2 | 11 | 15 | 12 | 9 | 7 | 3 | 10 | 5 | 0 |
| 3 | 15 | 12 | 8 | 2 | 4 | 9 | 1 | 7 | 5 | 11 | 3 | 14 | 10 | 0 | 6 | 13 |
- Without S-boxes: the cipher reduces to a linear function of the plaintext and key, breakable by solving simultaneous linear equations
- With S-boxes: the relationship between input and output becomes nonlinear and statistically unpredictable, and each S-box was specifically chosen to resist known cryptanalytic techniques
The Avalanche Effect
Flip a single bit in the plaintext or key, and a well-designed cipher should scramble roughly half the output bits. This property is called the avalanche effect, and DES is deliberately engineered to exhibit it strongly, growing round by round.
flowchart LR
A["Flip 1 Bit in Plaintext"] --> R1["After Round 1:\n~2 bits differ"]
R1 --> R2["After Round 2:\n~5 bits differ"]
R2 --> R3["After Round 3:\n~15 bits differ"]
R3 --> R5["After Round 5+:\n~32 bits differ (full avalanche)"]
The expansion permutation, S-box substitution, and the repeated 16 rounds all compound this effect. By round 5, a single input bit flip has already influenced roughly half the output block, which is what makes statistical analysis of the ciphertext useless to an attacker; there's no predictable pattern connecting input changes to output changes.
[!TIP]
If an exam question asks "why does DES use 16 rounds," the avalanche effect is the answer. Fewer rounds means insufficient diffusion, and the cipher becomes vulnerable to differential and linear cryptanalysis.
Differential and Linear Cryptanalysis
Two attacks specifically target the internal structure of block ciphers like DES, and both shaped how later ciphers were designed.
flowchart TD
A["Choose Plaintext Pairs\nwith Fixed XOR Difference"] --> B["Encrypt Both Plaintexts\nThrough the Cipher"]
B --> C["Observe Output XOR Difference"]
C --> D["Track High-Probability Differential\nCharacteristics Round by Round"]
D --> E["Recover Last-Round Subkey Bits"]
Differential cryptanalysis studies how a fixed difference (usually an XOR) between two plaintexts propagates through the rounds to produce a predictable difference in the ciphertexts, letting an attacker assign probabilities to particular key bits.
Linear cryptanalysis instead looks for a linear approximation, a simple equation involving plaintext bits, ciphertext bits, and key bits, that holds true with a probability noticeably different from 1/2. The more that probability deviates from 1/2, the more useful the approximation is for narrowing down key candidates.
| Feature | Differential Cryptanalysis | Linear Cryptanalysis |
|---|---|---|
| What it studies | How differences in plaintext pairs propagate to differences in ciphertext | Linear approximations between plaintext, ciphertext, and key bits |
| Attack type | Chosen-plaintext attack | Known-plaintext attack |
| Goal | Find high-probability input/output difference pairs to recover key bits | Find linear expressions that hold with probability far from 1/2 |
| Effectiveness on DES | Needs about 2^47 chosen plaintexts | Needs about 2^43 known plaintexts, more practical |
| Discovered | Publicly by Biham and Shamir, 1990 | By Mitsuru Matsui, 1993 |
DES's S-boxes were actually reasonably resistant to differential cryptanalysis, even though the attack wasn't publicly discovered until years after DES was standardized in 1977; the NSA is believed to have known about it and influenced the S-box design to resist it.
Strength of DES
The 56-bit key is DES's biggest weakness. It gives 2^56 possible keys, roughly 72 quadrillion, which sounds large, but is brute-forceable within hours using modern hardware or specialized cracking machines.
flowchart LR
A["1977: DES Standardized\n56-bit key"] --> B["1998: EFF Deep Crack\nBrute-forces DES in under 24 hours"]
B --> C["1999: Distributed.net + Deep Crack\nCracks DES in 22 hours"]
C --> D["2001+: AES Adopted as Replacement"]
- Algorithmic strength: the Feistel structure, S-boxes, and 16 rounds hold up reasonably well against differential and linear cryptanalysis, both requiring more plaintext than is realistically available in most attack scenarios
- Key length weakness: 56 bits is the practical failure point, not the algorithm's internal design
- Why it matters: this distinction is exactly why Triple DES exists — reuse the same trusted algorithm, just apply it multiple times with more key material, instead of designing an entirely new cipher from scratch
Design Principles of Block Cipher
Pulling everything together, a block cipher's design rests on these pillars:
- Diffusion and confusion: the Shannon properties every round must build toward
- Number of rounds: enough rounds to make shortcut attacks impractical, without excessive slowdown
- Function F design: the round function should be nonlinear and hard to invert without the subkey
- Key schedule algorithm: subkeys should not leak information about the main key, and small changes in the main key should produce very different subkeys
- Fast software and hardware implementation: practical ciphers need to run efficiently on real systems, not just be theoretically secure
- Avalanche property: small input changes must cascade into large, unpredictable output changes within a handful of rounds
MCQ
Multiple Encryption and Triple DES
DES's 56-bit key can be brute-forced in hours on modern hardware. You can't redesign a cipher that's been trusted and analyzed for 20 years overnight, so the practical fix was simpler: run DES more than once, with more key material.
Multiple encryption applies a block cipher two or more times with different keys, extending the effective key length without inventing a new algorithm. Triple DES (3DES) is the most widely deployed version of this idea.
Why Double DES Doesn't Work
The obvious first move is running DES twice with two different 56-bit keys, giving an apparent 112-bit key strength. This is broken by a meet-in-the-middle attack.
flowchart TD
P["Known Plaintext"] --> E1["Encrypt with Every Possible K1\n(2^56 possibilities)"]
E1 --> Table1["Store All Intermediate Results,\nSorted"]
C["Known Ciphertext"] --> D1["Decrypt with Every Possible K2\n(2^56 possibilities)"]
D1 --> Table2["Store All Intermediate Results,\nSorted"]
Table1 --> Match["Search for a Matching\nIntermediate Value"]
Table2 --> Match
Match --> Keys["Candidate (K1, K2) Pair"]
- The attack: an attacker encrypts the known plaintext with every possible first key, and separately decrypts the known ciphertext with every possible second key, then looks for a match in the intermediate value
- The cost: this reduces the effective security from 2^112 down to roughly 2^57, barely better than single DES, because the attacker never needs to try both keys together
- The lesson: doubling the key length doesn't double the security if the attack can split the problem into two independent halves
Triple DES (3DES)
Triple DES sidesteps the meet-in-the-middle weakness by using three encryption operations, typically in an Encrypt-Decrypt-Encrypt (EDE) sequence.
flowchart LR
P["Plaintext"] --> E1["Encrypt with K1"]
E1 --> D1["Decrypt with K2"]
D1 --> E2["Encrypt with K3"]
E2 --> C["Ciphertext"]
- Three-key 3DES: uses three independent 56-bit keys (K1, K2, K3), giving 168 bits of raw key material, though effective security is around 112 bits due to a more advanced meet-in-the-middle variant
- Two-key 3DES: sets K1 = K3, reducing key material to 112 bits raw and roughly 80 bits of effective security, while still resisting simple meet-in-the-middle better than double DES
- Why decrypt in the middle: using D instead of a second E makes 3DES backward-compatible with single DES when K1 = K2 = K3, since encrypt-then-decrypt-then-encrypt with equal keys collapses back to a single DES pass
[!NOTE]
Three-key 3DES is still found in legacy financial and payment systems (some ATM and POS infrastructure), but AES has replaced it almost everywhere else. 3DES runs roughly three times slower than a single DES pass, and is slower still than AES for equivalent effective security, since AES was designed from scratch to be efficient rather than bolted together from an older cipher.
Now that you've seen how repeating DES strengthens the key, the next question is: what do you do when your plaintext is longer than one block? That's what modes of operation solve, and each one makes a different tradeoff between speed, parallelism, and error behavior.
Electronic Code Book (ECB) Mode
The simplest possible approach: split the plaintext into blocks, encrypt each block independently with the same key.
flowchart LR
P1["Plaintext Block 1"] --> E1["Encrypt(K)"] --> C1["Ciphertext Block 1"]
P2["Plaintext Block 2"] --> E2["Encrypt(K)"] --> C2["Ciphertext Block 2"]
P3["Plaintext Block 3"] --> E3["Encrypt(K)"] --> C3["Ciphertext Block 3"]
- Strength: simple, parallelizable, no dependency between blocks, easy to implement in hardware
- Weakness: identical plaintext blocks always produce identical ciphertext blocks, leaking patterns in the data (the classic example is an ECB-encrypted image where you can still see the outline of the original picture through the ciphertext)
[!WARNING]
ECB should never be used for anything beyond a single block of data in practice. Its pattern leakage is a textbook example of why confidentiality needs more than just a strong underlying cipher; the mode of operation matters just as much as the algorithm.
Cipher Block Chaining (CBC) Mode
CBC fixes ECB's pattern-leakage problem by chaining blocks together: each plaintext block is XORed with the previous ciphertext block before encryption.
flowchart LR
IV["Initialization Vector"] --> X1["XOR"]
P1["Plaintext Block 1"] --> X1
X1 --> E1["Encrypt(K)"]
E1 --> C1["Ciphertext Block 1"]
C1 --> X2["XOR"]
P2["Plaintext Block 2"] --> X2
X2 --> E2["Encrypt(K)"]
E2 --> C2["Ciphertext Block 2"]
Decryption reverses the process, using the previous ciphertext block instead of feeding output forward:
flowchart LR
C1["Ciphertext Block 1"] --> D1["Decrypt(K)"]
D1 --> X1["XOR"]
IV["Initialization Vector"] --> X1
X1 --> P1["Plaintext Block 1"]
C2["Ciphertext Block 2"] --> D2["Decrypt(K)"]
D2 --> X2["XOR"]
C1 --> X2
X2 --> P2["Plaintext Block 2"]
- Initialization Vector (IV): a random block used in place of a "previous ciphertext" for the first block, ensuring identical plaintexts don't produce identical ciphertexts across different messages
- Chaining effect: because each block depends on the previous ciphertext, identical plaintext blocks now produce different ciphertext, hiding data patterns that ECB would leak
- Error propagation: a bit error in one ciphertext block corrupts that block fully and flips the corresponding bit in the next block's decryption, but no further than that
- Parallelism: encryption must happen sequentially block by block, but decryption can be parallelized since every ciphertext block is already available
CBC is one of the most widely used modes historically because it directly solves ECB's biggest flaw with minimal extra complexity.
Cipher Feedback (CFB) Mode
CFB turns a block cipher into something that behaves like a stream cipher, useful when data arrives in units smaller than a full block, like keystrokes typed over a remote terminal.
flowchart LR
IV["Initialization Vector"] --> E1["Encrypt(K)"]
E1 --> X1["XOR"]
P1["Plaintext Block 1"] --> X1
X1 --> C1["Ciphertext Block 1"]
C1 --> E2["Encrypt(K)"]
E2 --> X2["XOR"]
P2["Plaintext Block 2"] --> X2
X2 --> C2["Ciphertext Block 2"]
- How it differs from CBC: the block cipher encrypts the previous ciphertext, not the plaintext, and that result is XORed with the plaintext to produce the next ciphertext
- Stream-like behavior: you can encrypt data in units smaller than the full block size (e.g. 8 bits at a time instead of waiting for a full 64-bit block)
- Use case: real-time data streams where waiting for a full block isn't practical, like terminal sessions or character-by-character transmission
- Decryption note: interestingly, CFB decryption also uses the block cipher's encryption function, never its decryption function, which simplifies implementations that only need one direction of the underlying cipher
Output Feedback (OFB) Mode
OFB is similar to CFB, but it feeds the cipher's own output back into itself rather than the ciphertext, which changes its error behavior significantly.
flowchart LR
IV["Initialization Vector"] --> E1["Encrypt(K)"]
E1 --> O1["Output O1"]
O1 --> X1["XOR"]
P1["Plaintext Block 1"] --> X1
X1 --> C1["Ciphertext Block 1"]
O1 --> E2["Encrypt(K)"]
E2 --> O2["Output O2"]
O2 --> X2["XOR"]
P2["Plaintext Block 2"] --> X2
X2 --> C2["Ciphertext Block 2"]
- Key difference from CFB: the keystream (O1, O2, ...) is generated independently of the ciphertext, so a transmission error in one ciphertext block doesn't propagate into future blocks the way it does in CFB or CBC
- Precomputation: because the keystream doesn't depend on the plaintext or ciphertext at all, it can be generated entirely in advance, before the actual data even arrives
- Tradeoff: bit errors don't propagate, but bit-flipping attacks become easier, since flipping a bit in the ciphertext flips the exact same bit in the decrypted plaintext, in a fully predictable way
Counter (CTR) Mode
CTR mode replaces the chaining and feedback approach entirely with a simple incrementing counter, making it highly efficient and the default choice in many modern systems.
flowchart LR
CTR0["Counter Value 0"] --> E1["Encrypt(K)"]
E1 --> X1["XOR"]
P1["Plaintext Block 1"] --> X1
X1 --> C1["Ciphertext Block 1"]
CTR1["Counter Value 1"] --> E2["Encrypt(K)"]
E2 --> X2["XOR"]
P2["Plaintext Block 2"] --> X2
X2 --> C2["Ciphertext Block 2"]
- Counter value: starts at some initial value (often a nonce concatenated with a counter) and increments by one for each block, with no dependency on any previous plaintext or ciphertext
- Parallelizable both ways: since each block's keystream is fully independent, both encryption and decryption can happen in parallel across multiple cores, unlike CBC or CFB encryption
- Random access: you can decrypt any single block directly, without processing the blocks before it, which is exactly why CTR is popular for encrypted file storage and disk encryption where you need to seek to arbitrary positions
Comparing the Modes
| Mode | Parallelizable | Error Propagation | Needs IV/Nonce | Best Use Case |
|---|---|---|---|---|
| ECB | Yes (both directions) | Confined to one block | No | Never recommended beyond single blocks |
| CBC | Decryption only | Spreads to next block only | Yes (IV) | General-purpose bulk encryption |
| CFB | Decryption only | Spreads to next block only | Yes (IV) | Byte/bit-stream, terminal sessions |
| OFB | No (but precomputable) | None, errors stay local | Yes (IV) | Noisy channels where errors shouldn't spread |
| CTR | Yes (both directions) | None, errors stay local | Yes (nonce + counter) | High-performance, random-access encryption (modern default) |
flowchart TD
Start["Choosing a Mode of Operation"] --> Q1{"Need Random Access\nor Parallel Speed?"}
Q1 -->|"Yes"| CTR["Use CTR"]
Q1 -->|"No"| Q2{"Data Arrives in\nSmall Units (bytes/bits)?"}
Q2 -->|"Yes"| CFB["Use CFB"]
Q2 -->|"No"| Q3{"Noisy Channel Where\nErrors Must Stay Local?"}
Q3 -->|"Yes"| OFB["Use OFB"]
Q3 -->|"No"| CBC["Use CBC (general default)"]
[!TIP]
For exams, the fastest way to remember modes: ECB has no chaining at all, which is exactly its weakness. CBC and CFB chain through the ciphertext. OFB and CTR generate an independent keystream, which is why they don't let errors propagate.

