C: why a one-time pad key must never be reused
XOR two messages with the same key and show that the key cancels out and leaks information.
On this page
C bitwise XOR and byte arrays.
- XOR two messages with the same key and show that the key cancels out and leaks information.
- Predict the sample output and explain the state changes.
- Complete the practice task and check the boundary cases.
The concept
The one-time pad XORs the message with a random key as long as the message. It is perfectly secret if the key is truly random, used once, and kept secret. XOR is its own inverse: (p ^ k) ^ k gives p.
If the same key encrypts two messages, XORing the two ciphertexts cancels the key completely: c1 ^ c2 = p1 ^ p2. An attacker who sees both ciphertexts learns where the plaintexts differ, and if they know or guess one plaintext, they recover the other. This is a famous real-world failure, for example in reused stream-cipher keystreams. The lesson shows an attack on a misuse, to explain why reuse is forbidden.
Code example
#include <stdio.h>
#include <string.h>
int main(void) {
unsigned char p1[] = "ATTACK AT DAWN";
unsigned char p2[] = "ATTACK AT DUSK";
unsigned char key[] = {0x5A, 0x13, 0xC7, 0x88, 0x21, 0x6E, 0x90, 0x04, 0xB3, 0x7F, 0x42, 0xD1, 0x39, 0xEE};
size_t n = strlen((const char *)p1);
unsigned char c1[14], c2[14];
for (size_t i = 0; i < n; i++) {
c1[i] = p1[i] ^ key[i];
c2[i] = p2[i] ^ key[i];
}
int cancelled = 1;
for (size_t i = 0; i < n; i++) {
if ((c1[i] ^ c2[i]) != (p1[i] ^ p2[i])) {
cancelled = 0;
}
}
printf("key cancels out: %d\n", cancelled);
char recovered[15];
for (size_t i = 0; i < n; i++) {
recovered[i] = (char)(c1[i] ^ c2[i] ^ p1[i]);
}
recovered[n] = '\0';
printf("recovered second message: %s\n", recovered);
return 0;
}key cancels out: 1 recovered second message: ATTACK AT DUSK
Follow the code
Open the walkthrough to inspect the example one step at a time.
Show guided walkthrough
#include <stdio.h>#include <string.h> int main(void) { unsigned char p1[] = "ATTACK AT DAWN"; unsigned char p2[] = "ATTACK AT DUSK"; unsigned char key[] = {0x5A, 0x13, 0xC7, 0x88, 0x21, 0x6E, 0x90, 0x04, 0xB3, 0x7F, 0x42, 0xD1, 0x39, 0xEE}; size_t n = strlen((const char *)p1); unsigned char c1[14], c2[14]; for (size_t i = 0; i < n; i++) { c1[i] = p1[i] ^ key[i]; c2[i] = p2[i] ^ key[i]; } int cancelled = 1; for (size_t i = 0; i < n; i++) { if ((c1[i] ^ c2[i]) != (p1[i] ^ p2[i])) { cancelled = 0; } } printf("key cancels out: %d\n", cancelled); char recovered[15]; for (size_t i = 0; i < n; i++) { recovered[i] = (char)(c1[i] ^ c2[i] ^ p1[i]); } recovered[n] = '\0'; printf("recovered second message: %s\n", recovered); return 0;}Follow the example
Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.
View the complete dry-run table
| Step / line | What happens | State after step | Output so far |
|---|---|---|---|
| Encrypt the first message byte by byte with the key. | — | — | |
| Encrypting a second message with the same key is the mistake. | — | — | |
| The key appears twice and cancels, so the XOR of the ciphertexts equals the XOR of the plaintexts. | — | — | |
| Every byte matches, so an eavesdropper learns p1 ^ p2 without ever seeing the key. | cancelled = 1 | key cancels out: 1 | |
| Knowing p1, the attacker computes (p1 ^ p2) ^ p1 = p2. | cancelled = 1 | key cancels out: 1 | |
| The second message is recovered without the key. | cancelled = 1 | key cancels out: 1 recovered second message: ATTACK AT DUSK |
Behind the scenes
How C processes this code
The C compiler produces a native program. Variables occupy memory, and execution enters main.
Cost and common mistakes
O(n) for n bytes.
- A short key that repeats over a long message has the same problem, because the key is reused within the message.
- XOR with a fixed key provides no integrity either. An attacker can flip chosen bits in the ciphertext. Use an authenticated cipher such as AES-GCM or ChaCha20-Poly1305 from a vetted library.
- Never design your own encryption for real systems. This example exists to show why reuse is dangerous.
Try it yourself
How do modern stream ciphers avoid key reuse?
Reveal answer and reasoning
They combine the key with a unique nonce for every message, so the keystream is never the same twice. Reusing a key and nonce pair is still a fatal mistake.