|
| 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