99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
import binascii
|
|
import string
|
|
from typing import List, Dict, Set
|
|
|
|
|
|
def load_ciphers() -> List[bytes]:
|
|
with open("data/19_enc.txt", "r") as f:
|
|
r = [binascii.a2b_base64(line) for line in f]
|
|
return r
|
|
|
|
|
|
def xor_lookup_set(letters: str) -> Dict[int, Set[str]]:
|
|
d = {i: set() for i in range(256)}
|
|
for c1 in letters:
|
|
for c2 in letters:
|
|
xored = ord(c1) ^ ord(c2)
|
|
d[xored].add(c1)
|
|
d[xored].add(c2)
|
|
return d
|
|
|
|
|
|
def attack(ciphers: List[bytes]) -> List[List[str]]:
|
|
""" Find out possible characters for each cipher pair and hope that there
|
|
is only one possible character. If no character was found add '?' and
|
|
if more than one was found add '^'. """
|
|
|
|
ciphers_len = len(ciphers)
|
|
deciphered = [[] for _ in range(ciphers_len)]
|
|
max_cipher_len = max(map(len, ciphers))
|
|
for byte_index in range(0, max_cipher_len):
|
|
# Certain bytes only work with certain letters (found empirically).
|
|
if byte_index == 10:
|
|
LETTERS = string.ascii_letters + " _-.,;:'"
|
|
elif byte_index == 20:
|
|
LETTERS = string.ascii_letters + " _-.,;:?"
|
|
else:
|
|
LETTERS = string.ascii_letters + " _-.,;:"
|
|
lookup = xor_lookup_set(LETTERS)
|
|
target_bytes = [cipher[byte_index]
|
|
if len(cipher) > byte_index
|
|
else None
|
|
for cipher in ciphers]
|
|
possible_chars = [set(LETTERS) for _ in range(ciphers_len)]
|
|
for i in range(ciphers_len):
|
|
for j in range(i, ciphers_len):
|
|
if target_bytes[i] is None or target_bytes[j] is None:
|
|
continue
|
|
xored = target_bytes[i] ^ target_bytes[j]
|
|
chars = lookup[xored]
|
|
possible_chars[i] &= chars
|
|
possible_chars[j] &= chars
|
|
for cipher_index in range(ciphers_len):
|
|
if len(ciphers[cipher_index]) <= byte_index:
|
|
continue
|
|
chars = list(possible_chars[cipher_index])
|
|
match len(chars):
|
|
case 0:
|
|
# print(f"No chars for {cipher_index=} {byte_index=}")
|
|
deciphered[cipher_index].append('?')
|
|
case 1:
|
|
deciphered[cipher_index].append(chars[0].lower())
|
|
case 2:
|
|
if chars[0].lower() == chars[1].lower():
|
|
deciphered[cipher_index].append(chars[0].lower())
|
|
else:
|
|
# print(f"Two {chars=} {cipher_index=} {byte_index=}")
|
|
deciphered[cipher_index].append('^')
|
|
case _:
|
|
# print(f"Too many {chars=} {cipher_index=} {byte_index=}")
|
|
deciphered[cipher_index].append('^')
|
|
return deciphered
|
|
|
|
|
|
def manual(decrypts: List[List[str]]) -> List[bytes]:
|
|
""" Manually add guessed letters. """
|
|
decrypts[0][30] = 'y'
|
|
decrypts[2][30] = 'y'
|
|
decrypts[4][30] = 'e'
|
|
decrypts[4][32] = 'h'
|
|
decrypts[4][33] = 'e'
|
|
decrypts[4][34] = 'a'
|
|
decrypts[4][35] = 'd'
|
|
decrypts[6][30] = 'i'
|
|
decrypts[13][30] = ' '
|
|
decrypts[20][30] = ' '
|
|
decrypts[25][30] = 'n'
|
|
decrypts[28][30] = ' '
|
|
decrypts[29][30] = 't'
|
|
decrypts[37][30] = 'i'
|
|
decrypts = list(map(lambda l: "".join(l), decrypts))
|
|
return decrypts
|
|
|
|
|
|
if __name__ == "__main__":
|
|
ciphers = load_ciphers()
|
|
decrypts = manual(attack(ciphers))
|
|
for d in decrypts:
|
|
print(d)
|