Skip to content

Commit 4b12b7b

Browse files
authored
Fix doctest syntax in key_scheduling
1 parent e9c9958 commit 4b12b7b

1 file changed

Lines changed: 176 additions & 0 deletions

File tree

ciphers/rc4_cipher.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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+
>>> len(key_scheduling([65, 66, 67]))
43+
256
44+
45+
>>> key_scheduling([0]) == list(range(256))
46+
False
47+
"""
48+
key_length = len(key)
49+
# Initialise the state array as the identity permutation
50+
state = list(range(256))
51+
j = 0
52+
for i in range(256):
53+
j = (j + state[i] + key[i % key_length]) % 256
54+
# Swap state[i] and state[j]
55+
state[i], state[j] = state[j], state[i]
56+
return state
57+
58+
59+
def pseudo_random_generation(state: list[int], length: int) -> list[int]:
60+
"""
61+
Perform the Pseudo-Random Generation Algorithm (PRGA).
62+
63+
Generates a keystream of the requested length from the state array
64+
produced by the KSA.
65+
66+
Args:
67+
state: A 256-element permutation list from key_scheduling().
68+
length: The number of keystream bytes to generate.
69+
70+
Returns:
71+
A list of keystream bytes (integers 0-255).
72+
73+
>>> state = list(range(256))
74+
>>> keystream = pseudo_random_generation(state, 5)
75+
>>> len(keystream)
76+
5
77+
>>> all(0 <= b <= 255 for b in keystream)
78+
True
79+
"""
80+
i = 0
81+
j = 0
82+
keystream = []
83+
for _ in range(length):
84+
i = (i + 1) % 256
85+
j = (j + state[i]) % 256
86+
# Swap state[i] and state[j]
87+
state[i], state[j] = state[j], state[i]
88+
keystream.append(state[(state[i] + state[j]) % 256])
89+
return keystream
90+
91+
92+
def encrypt(plaintext: str, key: str) -> list[int]:
93+
"""
94+
Encrypt a plaintext string using RC4 with the given key.
95+
96+
Converts the plaintext and key to byte lists, runs KSA and PRGA, then
97+
XORs the plaintext bytes with the keystream to produce ciphertext bytes.
98+
99+
Args:
100+
plaintext: The message to encrypt (ASCII string).
101+
key: The encryption key (ASCII string, 1-256 characters).
102+
103+
Returns:
104+
A list of integers representing the ciphertext bytes.
105+
106+
Raises:
107+
ValueError: If the key is empty.
108+
109+
>>> encrypt("Hello", "secret")
110+
[165, 83, 190, 112, 237]
111+
112+
>>> encrypt("", "key")
113+
[]
114+
115+
>>> encrypt("Attack at dawn", "Key")
116+
[170, 235, 3, 224, 212, 95, 234, 19, 211, 57, 46, 73, 16, 216]
117+
"""
118+
if not key:
119+
raise ValueError("Key must not be empty.")
120+
key_bytes = [ord(c) for c in key]
121+
plaintext_bytes = [ord(c) for c in plaintext]
122+
state = key_scheduling(key_bytes)
123+
keystream = pseudo_random_generation(state, len(plaintext_bytes))
124+
return [p ^ k for p, k in zip(plaintext_bytes, keystream)]
125+
126+
127+
def decrypt(ciphertext: list[int], key: str) -> str:
128+
"""
129+
Decrypt RC4 ciphertext bytes back to a plaintext string.
130+
131+
RC4 decryption is identical to encryption: generate the same keystream
132+
and XOR it with the ciphertext bytes.
133+
134+
Args:
135+
ciphertext: A list of integers (ciphertext bytes) from encrypt().
136+
key: The same key used during encryption.
137+
138+
Returns:
139+
The decrypted plaintext as a string.
140+
141+
Raises:
142+
ValueError: If the key is empty.
143+
144+
>>> decrypt([165, 83, 190, 112, 237], "secret")
145+
'Hello'
146+
147+
>>> decrypt([], "key")
148+
''
149+
150+
>>> decrypt([170, 235, 3, 224, 212, 95, 234, 19, 211, 57, 46, 73, 16, 216], "Key")
151+
'Attack at dawn'
152+
"""
153+
if not key:
154+
raise ValueError("Key must not be empty.")
155+
key_bytes = [ord(c) for c in key]
156+
state = key_scheduling(key_bytes)
157+
keystream = pseudo_random_generation(state, len(ciphertext))
158+
return "".join(chr(c ^ k) for c, k in zip(ciphertext, keystream))
159+
160+
161+
if __name__ == "__main__":
162+
import doctest
163+
164+
doctest.testmod()
165+
166+
# Example usage
167+
message = "Hello, World!"
168+
secret_key = "mysecretkey"
169+
170+
print(f"Original : {message}")
171+
encrypted = encrypt(message, secret_key)
172+
print(f"Encrypted: {encrypted}")
173+
decrypted = decrypt(encrypted, secret_key)
174+
print(f"Decrypted: {decrypted}")
175+
assert decrypted == message, "Decryption failed — output does not match original."
176+
print("Encrypt -> Decrypt round-trip successful.")

0 commit comments

Comments
 (0)