X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=cipher.py;h=6a138a075e2fa570260fdff8b4c1d8f10b93351c;hb=0d469a1c5ce3895f1b3d3ecbf7c58c69d7e0d2ed;hp=8034043e8c59807df788f3add91e426b491ff2be;hpb=6e185eba9169eb276bd05807ce710a1bdfbec339;p=cipher-tools.git diff --git a/cipher.py b/cipher.py index 8034043..6a138a0 100644 --- a/cipher.py +++ b/cipher.py @@ -2,6 +2,16 @@ import string import collections import norms import logging +import math +from itertools import zip_longest +from segment import segment + +# To time a run: +# +# import timeit +# c5a = open('2012/5a.ciphertext', 'r').read() +# timeit.timeit('keyword_break(c5a)', setup='gc.enable() ; from __main__ import c5a ; from cipher import keyword_break', number=1) + logger = logging.getLogger(__name__) logger.addHandler(logging.FileHandler('cipher.log')) @@ -24,13 +34,6 @@ for a in range(26): c = (a * b) % 26 modular_division_table[b][c] = a -modular_division_table_one_based = [[0]*27 for x in range(27)] -for a in range(27): - for b in range(27): - c = ((a * b)-1) % 26 + 1 - modular_division_table_one_based[b][c] = a - - def sanitise(text): """Remove all non-alphabetic characters and convert the text to lowercase @@ -53,6 +56,33 @@ def ngrams(text, n): """ return [tuple(text[i:i+n]) for i in range(len(text)-n+1)] +def every_nth(text, n): + """Returns n strings, each of which consists of every nth character, + starting with the 0th, 1st, 2nd, ... (n-1)th character + + >>> every_nth(string.ascii_lowercase, 5) + ['afkpuz', 'bglqv', 'chmrw', 'dinsx', 'ejoty'] + >>> every_nth(string.ascii_lowercase, 1) + ['abcdefghijklmnopqrstuvwxyz'] + >>> every_nth(string.ascii_lowercase, 26) + ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] + """ + split_text = [text[i:i+n] for i in range(0, len(text), n)] + return [''.join(l) for l in zip_longest(*split_text, fillvalue='')] + +def combine_every_nth(split_text): + """Reforms a text split into every_nth strings + + >>> combine_every_nth(every_nth(string.ascii_lowercase, 5)) + 'abcdefghijklmnopqrstuvwxyz' + >>> combine_every_nth(every_nth(string.ascii_lowercase, 1)) + 'abcdefghijklmnopqrstuvwxyz' + >>> combine_every_nth(every_nth(string.ascii_lowercase, 26)) + 'abcdefghijklmnopqrstuvwxyz' + """ + return ''.join([''.join(l) for l in zip_longest(*split_text, fillvalue='')]) + + def letter_frequencies(text): """Count the number of occurrences of each character in text @@ -155,15 +185,10 @@ def affine_encipher_letter(letter, multiplier=1, adder=0, one_based=True): else: alphabet_start = ord('a') letter_number = ord(letter) - alphabet_start - if one_based: - letter_number += 1 - raw_cipher_number = (letter_number * multiplier + adder) - cipher_number = 0 - if one_based: - cipher_number = (raw_cipher_number - 1) % 26 - else: - cipher_number = raw_cipher_number % 26 - return chr(cipher_number + alphabet_start) + if one_based: letter_number += 1 + cipher_number = (letter_number * multiplier + adder) % 26 + if one_based: cipher_number -= 1 + return chr(cipher_number % 26 + alphabet_start) else: return letter @@ -181,15 +206,10 @@ def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True): else: alphabet_start = ord('a') cipher_number = ord(letter) - alphabet_start - if one_based: - cipher_number += 1 - plaintext_number = 0 - if one_based: - plaintext_number = (modular_division_table_one_based[multiplier][(cipher_number - adder + 26) % 26] - 1) % 26 - else: - #plaintext_number = (modular_division_table[multiplier][cipher_number] - adder) % 26 - plaintext_number = modular_division_table[multiplier][(cipher_number - adder + 26) % 26] - return chr(plaintext_number + alphabet_start) + if one_based: cipher_number += 1 + plaintext_number = modular_division_table[multiplier][(cipher_number - adder) % 26] + if one_based: plaintext_number -= 1 + return chr(plaintext_number % 26 + alphabet_start) else: return letter @@ -285,6 +305,40 @@ def keyword_decipher(message, keyword, wrap_alphabet=0): cipher_translation = ''.maketrans(cipher_alphabet, string.ascii_lowercase) return message.lower().translate(cipher_translation) +def scytale_encipher(message, rows): + """Enciphers using the scytale transposition cipher + + >>> scytale_encipher('thequickbrownfox', 3) + 'tcnhkfeboqrxuoiw' + >>> scytale_encipher('thequickbrownfox', 4) + 'tubnhirfecooqkwx' + >>> scytale_encipher('thequickbrownfox', 5) + 'tubnhirfecooqkwx' + >>> scytale_encipher('thequickbrownfox', 6) + 'tqcrnxhukofeibwo' + >>> scytale_encipher('thequickbrownfox', 7) + 'tqcrnxhukofeibwo' + """ + row_length = math.ceil(len(message) / rows) + slices = [message[i:i+row_length] for i in range(0, len(message), row_length)] + return ''.join([''.join(r) for r in zip_longest(*slices, fillvalue='')]) + +def scytale_decipher(message, rows): + """Deciphers using the scytale transposition cipher + + >>> scytale_decipher('tcnhkfeboqrxuoiw', 3) + 'thequickbrownfox' + """ + cols = math.ceil(len(message) / rows) + if len(message) % rows == 0: + part_cols = 0 + else: + part_cols = rows - len(message) % rows + full_cols = cols - part_cols + columns = [message[i:i+rows] for i in range(0, full_cols * rows, rows)] + \ + [message[i:i+rows-1] for i in range(full_cols * rows, len(message), rows - 1)] + return ''.join([''.join(c) for c in zip_longest(*columns, fillvalue='')]) + def caesar_break(message, metric=norms.euclidean_distance, target_frequencies=normalised_english_counts, message_frequency_scaling=norms.normalise): """Breaks a Caesar cipher using frequency analysis