Files
cryptopals/data/set3c19.py

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)