X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=autokey.py;fp=autokey.py;h=b84f0a94c08c1acefc4cece55054cab15ce5b8f3;hb=3d8f7067b9c3a48ef140d7cff834d18ee91f58b3;hp=0000000000000000000000000000000000000000;hpb=21c390a77d42729afa23844ef2f1295106bed3de;p=cipher-tools.git diff --git a/autokey.py b/autokey.py new file mode 100644 index 0000000..b84f0a9 --- /dev/null +++ b/autokey.py @@ -0,0 +1,118 @@ +from utilities import * +from language_models import * +import multiprocessing + +from logger import logger + + +def autokey_encipher(message, keyword): + """Encipher with the autokey cipher + + >>> autokey_encipher('meetatthefountain', 'kilt') + 'wmpmmxxaeyhbryoca' + """ + shifts = [pos(l) for l in keyword + message] + pairs = zip(message, shifts) + return cat([caesar_encipher_letter(l, k) for l, k in pairs]) + +def autokey_decipher(ciphertext, keyword): + """Decipher with the autokey cipher + + >>> autokey_decipher('wmpmmxxaeyhbryoca', 'kilt') + 'meetatthefountain' + """ + plaintext = [] + keys = list(keyword) + for c in ciphertext: + plaintext_letter = caesar_decipher_letter(c, pos(keys[0])) + plaintext += [plaintext_letter] + keys = keys[1:] + [plaintext_letter] + return cat(plaintext) + + + +def autokey_sa_break( message + , min_keylength=2 + , max_keylength=20 + , workers=10 + , initial_temperature=200 + , max_iterations=20000 + , fitness=Pletters + , chunksize=1 + , result_count=1 + ): + """Break an autokey cipher by simulated annealing + """ + worker_args = [] + ciphertext = sanitise(message) + for keylength in range(min_keylength, max_keylength+1): + for i in range(workers): + key = cat(random.choice(string.ascii_lowercase) for _ in range(keylength)) + worker_args.append((ciphertext, key, + initial_temperature, max_iterations, fitness)) + + with multiprocessing.Pool() as pool: + breaks = pool.starmap(autokey_sa_break_worker, + worker_args, chunksize) + if result_count <= 1: + return max(breaks, key=lambda k: k[1]) + else: + return sorted(set(breaks), key=lambda k: k[1], reverse=True)[:result_count] + + +def autokey_sa_break_worker(message, key, + t0, max_iterations, fitness): + + temperature = t0 + + dt = t0 / (0.9 * max_iterations) + + plaintext = autokey_decipher(message, key) + current_fitness = fitness(plaintext) + current_key = key + + best_key = current_key + best_fitness = current_fitness + best_plaintext = plaintext + + # print('starting for', max_iterations) + for i in range(max_iterations): + swap_pos = random.randrange(len(current_key)) + swap_char = random.choice(string.ascii_lowercase) + + new_key = current_key[:swap_pos] + swap_char + current_key[swap_pos+1:] + + plaintext = autokey_decipher(message, new_key) + new_fitness = fitness(plaintext) + try: + sa_chance = math.exp((new_fitness - current_fitness) / temperature) + except (OverflowError, ZeroDivisionError): + # print('exception triggered: new_fit {}, current_fit {}, temp {}'.format(new_fitness, current_fitness, temperature)) + sa_chance = 0 + if (new_fitness > current_fitness or random.random() < sa_chance): + # logger.debug('Simulated annealing: iteration {}, temperature {}, ' + # 'current alphabet {}, current_fitness {}, ' + # 'best_plaintext {}'.format(i, temperature, current_alphabet, + # current_fitness, best_plaintext[:50])) + + # logger.debug('new_fit {}, current_fit {}, temp {}, sa_chance {}'.format(new_fitness, current_fitness, temperature, sa_chance)) +# print(new_fitness, new_key, plaintext[:100]) + current_fitness = new_fitness + current_key = new_key + + if current_fitness > best_fitness: + best_key = current_key + best_fitness = current_fitness + best_plaintext = plaintext + if i % 500 == 0: + logger.debug('Simulated annealing: iteration {}, temperature {}, ' + 'current key {}, current_fitness {}, ' + 'best_plaintext {}'.format(i, temperature, current_key, + current_fitness, plaintext[:50])) + temperature = max(temperature - dt, 0.001) + +# print(best_key, best_fitness, best_plaintext[:70]) + return best_key, best_fitness # current_alphabet, current_fitness + +if __name__ == "__main__": + import doctest \ No newline at end of file