|
1 | | -""" |
2 | | -RC4 (Rivest Cipher 4) Stream Cipher |
3 | 1 |
|
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