Skip to content

Commit 30ee2ae

Browse files
authored
Add RC4 stream cipher implementation
1 parent a9f2e72 commit 30ee2ae

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

rc4_cipher.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""
2+
RC4 (Rivest Cipher 4) Stream Cipher
3+
4+
RC4 is a symmetric stream cipher designed by Ron Rivest in 1987. It was widely
5+
used in protocols such as SSL/TLS and WEP before being deprecated due to
6+
statistical biases in its keystream. Understanding RC4 remains important for
7+
security education, particularly for studying why stream cipher design matters.
8+
9+
The algorithm has two phases:
10+
1. Key Scheduling Algorithm (KSA): Initialises a 256-byte permutation using
11+
the key.
12+
2. Pseudo-Random Generation Algorithm (PRGA): Produces keystream bytes by
13+
further permuting the state array.
14+
15+
Encryption and decryption are identical: XOR the keystream with the plaintext
16+
to encrypt, or XOR with the ciphertext to decrypt.
17+
18+
Reference:
19+
https://en.wikipedia.org/wiki/RC4
20+
21+
Security note:
22+
RC4 is cryptographically broken and must NOT be used in production systems.
23+
This implementation is provided for educational purposes only.
24+
"""
25+
26+
from __future__ import annotations
27+
28+
29+
def key_scheduling(key: list[int]) -> list[int]:
30+
"""
31+
Perform the Key Scheduling Algorithm (KSA).
32+
33+
Initialises a 256-byte identity permutation and scrambles it using the
34+
provided key bytes.
35+
36+
Args:
37+
key: A list of integers (0-255) representing the key bytes.
38+
39+
Returns:
40+
A 256-element permutation list (the initial state array S).
41+
42+
>>> key_scheduling([1, 2, 3])
43+
... # doctest: +ELLIPSIS
44+
[...]
45+
46+
>>> len(key_scheduling([65, 66, 67]))
47+
256
48+
49+
>>> key_scheduling([0]) == list(range(256))
50+
False
51+
"""
52+
key_length = len(key)
53+
# Initialise the state array as the identity permutation
54+
state = list(range(256))
55+
j = 0
56+
for i in range(256):
57+
j = (j + state[i] + key[i % key_length]) % 256
58+
# Swap state[i] and state[j]
59+
state[i], state[j] = state[j], state[i]
60+
return state
61+
62+
63+
def pseudo_random_generation(state: list[int], length: int) -> list[int]:
64+
"""
65+
Perform the Pseudo-Random Generation Algorithm (PRGA).
66+
67+
Generates a keystream of the requested length from the state array
68+
produced by the KSA.
69+
70+
Args:
71+
state: A 256-element permutation list from key_scheduling().
72+
length: The number of keystream bytes to generate.
73+
74+
Returns:
75+
A list of keystream bytes (integers 0-255).
76+
77+
>>> state = list(range(256))
78+
>>> keystream = pseudo_random_generation(state, 5)
79+
>>> len(keystream)
80+
5
81+
>>> all(0 <= b <= 255 for b in keystream)
82+
True
83+
"""
84+
i = 0
85+
j = 0
86+
keystream = []
87+
for _ in range(length):
88+
i = (i + 1) % 256
89+
j = (j + state[i]) % 256
90+
# Swap state[i] and state[j]
91+
state[i], state[j] = state[j], state[i]
92+
keystream.append(state[(state[i] + state[j]) % 256])
93+
return keystream
94+
95+
96+
def encrypt(plaintext: str, key: str) -> list[int]:
97+
"""
98+
Encrypt a plaintext string using RC4 with the given key.
99+
100+
Converts the plaintext and key to byte lists, runs KSA and PRGA, then
101+
XORs the plaintext bytes with the keystream to produce ciphertext bytes.
102+
103+
Args:
104+
plaintext: The message to encrypt (ASCII string).
105+
key: The encryption key (ASCII string, 1-256 characters).
106+
107+
Returns:
108+
A list of integers representing the ciphertext bytes.
109+
110+
Raises:
111+
ValueError: If the key is empty.
112+
113+
>>> encrypt("Hello", "secret")
114+
[165, 83, 190, 112, 237]
115+
116+
>>> encrypt("", "key")
117+
[]
118+
119+
>>> encrypt("Attack at dawn", "Key")
120+
[170, 235, 3, 224, 212, 95, 234, 19, 211, 57, 46, 73, 16, 216]
121+
"""
122+
if not key:
123+
raise ValueError("Key must not be empty.")
124+
key_bytes = [ord(c) for c in key]
125+
plaintext_bytes = [ord(c) for c in plaintext]
126+
state = key_scheduling(key_bytes)
127+
keystream = pseudo_random_generation(state, len(plaintext_bytes))
128+
return [p ^ k for p, k in zip(plaintext_bytes, keystream)]
129+
130+
131+
def decrypt(ciphertext: list[int], key: str) -> str:
132+
"""
133+
Decrypt RC4 ciphertext bytes back to a plaintext string.
134+
135+
RC4 decryption is identical to encryption: generate the same keystream
136+
and XOR it with the ciphertext bytes.
137+
138+
Args:
139+
ciphertext: A list of integers (ciphertext bytes) from encrypt().
140+
key: The same key used during encryption.
141+
142+
Returns:
143+
The decrypted plaintext as a string.
144+
145+
Raises:
146+
ValueError: If the key is empty.
147+
148+
>>> decrypt([165, 83, 190, 112, 237], "secret")
149+
'Hello'
150+
151+
>>> decrypt([], "key")
152+
''
153+
154+
>>> decrypt([170, 235, 3, 224, 212, 95, 234, 19, 211, 57, 46, 73, 16, 216], "Key")
155+
'Attack at dawn'
156+
"""
157+
if not key:
158+
raise ValueError("Key must not be empty.")
159+
key_bytes = [ord(c) for c in key]
160+
state = key_scheduling(key_bytes)
161+
keystream = pseudo_random_generation(state, len(ciphertext))
162+
return "".join(chr(c ^ k) for c, k in zip(ciphertext, keystream))
163+
164+
165+
if __name__ == "__main__":
166+
import doctest
167+
168+
doctest.testmod()
169+
170+
# Example usage
171+
message = "Hello, World!"
172+
secret_key = "mysecretkey"
173+
174+
print(f"Original : {message}")
175+
encrypted = encrypt(message, secret_key)
176+
print(f"Encrypted: {encrypted}")
177+
decrypted = decrypt(encrypted, secret_key)
178+
print(f"Decrypted: {decrypted}")
179+
assert decrypted == message, "Decryption failed — output does not match original."
180+
print("Encrypt -> Decrypt round-trip successful.")

0 commit comments

Comments
 (0)