|
| 1 | +CHARACTER_FREQ = { |
| 2 | + 'a': 0.0651738, 'b': 0.0124248, 'c': 0.0217339, 'd': 0.0349835, |
| 3 | + 'e': 0.1041442, 'f': 0.0197881, 'g': 0.0158610, 'h': 0.0492888, |
| 4 | + 'i': 0.0558094, 'j': 0.0009033, 'k': 0.0050529, 'l': 0.0331490, |
| 5 | + 'm': 0.0202124, 'n': 0.0564513, 'o': 0.0596302, 'p': 0.0137645, |
| 6 | + 'q': 0.0008606, 'r': 0.0497563, 's': 0.0515760, 't': 0.0729357, |
| 7 | + 'u': 0.0225134, 'v': 0.0082903, 'w': 0.0171272, 'x': 0.0013692, |
| 8 | + 'y': 0.0145984, 'z': 0.0007836, ' ': 0.1918182 |
| 9 | +} |
| 10 | +def get_english_score(input_bytes): |
| 11 | + #Returns a score which is the sum of the probabilities in how each letter of the input data |
| 12 | + #appears in the English language. Uses the above probabilities. |
| 13 | + |
| 14 | + score = 0 |
| 15 | + |
| 16 | + for byte in input_bytes: |
| 17 | + score += CHARACTER_FREQ.get(chr(byte).lower(), 0) |
| 18 | + |
| 19 | + return score |
| 20 | + |
| 21 | + |
| 22 | +def singlechar_xor(input_bytes, key_value): |
| 23 | + #XORs every byte of the input with the given key_value and returns the result. |
| 24 | + output = b'' |
| 25 | + |
| 26 | + for char in input_bytes: |
| 27 | + output += bytes([char ^ key_value]) |
| 28 | + |
| 29 | + return output |
| 30 | + |
| 31 | + |
| 32 | +def singlechar_xor_brute_force(ciphertext): |
| 33 | + """Tries every possible byte for the single-char key, decrypts the ciphertext with that byte |
| 34 | + and computes the english score for each plaintext. The plaintext with the highest score |
| 35 | + is likely to be the one decrypted with the correct value of key. |
| 36 | + """ |
| 37 | + candidates = [] |
| 38 | + |
| 39 | + for key_candidate in range(256): |
| 40 | + plaintext_candidate = singlechar_xor(ciphertext, key_candidate) |
| 41 | + candidate_score = get_english_score(plaintext_candidate) |
| 42 | + |
| 43 | + result = { |
| 44 | + 'key': key_candidate, |
| 45 | + 'score': candidate_score, |
| 46 | + 'plaintext': plaintext_candidate |
| 47 | + } |
| 48 | + |
| 49 | + candidates.append(result) |
| 50 | + |
| 51 | + # Return the candidate with the highest English score |
| 52 | + |
| 53 | + return sorted(candidates, key=lambda c: c['score'], reverse=True)[0] |
| 54 | + |
| 55 | +def pretty_print_result(result): |
| 56 | + """Prints the given resulting candidate in a pretty format.""" |
| 57 | + print(result['plaintext'].decode().rstrip(), "\tScore:", "{0:.2f}".format(result['score']), |
| 58 | + "\tKey:", chr(result['key'])) |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | +def main(): |
| 63 | + ciphertext = bytes.fromhex("1b37373331363f78151b7f2b783431333d78397828372d363c78373e783a393b3736") |
| 64 | + most_likely_plaintext = singlechar_xor_brute_force(ciphertext) |
| 65 | + pretty_print_result(most_likely_plaintext) |
| 66 | + |
| 67 | +main() |
0 commit comments