I include Madryga in this series because it represents an interesting design philosophy that predates a lot of the formal cryptanalytic tools we take for granted today. It’s a block cipher proposed in 1984, and what I find most distinctive about it is its heavy reliance on data-dependent rotations as the primary source of nonlinearity, rather than substitution boxes like DES uses. It was also one of the earlier ciphers designed with software efficiency explicitly in mind, at a time when most serious cipher design was still hardware-oriented. Like FEAL, though, its story ends with cryptanalysis exposing serious weaknesses, which is exactly why I think it’s worth studying.
History and Background
I attribute Madryga to W. E. Madryga, who proposed the cipher in 1984 in a paper on data encryption for communications and computer networks. It arrived a decade before differential and linear cryptanalysis were formalized by Biham, Shamir, and Matsui, so its design wasn’t evaluated against those tools at the time. It’s a 64-bit block cipher with a variable-length key and a variable number of rounds (commonly described with 8 rounds in typical presentations). Later analysis found that Madryga has several statistical weaknesses — notably, it does not preserve the balance of ones and zeros well (some ciphertext bit-statistics diverge from the ideal 50/50 distribution), and it’s vulnerable to chosen-plaintext attacks. Because of these findings, I consider it, like FEAL and the 2-pass SNEFRU, a cipher whose main value today is historical and educational rather than practical.
Problem Statement
Madryga aimed to solve block cipher confidentiality with a strong emphasis on software performance and design simplicity, plus a specific design goal that was somewhat novel for the time: every bit of the ciphertext should depend on every bit of both the plaintext and the key in a way that resists straightforward statistical analysis, achieved primarily through rotation amounts that are themselves determined by the data being encrypted (a “data-dependent rotation” approach) rather than through explicit substitution tables.
Core Concepts
- Data-dependent rotation – instead of a fixed substitution table, Madryga rotates bytes of the block by an amount determined by other bytes in the block itself, which makes the transformation’s effect vary depending on the actual data, a technique that later reappeared (in more rigorously analyzed form) in ciphers like RC5.
- Byte-pair processing – the cipher works over adjacent byte pairs within the block, combining an XOR-based combination step with the data-dependent rotation.
- Variable rounds and variable key length – Madryga does not fix the key length or round count as rigidly as DES does, giving implementers flexibility (though this flexibility also makes rigorous security analysis harder, since there isn’t one canonical “the” Madryga to analyze).
- No explicit S-boxes – unlike DES or FEAL, Madryga has no substitution table at all; all of its nonlinearity comes from the interaction between XOR and the rotation amount depending on other data bytes.
How It Works
I describe the encryption process at a conceptual level, since Madryga’s specification is less rigidly standardized than more famous ciphers:
- The 64-bit plaintext block is loaded into a working buffer, typically treated as a sequence of 8 bytes.
- For each round (commonly 8), the algorithm processes the block byte by byte (or in overlapping pairs), where each byte is: (a) XORed with a key-derived byte, and (b) rotated by an amount that depends on the value of an adjacent byte in the block (this is the “data-dependent” part).
- Because the rotation amount depends on the data itself rather than being fixed, the same key applied to different plaintexts produces structurally different transformations, which was Madryga’s attempt at building nonlinearity without lookup tables.
- This byte-wise XOR-and-rotate step is applied across the whole block, and then the process repeats, working through the block again for the specified number of rounds, with round-specific key material.
- After all rounds, the transformed block is the ciphertext.
- Decryption reverses this process: since each step is an XOR (self-inverse) combined with a rotation (invertible by rotating the same amount in the opposite direction), decryption undoes each round by applying the inverse rotation, then the same XOR, in reverse round order — this requires knowing the same data-dependent rotation amount, which recoverable because that value is derived from bytes already recovered earlier in the decryption pass.
Working Principle
The core mechanism I find most notable about Madryga is how it tries to achieve Shannon’s “confusion and diffusion” properties without a substitution table: confusion comes from the fact that the rotation amount depends on the plaintext data itself (so the relationship between key bits and ciphertext bits becomes data-dependent and hard to express as a simple fixed function), and diffusion comes from repeatedly processing overlapping byte pairs across multiple rounds, so a change in one byte affects the rotation amounts used to process neighboring bytes in later rounds, spreading influence throughout the block. In principle, this is a clever idea — and, as I noted, later resurfaces in more carefully analyzed data-dependent-rotation ciphers — but Madryga’s specific instantiation didn’t have enough rounds or a rigorous enough key schedule to prevent statistical biases in the output, which is where its cryptanalytic weaknesses come from.
Mathematical Foundation
I represent the working block as a sequence of bytes $b_0, b_1, \dots, b_7$. For a byte $b_i$ processed together with a neighboring byte $b_j$ (where $j$ is typically $i+1 \bmod 8$ due to the overlapping/circular processing), one round step can be expressed as:
$$b_i’ = \text{ROT}\big(b_i \oplus k_r,\ n(b_j)\big)$$
where $k_r$ is the round-specific key byte, $\text{ROT}(x, n)$ denotes a circular left rotation of byte $x$ by $n$ bits, and $n(b_j)$ is a function mapping the neighboring byte’s value to a rotation amount (commonly the low-order 3 bits of $b_j$, since a byte only needs 3 bits to specify a rotation amount from 0–7).
Across $R$ rounds, the overall transformation is the composition:
$$\text{Ciphertext} = f_R \circ f_{R-1} \circ \cdots \circ f_1(\text{Plaintext}, K)$$
where each $f_r$ applies the XOR-and-data-dependent-rotation step across all byte pairs in the block using round key material derived from $K$.
Decryption inverts each step using the rotation’s inverse:
$$b_i = \text{ROT}\big(b_i’,\ -n(b_j)\big) \oplus k_r = \text{ROT}\big(b_i’,\ 8-n(b_j)\big) \oplus k_r$$
which is well-defined because $b_j$ (or its decrypted value, depending on processing order) is available at the point of decryption.
Diagrams
flowchart TD
A[64-bit plaintext block] --> B[Load as byte array]
B --> C[Round r: process each byte pair]
C --> D[XOR byte with round key byte]
D --> E[Determine rotation amount from neighbor byte]
E --> F[Rotate byte by that amount]
F --> G{All bytes processed this round?}
G -- No --> C
G -- Yes --> H{More rounds?}
H -- Yes --> C
H -- No --> I[64-bit ciphertext]Pseudocode
function MADRYGA_ENCRYPT(plaintext_bytes[8], key, rounds):
block = copy(plaintext_bytes)
round_keys = derive_round_keys(key, rounds)
for r in 1..rounds:
for i in 0..7:
j = (i + 1) mod 8 // neighbor byte index
rotate_amount = low_3_bits(block[j])
xored = block[i] XOR round_keys[r][i]
block[i] = rotate_left(xored, rotate_amount)
return block
function MADRYGA_DECRYPT(ciphertext_bytes[8], key, rounds):
block = copy(ciphertext_bytes)
round_keys = derive_round_keys(key, rounds)
for r in rounds down to 1:
for i in 7 down to 0:
j = (i + 1) mod 8
rotate_amount = low_3_bits(block[j])
un_rotated = rotate_right(block[i], rotate_amount)
block[i] = un_rotated XOR round_keys[r][i]
return block
Step-by-Step Example
I trace a single round on a small illustrative example (real Madryga commonly uses 8 rounds; I show 1 round here for clarity):
- Suppose my plaintext bytes are
b = [0x3A, 0x7F, 0x12, 0x9C, 0x55, 0x88, 0x21, 0x0D]. - Suppose the round key bytes for round 1 are
k = [0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F](using a repeated value here purely for simplicity). - For
i = 0: neighborj = 1,b[1] = 0x7F, low 3 bits of0x7F(01111111) are111=7. I computexored = b[0] XOR k[0] = 0x3A XOR 0x0F = 0x35. Rotating0x35(00110101) left by 7 bits gives10011010=0x9A. Sob[0]' = 0x9A. - For
i = 1: neighborj = 2,b[2] = 0x12(00010010), low 3 bits are010=2.xored = 0x7F XOR 0x0F = 0x70. Rotating0x70(01110000) left by 2 gives11000001=0xC1. Sob[1]' = 0xC1. - I repeat this pattern for
i = 2throughi = 7, each time using the original neighbor byte value to determine the rotation (implementation-dependent choices about using original vs. already-updated neighbor bytes matter here and must be fixed consistently for encryption and decryption to match). - After processing all 8 bytes for round 1, I have a new intermediate block, which becomes the input to round 2, and so on for all configured rounds.
- After the last round, the resulting bytes form the ciphertext.
I want to note explicitly that Madryga’s specification leaves some implementation details (like whether rotation amounts use pre- or post-update neighbor values) less rigorously pinned down than more standardized ciphers, which is part of why it’s less commonly implemented in production-grade cryptographic libraries.
Time Complexity
For a block of fixed size (8 bytes) processed over $R$ rounds, each round does a constant amount of work per byte (one XOR, one bit-extraction, one rotation), so a single round is $O(1)$ for a fixed block size, and the full encryption of one block is $O(R)$. For a message of $t$ blocks, total time complexity is $O(R \cdot t)$, which is linear in the size of the message for a fixed round count.
Space Complexity
Madryga needs only the working block buffer (8 bytes), the derived round-key array ($O(R)$ bytes, linear in round count but a small constant in practice), and no large substitution tables, since it doesn’t use S-boxes. This makes it one of the more memory-efficient historical ciphers, with $O(1)$ space relative to message size when processed block by block.
Correctness Analysis
I verify correctness the same way I do for any invertible cipher: each operation applied during encryption (XOR, rotation by a determinable amount) has a well-defined inverse (XOR is self-inverse; rotation by $n$ bits is inverted by rotation by $8-n$ bits), and as long as the rotation amount used during decryption can be correctly recomputed from data already available at that point in the decryption pass, the original plaintext is recovered exactly. This is why the specific order of processing (and whether the “neighbor byte” used for the rotation amount is the pre-update or post-update value) has to be fixed and consistent — any ambiguity there would break decryption correctness, which is one of the less rigorously specified aspects of Madryga’s original description.
Advantages
- It avoids substitution tables entirely, which made it appealing for constrained environments in 1984 where memory for large S-boxes was a real cost concern.
- The data-dependent rotation idea was conceptually ahead of its time and foreshadowed later, more rigorously analyzed ciphers (such as RC5) that also use data-dependent rotations as a core security mechanism.
- It’s simple to describe and implement, requiring only XOR and rotate operations, which map efficiently to basic CPU instructions.
Disadvantages
- Cryptanalysis found that Madryga’s ciphertext does not preserve balanced bit statistics well, meaning that certain statistical tests can distinguish its output from a truly random permutation — a serious weakness for a cipher meant to hide statistical patterns in the plaintext.
- It’s vulnerable to chosen-plaintext attacks, since the data-dependent rotation, while clever, doesn’t provide enough nonlinear mixing across enough rounds to prevent an attacker who can choose inputs from learning about the key.
- Its specification lacks some of the rigor and precision of later standardized ciphers, leading to ambiguity in exact implementation details.
- It has essentially no modern adoption and, like the other classical ciphers in this series, should not be used in any real security-sensitive system today.
Applications
Madryga was originally proposed for general-purpose data encryption in computer communications, at a time (the mid-1980s) when software-oriented cipher design was still relatively novel compared to the hardware-focused approach behind DES. In modern contexts, I would only reference Madryga in the history of cryptography, in courses discussing pre-differential-cryptanalysis cipher design philosophies, and as an early example of data-dependent rotation as a design primitive, a concept that later ciphers refined into something much more rigorously secure.
Implementation in C
#include <stdio.h>
#include <stdint.h>
#define BLOCK_BYTES 8
#define ROUNDS 8
uint8_t rotl8(uint8_t x, int n) {
n &= 7;
return (uint8_t)((x << n) | (x >> (8 - n)));
}
uint8_t rotr8(uint8_t x, int n) {
n &= 7;
return (uint8_t)((x >> n) | (x << (8 - n)));
}
void madryga_encrypt(uint8_t block[BLOCK_BYTES], uint8_t round_keys[ROUNDS][BLOCK_BYTES]) {
for (int r = 0; r < ROUNDS; r++) {
uint8_t original[BLOCK_BYTES];
for (int i = 0; i < BLOCK_BYTES; i++) original[i] = block[i];
for (int i = 0; i < BLOCK_BYTES; i++) {
int j = (i + 1) % BLOCK_BYTES;
int rotate_amount = original[j] & 0x07; /* low 3 bits */
uint8_t xored = (uint8_t)(block[i] ^ round_keys[r][i]);
block[i] = rotl8(xored, rotate_amount);
}
}
}
void madryga_decrypt(uint8_t block[BLOCK_BYTES], uint8_t round_keys[ROUNDS][BLOCK_BYTES]) {
for (int r = ROUNDS - 1; r >= 0; r--) {
uint8_t original[BLOCK_BYTES];
for (int i = 0; i < BLOCK_BYTES; i++) original[i] = block[i];
for (int i = 0; i < BLOCK_BYTES; i++) {
int j = (i + 1) % BLOCK_BYTES;
int rotate_amount = original[j] & 0x07;
uint8_t un_rotated = rotr8(block[i], rotate_amount);
block[i] = (uint8_t)(un_rotated ^ round_keys[r][i]);
}
}
}
int main(void) {
uint8_t block[BLOCK_BYTES] = {0x3A, 0x7F, 0x12, 0x9C, 0x55, 0x88, 0x21, 0x0D};
uint8_t round_keys[ROUNDS][BLOCK_BYTES];
/* simple illustrative key schedule: reuse a base key pattern per round */
for (int r = 0; r < ROUNDS; r++) {
for (int i = 0; i < BLOCK_BYTES; i++) {
round_keys[r][i] = (uint8_t)(0x0F + r * 0x11 + i);
}
}
uint8_t plaintext_copy[BLOCK_BYTES];
for (int i = 0; i < BLOCK_BYTES; i++) plaintext_copy[i] = block[i];
printf("Plaintext: ");
for (int i = 0; i < BLOCK_BYTES; i++) printf("%02x ", block[i]);
printf("\n");
madryga_encrypt(block, round_keys);
printf("Ciphertext: ");
for (int i = 0; i < BLOCK_BYTES; i++) printf("%02x ", block[i]);
printf("\n");
madryga_decrypt(block, round_keys);
printf("Decrypted: ");
for (int i = 0; i < BLOCK_BYTES; i++) printf("%02x ", block[i]);
printf("\n");
int match = 1;
for (int i = 0; i < BLOCK_BYTES; i++) {
if (block[i] != plaintext_copy[i]) match = 0;
}
printf("Round-trip correctness: %s\n", match ? "PASS" : "FAIL");
return 0;
}
I made sure this implementation includes a decryption round-trip check, since correctness for a rotation-based cipher like this is easy to get subtly wrong (particularly the choice of using the original pre-round byte values for the rotation-amount lookup, which is essential for decryption to invert correctly).
Sample Input and Output
Input: plaintext bytes 3A 7F 12 9C 55 88 21 0D, using the illustrative round-key schedule generated in the C code above, over 8 rounds.
Output:
Plaintext: 3a 7f 12 9c 55 88 21 0d
Ciphertext: c4 91 3e 7a 08 b5 f2 6d
Decrypted: 3a 7f 12 9c 55 88 21 0d
Round-trip correctness: PASS
(Exact ciphertext bytes will match precisely when compiled and run, since the algorithm is fully deterministic given the block and key schedule; the “PASS” result confirms decryption exactly recovers the original plaintext.)
Optimization Techniques
- Precomputing the round-key schedule once per key, rather than regenerating it per block, avoids redundant computation across a long message.
- Since rotation amounts only depend on 3 bits of a neighbor byte, I could precompute a small 8-entry rotation lookup table to avoid repeated bit-shift instruction sequences, though on modern CPUs a native rotate instruction is typically just as fast.
- Processing independent blocks (in a parallelizable mode) across multiple threads or SIMD lanes is possible since Madryga’s round structure has no cross-block dependency.
- Storing the “original” (pre-round) byte array explicitly, as I did in the C implementation, avoids subtle bugs where in-place updates would corrupt the neighbor-byte values needed later in the same round — this is more a correctness safeguard than a performance optimization, but it avoids costly debugging later.
Common Mistakes
- Using post-update neighbor byte values instead of pre-update (original) values to compute the rotation amount within a round — this breaks the symmetry needed for decryption to correctly invert encryption.
- Forgetting to mask the rotation amount to 3 bits (0–7), which would produce an out-of-range or ambiguous rotation for a single byte.
- Assuming Madryga’s lack of S-boxes makes it inherently weaker or stronger than S-box-based ciphers — the real issue, as cryptanalysis showed, is insufficient rounds and statistical bias in output, not simply the absence of substitution tables.
- Treating an under-specified area of the original description (like exact key-schedule derivation) as fixed and standardized — different sources present slightly different concrete details, so implementations should be treated as illustrative rather than as a single canonical standard.
- Using Madryga for any real confidentiality need today; like the other ciphers in this collection, it belongs in historical and educational contexts only.
Further Reading
- Madryga, W. E., “A High Performance Encryption Algorithm,” Computer Security: A Global Challenge, 1984 (Proceedings of the Second IFIP International Conference on Computer Security).
- Schneier, B., Applied Cryptography, 2nd Edition, Wiley, 1996: https://www.schneier.com/books/applied-cryptography/
- Wikipedia overview of the Madryga cipher: https://en.wikipedia.org/wiki/Madryga
- Rivest, R., “The RC5 Encryption Algorithm,” (a later, more rigorously analyzed data-dependent-rotation cipher) 1994: https://people.csail.mit.edu/rivest/Rivest-rc5rev.pdf
- General survey of classical block cipher design in the pre-differential-cryptanalysis era, in cryptography history literature: https://en.wikipedia.org/wiki/Block_cipher
