Understanding XOR Encryption and Decryption
A practical and beginner-friendly guide to how XOR encryption works and why it's reversible by design.
XOR encryption is one of the simplest forms of symmetric encryption. It’s so simple you can implement it in just a few lines of code, yet it teaches powerful concepts used in cryptography.
This post will break down what XOR is, why it works the way it does, and show Python examples you can run yourself.
🎯 What is XOR?
XOR stands for exclusive OR, a bitwise operation that:
- Compares two bits.
- Returns
1
if only one of them is1
. - Returns
0
if they are both the same.
Truth Table:
A | B | A XOR B |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
✨ Key Properties of XOR
XOR is interesting because it has a self-inverse property:
If you XOR something twice with the same key, you get the original back.
Mathematically:
1
(A XOR B) XOR B = A
This means encryption and decryption use the same operation.
🔐 XOR Encryption Explained
Imagine you have:
plaintext
: your datakey
: a secret byte
You encrypt:
1
ciphertext = plaintext XOR key
To decrypt:
1
plaintext = ciphertext XOR key
That’s it—no fancy division or special functions.
🧠 Why XOR is Not Like Multiplication
Some people think:
If XOR is like multiplication, then decryption must be division.
This is a misconception. XOR is more like flipping switches:
- The first XOR flips some bits.
- The second XOR flips them back.
✅ No division involved.
🧩 Example with a Single Byte
Let’s encrypt and decrypt 'A'
(ASCII 65) using key 0x5A
(90):
Encryption:
Decimal:
1
65 XOR 90 = 27
Binary:
1
2
3
4
5
01000001
XOR
01011010
=
00011011
Result: 27
(a non-printable character).
Decryption:
1
27 XOR 90 = 65
Binary:
1
2
3
4
5
00011011
XOR
01011010
=
01000001
Recovered: ASCII 65
= 'A'
.
🐍 Python Code Example
Here’s a minimal Python script to try it yourself:
1
2
3
4
5
6
7
8
9
10
11
12
plaintext_byte = ord('A') # 65
key_byte = 0x5A # 90
# Encrypt
ciphertext_byte = plaintext_byte ^ key_byte
# Decrypt
decrypted_byte = ciphertext_byte ^ key_byte
print("Plaintext:", chr(plaintext_byte))
print("Ciphertext byte:", ciphertext_byte)
print("Decrypted:", chr(decrypted_byte))
Expected output:
1
2
3
Plaintext: A
Ciphertext byte: 27
Decrypted: A
🧵 XOR with a String
To encrypt an entire string with a (potentially longer) key, you can cycle through the key bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def xor_encrypt(plaintext, key):
encrypted = []
for i in range(len(plaintext)):
encrypted_byte = ord(plaintext[i]) ^ ord(key[i % len(key)])
encrypted.append(encrypted_byte)
return bytes(encrypted)
def xor_decrypt(ciphertext, key):
decrypted = []
for i in range(len(ciphertext)):
decrypted_byte = ciphertext[i] ^ ord(key[i % len(key)])
decrypted.append(chr(decrypted_byte))
return ''.join(decrypted)
# Example usage
plaintext = "HELLO"
key = "K"
ciphertext = xor_encrypt(plaintext, key)
print("Ciphertext bytes:", list(ciphertext))
decrypted = xor_decrypt(ciphertext, key)
print("Decrypted text:", decrypted)
Sample output:
1
2
Ciphertext bytes: [3, 14, 0, 7, 4]
Decrypted text: HELLO
⚡ Why is XOR Popular in Simple Ciphers?
- Fast: XOR is a low-level bitwise operation.
- Simple: The same function encrypts and decrypts.
- Self-inverse: XORing twice with the same key recovers the plaintext.
- Symmetric: Both parties need only the shared key.
But: XOR is not secure against known-plaintext attacks if you reuse the key (this is called a “many-time pad”). For real security, you need a truly random key used only once (a one-time pad).
🪄 Quick Recap
✅ XOR is not like multiplication.
✅ You don’t need division to reverse it.
✅ Just XOR twice with the same key.
✅ This principle is used in stream ciphers and other cryptographic primitives.