Skip to content

Commit 39c3947

Browse files
feat: Add RC4 stream cipher implementation (#14753)
* feat: Add RC4 stream cipher implementation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * fix: resolve ruff lint check errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * docs: add description of RC4 operation and security warning --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f26093b commit 39c3947

1 file changed

Lines changed: 217 additions & 0 deletions

File tree

ciphers/rc4.py

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
"""
2+
RC4 (Rivest Cipher 4) Stream Cipher Algorithm
3+
=============================================
4+
5+
RC4 is a symmetric stream cipher designed by Ron Rivest in 1987 for RSA Security.
6+
It is famous for its simplicity and speed in software. It operates on bytes,
7+
encrypting and decrypting data one byte at a time by XORing the plaintext with
8+
a pseudorandom keystream.
9+
10+
How it works:
11+
-------------
12+
1. Key Scheduling Algorithm (KSA):
13+
Initializes and permutes a 256-byte state array (the S-box) based on the secret key.
14+
2. Pseudo-Random Generation Algorithm (PRGA):
15+
Generates a continuous sequence of pseudorandom bytes (the keystream)
16+
from the S-box.
17+
With each byte generated, the state array is mutated to ensure unpredictability.
18+
3. Encryption/Decryption:
19+
The plaintext is XORed byte-by-byte with the keystream to produce the ciphertext.
20+
Since XOR is its own inverse, decryption uses the exact same process (XORing
21+
the ciphertext with the same keystream).
22+
23+
Security Status:
24+
----------------
25+
WARNING: RC4 is cryptographically broken and insecure.
26+
It suffers from significant keystream biases, particularly in the initial bytes.
27+
If the same key is reused, or if an attacker captures enough ciphertext, they
28+
can reconstruct the plaintext or the key. The use of RC4 is prohibited in modern
29+
protocols (such as TLS via RFC 7465). It is implemented here strictly for
30+
educational purposes.
31+
32+
Further reading:
33+
----------------
34+
* https://en.wikipedia.org/wiki/RC4
35+
"""
36+
37+
from collections.abc import Generator
38+
39+
40+
def ksa(key: bytes) -> list[int]:
41+
"""
42+
Key Scheduling Algorithm (KSA)
43+
==============================
44+
45+
The KSA initializes the permutation in the array S (S-box) of size 256
46+
with values from 0 to 255. Then, it shuffles the array using the secret key.
47+
48+
Parameters:
49+
-----------
50+
* `key`: The secret key used for encryption/decryption as a bytes object.
51+
52+
Returns:
53+
--------
54+
* A list of 256 integers representing the permuted S-box.
55+
56+
Doctests:
57+
=========
58+
>>> ksa(b"Key")[:5]
59+
[75, 51, 132, 157, 192]
60+
"""
61+
s_box = list(range(256))
62+
j = 0
63+
key_length = len(key)
64+
for i in range(256):
65+
j = (j + s_box[i] + key[i % key_length]) % 256
66+
s_box[i], s_box[j] = s_box[j], s_box[i]
67+
return s_box
68+
69+
70+
def prga(s_box: list[int]) -> Generator[int]:
71+
"""
72+
Pseudo-Random Generation Algorithm (PRGA)
73+
=========================================
74+
75+
The PRGA generates keystream bytes from the permuted S-box S.
76+
For each iteration, it modifies the S-box and outputs one byte of the keystream.
77+
78+
Parameters:
79+
-----------
80+
* `s_box`: The permuted state array S-box.
81+
82+
Yields:
83+
-------
84+
* An integer representing the next byte of the pseudo-random keystream.
85+
86+
Doctests:
87+
=========
88+
>>> box = ksa(b"Key")
89+
>>> stream = prga(box)
90+
>>> [next(stream) for _ in range(5)]
91+
[235, 159, 119, 129, 183]
92+
"""
93+
s = s_box.copy()
94+
i = 0
95+
j = 0
96+
while True:
97+
i = (i + 1) % 256
98+
j = (j + s[i]) % 256
99+
s[i], s[j] = s[j], s[i]
100+
yield s[(s[i] + s[j]) % 256]
101+
102+
103+
def encrypt(plaintext: bytes, key: bytes) -> bytes:
104+
"""
105+
Encrypts/Decrypts the plaintext bytes with a key using the RC4 stream cipher.
106+
107+
Parameters:
108+
-----------
109+
* `plaintext`: The input message to encrypt/decrypt (bytes).
110+
* `key`: The secret key (bytes).
111+
112+
Returns:
113+
--------
114+
* The encrypted/decrypted result (bytes).
115+
116+
More on RC4:
117+
============
118+
RC4 (Rivest Cipher 4) is a symmetric stream cipher. Because it is symmetric,
119+
the encryption and decryption operations are identical. The cipher
120+
generates a pseudorandom stream of bytes (keystream) which is combined with
121+
the plaintext using bitwise exclusive-or (XOR).
122+
123+
Warning:
124+
--------
125+
RC4 is cryptographically insecure and vulnerable to several attacks (such
126+
as keystream biases). It should not be used in secure systems today. It is
127+
implemented here purely for educational purposes.
128+
129+
Further reading:
130+
================
131+
* https://en.wikipedia.org/wiki/RC4
132+
133+
Doctests:
134+
=========
135+
>>> encrypt(b"Plaintext", b"Key")
136+
b'\\xbb\\xf3\\x16\\xe8\\xd9@\\xaf\\n\\xd3'
137+
>>> encrypt(b"pedia", b"Wiki")
138+
b'\\x10!\\xbf\\x04 '
139+
>>> encrypt(b"\\x10!\\xbf\\x04 ", b"Wiki")
140+
b'pedia'
141+
"""
142+
if not key:
143+
raise ValueError("Key must not be empty.")
144+
145+
s_box = ksa(key)
146+
keystream = prga(s_box)
147+
return bytes(p ^ next(keystream) for p in plaintext)
148+
149+
150+
def decrypt(ciphertext: bytes, key: bytes) -> bytes:
151+
"""
152+
Decrypts the ciphertext bytes with a key using the RC4 stream cipher.
153+
154+
Since RC4 is symmetric, decryption is identical to encryption.
155+
156+
Parameters:
157+
-----------
158+
* `ciphertext`: The input cipher text to decrypt (bytes).
159+
* `key`: The secret key (bytes).
160+
161+
Returns:
162+
--------
163+
* The decrypted plaintext (bytes).
164+
165+
Doctests:
166+
=========
167+
>>> decrypt(b'\\x10!\\xbf\\x04 ', b"Wiki")
168+
b'pedia'
169+
"""
170+
return encrypt(ciphertext, key)
171+
172+
173+
if __name__ == "__main__":
174+
import sys
175+
176+
# Check for doctests
177+
if len(sys.argv) > 1 and sys.argv[1] == "--test":
178+
import doctest
179+
180+
doctest.testmod()
181+
sys.exit(0)
182+
183+
print(f"\n{'-' * 10}\n RC4 Cipher Menu\n{'-' * 10}")
184+
print("1. Encrypt String")
185+
print("2. Decrypt Hex String")
186+
print("3. Quit")
187+
188+
while True:
189+
choice = input("\nWhat would you like to do?: ").strip()
190+
if choice == "3" or not choice:
191+
print("Goodbye.")
192+
break
193+
elif choice == "1":
194+
plain_str = input("Enter plain text to encrypt: ")
195+
key_str = input("Enter key: ")
196+
if not key_str:
197+
print("Key cannot be empty!")
198+
continue
199+
encrypted_bytes = encrypt(
200+
plain_str.encode("utf-8"), key_str.encode("utf-8")
201+
)
202+
print(f"Ciphertext (Hex): {encrypted_bytes.hex()}")
203+
elif choice == "2":
204+
hex_str = input("Enter hex ciphertext to decrypt: ")
205+
key_str = input("Enter key: ")
206+
if not key_str:
207+
print("Key cannot be empty!")
208+
continue
209+
try:
210+
cipher_bytes = bytes.fromhex(hex_str)
211+
decrypted_bytes = decrypt(cipher_bytes, key_str.encode("utf-8"))
212+
decrypted_text = decrypted_bytes.decode("utf-8", errors="replace")
213+
print(f"Decrypted text: {decrypted_text}")
214+
except ValueError as e:
215+
print(f"Invalid input: {e}")
216+
else:
217+
print("Invalid choice, please enter 1, 2, or 3.")

0 commit comments

Comments
 (0)