Done one-based affine ciphers, and breaking them.
[cipher-tools.git] / cipher.py
index 58d381290e9fbe33f533b558f1644a7552e052d8..3f9e29087408202e91b0294f231bea9edfc0054a 100644 (file)
--- a/cipher.py
+++ b/cipher.py
@@ -5,7 +5,7 @@ import logging
 
 logger = logging.getLogger(__name__)
 logger.addHandler(logging.FileHandler('cipher.log'))
-# logging.basicConfig(filename='cipher.log',level=logging.WARNING)
+logger.setLevel(logging.WARNING)
 logger.setLevel(logging.INFO)
 
 english_counts = collections.defaultdict(int)
@@ -22,6 +22,13 @@ 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
@@ -120,44 +127,73 @@ def caesar_decipher(message, shift):
     """
     return caesar_encipher(message, -shift)
 
-def affine_encipher_letter(letter, multiplier, adder, multiply_then_add=True):
+def affine_encipher_letter(letter, multiplier=1, adder=0, one_based=True):
+    """Encipher a letter, given a multiplier and adder
+    
+    >>> ''.join([affine_encipher_letter(l, 3, 5, True) for l in string.ascii_uppercase])
+    'HKNQTWZCFILORUXADGJMPSVYBE'
+    >>> ''.join([affine_encipher_letter(l, 3, 5, False) for l in string.ascii_uppercase])
+    'FILORUXADGJMPSVYBEHKNQTWZC'
+    """
     if letter in string.ascii_letters:
         if letter in string.ascii_uppercase:
             alphabet_start = ord('A')
         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 multiply_then_add:
-            cipher_number = (letter_number * multiplier + adder) % 26
+        if one_based: 
+            cipher_number = (raw_cipher_number - 1) % 26
         else:
-            cipher_number = ((letter_number + adder) * multiplier) % 26
+            cipher_number = raw_cipher_number % 26        
         return chr(cipher_number + alphabet_start)
     else:
         return letter
 
-def affine_decipher_letter(letter, multiplier, adder, multiply_then_add=True):
+def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True):
+    """Encipher a letter, given a multiplier and adder
+    
+    >>> ''.join([affine_decipher_letter(l, 3, 5, True) for l in 'HKNQTWZCFILORUXADGJMPSVYBE'])
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+    >>> ''.join([affine_decipher_letter(l, 3, 5, False) for l in 'FILORUXADGJMPSVYBEHKNQTWZC'])
+    'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+    """
     if letter in string.ascii_letters:
         if letter in string.ascii_uppercase:
             alphabet_start = ord('A')
         else:
             alphabet_start = ord('a')
         cipher_number = ord(letter) - alphabet_start
+        if one_based: cipher_number += 1
         plaintext_number = 0
-        if multiply_then_add:
-            plaintext_number = modular_division_table[multiplier][(cipher_number - adder + 26) % 26]
+        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
+            plaintext_number = modular_division_table[multiplier][(cipher_number - adder + 26) % 26]            
         return chr(plaintext_number + alphabet_start)
     else:
         return letter
 
-def affine_encipher(message, multiplier, adder, multiply_then_add=True):
-    enciphered = [affine_encipher_letter(l, multiplier, adder, multiply_then_add) for l in message]
+def affine_encipher(message, multiplier=1, adder=0, one_based=True):
+    """Encipher a message
+    
+    >>> affine_encipher('hours passed during which jerico tried every trick he could think of', 15, 22, True)
+    'lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh'
+    """
+    
+    enciphered = [affine_encipher_letter(l, multiplier, adder, one_based) for l in message]
     return ''.join(enciphered)
 
-def affine_decipher(message, multiplier, adder, multiply_then_add=True):
-    enciphered = [affine_decipher_letter(l, multiplier, adder, multiply_then_add) for l in message]
+def affine_decipher(message, multiplier=1, adder=0, one_based=True):
+    """Decipher a message
+    
+    >>> affine_decipher('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh', 15, 22, True)
+    'hours passed during which jerico tried every trick he could think of'
+    """
+    enciphered = [affine_decipher_letter(l, multiplier, adder, one_based) for l in message]
     return ''.join(enciphered)
 
 
@@ -187,26 +223,29 @@ def caesar_break(message, metric=norms.euclidean_distance, target_frequencies=no
 
 def affine_break(message, metric=norms.euclidean_distance, target_frequencies=normalised_english_counts, message_frequency_scaling=norms.normalise):
     """Breaks an affine cipher using frequency analysis
+    
+    >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ofgbjmwfkiu olfmxmtmwaokttg jlsx ls kffkxwsd jlsi zg tsxwjl. jlsx ls umfjsd jlsi zg hfsqysxog. ls dmmdtsd mx jls bats mh bkbsf. ls bfmctsd kfmyxd jls lyj, mztanamyu xmc jm clm cku tmmeaxw kj lai kxd clm ckuxj.')
+    ((15, 22, True), 0.2357036181865554)
     """
     sanitised_message = sanitise(message)
     best_multiplier = 0
     best_adder = 0
-    best_multiply_then_add = True
+    best_one_based = True
     best_fit = float("inf")
-    for multiply_then_add in [True, False]:
+    for one_based in [True, False]:
         for multiplier in range(1, 26, 2):
             for adder in range(26):
-                plaintext = affine_decipher(sanitised_message, multiplier, adder, multiply_then_add)
+                plaintext = affine_decipher(sanitised_message, multiplier, adder, one_based)
                 frequencies = message_frequency_scaling(letter_frequencies(plaintext))
                 fit = metric(target_frequencies, frequencies)
-                logger.info('Affine break attempt using key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(multiplier, adder, multiply_then_add, fit, plaintext[:50]))
+                logger.info('Affine break attempt using key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(multiplier, adder, one_based, fit, plaintext[:50]))
                 if fit < best_fit:
                     best_fit = fit
                     best_multiplier = multiplier
                     best_adder = adder
-                    best_multiply_then_add = multiply_then_add
-    logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(best_multiplier, best_adder, best_multiply_then_add, best_fit, affine_decipher(sanitised_message, best_multiplier, best_adder, best_multiply_then_add)[:50]))
-    return (best_multiplier, best_adder, best_multiply_then_add), best_fit
+                    best_one_based = one_based
+    logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of {3} and decrypt starting: {4}'.format(best_multiplier, best_adder, best_one_based, best_fit, affine_decipher(sanitised_message, best_multiplier, best_adder, best_one_based)[:50]))
+    return (best_multiplier, best_adder, best_one_based), best_fit
 
 
 if __name__ == "__main__":