Scytale enciphering working, but have to think about what to do with a 16 character...
[cipher-tools.git] / cipher.py
index 8034043e8c59807df788f3add91e426b491ff2be..6a138a075e2fa570260fdff8b4c1d8f10b93351c 100644 (file)
--- 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