Skip to content

Commit db3a343

Browse files
committed
feat: Add RC4 stream cipher implementation
1 parent 6c04620 commit db3a343

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

ciphers/rc4.py

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

0 commit comments

Comments
 (0)