Merge branch 'presentation-slides' of github.com:NeilNjae/cipher-training into presen...
authorNeil Smith <neil.git@njae.me.uk>
Wed, 2 Jul 2014 21:15:16 +0000 (22:15 +0100)
committerNeil Smith <neil.git@njae.me.uk>
Wed, 2 Jul 2014 21:15:16 +0000 (22:15 +0100)
cipherbreak.py
slides/affine-break.html
slides/affine-encipher.html
slides/fast-good-cheap.gif [new file with mode: 0644]
slides/keyword-break.html

index 2a70ceebbadb3ae3303545219420f4866ae6dd83..e89407c730ccdf721ea2247eaa1d9ec62e479e9e 100644 (file)
@@ -2,7 +2,7 @@ import string
 import collections
 import norms
 import logging
-from itertools import zip_longest, cycle, permutations
+from itertools import zip_longest, cycle, permutations, starmap
 from segment import segment
 from multiprocessing import Pool
 from math import log10
@@ -183,67 +183,44 @@ def keyword_break_worker(message, keyword, wrap_alphabet, fitness):
                      wrap_alphabet, fit, sanitise(plaintext)[:50]))
     return (keyword, wrap_alphabet), fit
 
-def scytale_break(message, fitness=Pbigrams):
-    """Breaks a Scytale cipher
-    
-    >>> scytale_break('tfeulchtrtteehwahsdehneoifeayfsondmwpltmaoalhikotoere' \
-           'dcweatehiplwxsnhooacgorrcrcraotohsgullasenylrendaianeplscdriioto' \
-           'aek') # doctest: +ELLIPSIS
-    (6, -281.276219108...)
-    """
-    best_key = 0
-    best_fit = float("-inf")
-    for key in range(1, 20):
-        if len(message) % key == 0:
-            plaintext = scytale_decipher(message, key)
-            fit = fitness(sanitise(plaintext))
-            logger.debug('Scytale break attempt using key {0} gives fit of '
-                         '{1} and decrypt starting: {2}'.format(key, 
-                             fit, sanitise(plaintext)[:50]))
-            if fit > best_fit:
-                best_fit = fit
-                best_key = key
-    logger.info('Scytale break best fit with key {0} gives fit of {1} and '
-                'decrypt starting: {2}'.format(best_key, best_fit, 
-                    sanitise(scytale_decipher(message, best_key))[:50]))
-    return best_key, best_fit
-
 
 def column_transposition_break_mp(message, translist=transpositions, 
                      fitness=Pbigrams, chunksize=500):
     """Breaks a column transposition cipher using a dictionary and 
     n-gram frequency analysis
+
+    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
+            "It is a truth universally acknowledged, that a single man in \
+             possession of a good fortune, must be in want of a wife. However \
+             little known the feelings or views of such a man may be on his \
+             first entering a neighbourhood, this truth is so well fixed in the \
+             minds of the surrounding families, that he is considered the \
+             rightful property of some one or other of their daughters."), \
+        'encipher'), \
+        translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
+                   (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
+                   (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
+    (((2, 0, 5, 3, 1, 4, 6), False, False), -709.4646722...)
+    >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
+            "It is a truth universally acknowledged, that a single man in \
+             possession of a good fortune, must be in want of a wife. However \
+             little known the feelings or views of such a man may be on his \
+             first entering a neighbourhood, this truth is so well fixed in the \
+             minds of the surrounding families, that he is considered the \
+             rightful property of some one or other of their daughters."), \
+        'encipher'), \
+        translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
+                   (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
+                   (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
+        fitness=Ptrigrams) # doctest: +ELLIPSIS
+    (((2, 0, 5, 3, 1, 4, 6), False, False), -997.0129085...)
     """
-    # >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-    #         "It is a truth universally acknowledged, that a single man in \
-    #          possession of a good fortune, must be in want of a wife. However \
-    #          little known the feelings or views of such a man may be on his \
-    #          first entering a neighbourhood, this truth is so well fixed in the \
-    #          minds of the surrounding families, that he is considered the \
-    #          rightful property of some one or other of their daughters."), \
-    #     'encipher'), \
-    #     translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
-    #                (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
-    #                (6, 1, 0, 4, 5, 3, 2): ['keyword']}) # doctest: +ELLIPSIS
-    # (((2, 0, 5, 3, 1, 4, 6), False), 0.0628106372...)
-    # >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-    #         "It is a truth universally acknowledged, that a single man in \
-    #          possession of a good fortune, must be in want of a wife. However \
-    #          little known the feelings or views of such a man may be on his \
-    #          first entering a neighbourhood, this truth is so well fixed in the \
-    #          minds of the surrounding families, that he is considered the \
-    #          rightful property of some one or other of their daughters."), \
-    #     'encipher'), \
-    #     translist={(2, 0, 5, 3, 1, 4, 6): ['encipher'], \
-    #                (5, 0, 6, 1, 3, 4, 2): ['fourteen'], \
-    #                (6, 1, 0, 4, 5, 3, 2): ['keyword']}, \
-    #     target_counts=normalised_english_trigram_counts) # doctest: +ELLIPSIS
-    # (((2, 0, 5, 3, 1, 4, 6), False), 0.0592259560...)
-    # """
     with Pool() as pool:
-        helper_args = [(message, trans, columnwise, fitness) 
+        helper_args = [(message, trans, fillcolumnwise, emptycolumnwise, 
+                          fitness) 
                        for trans in translist.keys() 
-                       for columnwise in [True, False]]
+                       for fillcolumnwise in [True, False]
+                       for emptycolumnwise in [True, False]]
         # Gotcha: the helper function here needs to be defined at the top level 
         #   (limitation of Pool.starmap)
         breaks = pool.starmap(column_transposition_break_worker, 
@@ -251,64 +228,56 @@ def column_transposition_break_mp(message, translist=transpositions,
         return max(breaks, key=lambda k: k[1])
 column_transposition_break = column_transposition_break_mp
 
-def column_transposition_break_worker(message, transposition, columnwise, 
-                                        fitness):
-    plaintext = column_transposition_decipher(message, transposition, columnwise=columnwise)
+def column_transposition_break_worker(message, transposition, 
+        fillcolumnwise, emptycolumnwise, fitness):
+    plaintext = column_transposition_decipher(message, transposition, 
+        fillcolumnwise=fillcolumnwise, emptycolumnwise=emptycolumnwise)
     fit = fitness(sanitise(plaintext))
     logger.debug('Column transposition break attempt using key {0} '
                          'gives fit of {1} and decrypt starting: {2}'.format(
                              transposition, fit, 
                              sanitise(plaintext)[:50]))
-    return (transposition, columnwise), fit
-
-
-def transposition_break_exhaustive(message, fitness=Pbigrams):
-    best_transposition = ''
-    best_pw = float('-inf')
-    for keylength in range(1, 21):
-        if len(message) % keylength == 0:
-            for transposition in permutations(range(keylength)):
-                for columnwise in [True, False]:
-                    plaintext = column_transposition_decipher(message, 
-                        transposition, columnwise=columnwise)
-                    fit=fitness(plaintext)
-                    logger.debug('Column transposition break attempt using key {0} {1} '
-                         'gives fit of {2} and decrypt starting: {3}'.format(
-                             transposition, columnwise, pw, 
-                             sanitise(plaintext)[:50]))
-                    if fit > best_fit:
-                        best_transposition = transposition
-                        best_columnwise = columnwise
-                        best_fit = fit
-    return (best_transposition, best_columnwise), best_pw
+    return (transposition, fillcolumnwise, emptycolumnwise), fit
 
 
-def vigenere_keyword_break(message, wordlist=keywords, fitness=Pletters):
-    """Breaks a vigenere cipher using a dictionary and 
-    frequency analysis
-    
-    >>> vigenere_keyword_break(vigenere_encipher(sanitise('this is a test ' \
-             'message for the vigenere decipherment'), 'cat'), \
-             wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    ('cat', -52.947271216...)
+def scytale_break_mp(message, max_key_length=20,
+                     fitness=Pbigrams, chunksize=500):
+    """Breaks a scytale cipher using a range of lengths and
+    n-gram frequency analysis
+
+    >>> scytale_break_mp(scytale_encipher(sanitise( \
+            "It is a truth universally acknowledged, that a single man in \
+             possession of a good fortune, must be in want of a wife. However \
+             little known the feelings or views of such a man may be on his \
+             first entering a neighbourhood, this truth is so well fixed in the \
+             minds of the surrounding families, that he is considered the \
+             rightful property of some one or other of their daughters."), \
+        5)) # doctest: +ELLIPSIS
+    (5, -709.4646722...)
+    >>> scytale_break_mp(scytale_encipher(sanitise( \
+            "It is a truth universally acknowledged, that a single man in \
+             possession of a good fortune, must be in want of a wife. However \
+             little known the feelings or views of such a man may be on his \
+             first entering a neighbourhood, this truth is so well fixed in the \
+             minds of the surrounding families, that he is considered the \
+             rightful property of some one or other of their daughters."), \
+        5), \
+        fitness=Ptrigrams) # doctest: +ELLIPSIS
+    (5, -997.0129085...)
     """
-    best_keyword = ''
-    best_fit = float("-inf")
-    for keyword in wordlist:
-        plaintext = vigenere_decipher(message, keyword)
-        fit = fitness(plaintext)
-        logger.debug('Vigenere break attempt using key {0} '
-                         'gives fit of {1} and decrypt starting: {2}'.format(
-                             keyword, fit, 
-                             sanitise(plaintext)[:50]))
-        if fit > best_fit:
-            best_fit = fit
-            best_keyword = keyword
-    logger.info('Vigenere break best fit with key {0} gives fit '
-                'of {1} and decrypt starting: {2}'.format(best_keyword, 
-                    best_fit, sanitise(
-                        vigenere_decipher(message, best_keyword))[:50]))
-    return best_keyword, best_fit
+    with Pool() as pool:
+        helper_args = [(message, trans, False, True, fitness) 
+            for trans in 
+                [[col for col in range(math.ceil(len(message)/rows))] 
+                    for rows in range(1,max_key_length+1)]]
+        # Gotcha: the helper function here needs to be defined at the top level 
+        #   (limitation of Pool.starmap)
+        breaks = pool.starmap(column_transposition_break_worker, 
+          helper_args, chunksize) 
+        best = max(breaks, key=lambda k: k[1]) 
+        return math.trunc(len(message) / len(best[0][0])), best[1]
+scytale_break = scytale_break_mp
+
 
 def vigenere_keyword_break_mp(message, wordlist=keywords, fitness=Pletters, 
                      chunksize=500):
@@ -327,6 +296,7 @@ def vigenere_keyword_break_mp(message, wordlist=keywords, fitness=Pletters,
         #   (limitation of Pool.starmap)
         breaks = pool.starmap(vigenere_keyword_break_worker, helper_args, chunksize) 
         return max(breaks, key=lambda k: k[1])
+vigenere_keyword_break = vigenere_keyword_break_mp
 
 def vigenere_keyword_break_worker(message, keyword, fitness):
     plaintext = vigenere_decipher(message, keyword)
@@ -338,7 +308,7 @@ def vigenere_keyword_break_worker(message, keyword, fitness):
 
 
 
-def vigenere_frequency_break(message, fitness=Pletters):
+def vigenere_frequency_break(message, max_key_length=20, fitness=Pletters):
     """Breaks a Vigenere cipher with frequency analysis
 
     >>> vigenere_frequency_break(vigenere_encipher(sanitise("It is time to " \
@@ -350,26 +320,19 @@ def vigenere_frequency_break(message, fitness=Pletters):
             "sure"), 'florence')) # doctest: +ELLIPSIS
     ('florence', -307.5473096791...)
     """
-    best_fit = float("-inf")
-    best_key = ''
-    sanitised_message = sanitise(message)
-    for trial_length in range(1, 20):
-        splits = every_nth(sanitised_message, trial_length)
+    def worker(message, key_length, fitness):
+        splits = every_nth(sanitised_message, key_length)
         key = ''.join([chr(caesar_break(s)[0] + ord('a')) for s in splits])
-        plaintext = vigenere_decipher(sanitised_message, key)
+        plaintext = vigenere_decipher(message, key)
         fit = fitness(plaintext)
-        logger.debug('Vigenere key length of {0} ({1}) gives fit of {2}'.
-                     format(trial_length, key, fit))
-        if fit > best_fit:
-            best_fit = fit
-            best_key = key
-    logger.info('Vigenere break best fit with key {0} gives fit '
-                'of {1} and decrypt starting: {2}'.format(best_key, 
-                    best_fit, sanitise(
-                        vigenere_decipher(message, best_key))[:50]))
-    return best_key, best_fit
-
-def beaufort_frequency_break(message, fitness=Pletters):
+        return key, fit
+    sanitised_message = sanitise(message)
+    results = starmap(worker, [(sanitised_message, i, fitness) 
+        for i in range(1, max_key_length+1)])
+    return max(results, key=lambda k: k[1])
+
+
+def beaufort_frequency_break(message, max_key_length=20, fitness=Pletters):
     """Breaks a Beaufort cipher with frequency analysis
 
     >>> beaufort_frequency_break(beaufort_encipher(sanitise("It is time to " \
@@ -381,25 +344,16 @@ def beaufort_frequency_break(message, fitness=Pletters):
             "that he is sure"), 'florence')) # doctest: +ELLIPSIS
     ('florence', -307.5473096791...)
     """
-    best_fit = float("-inf")
-    best_key = ''
-    sanitised_message = sanitise(message)
-    for trial_length in range(1, 20):
-        splits = every_nth(sanitised_message, trial_length)
+    def worker(message, key_length, fitness):
+        splits = every_nth(sanitised_message, key_length)
         key = ''.join([chr(-caesar_break(s)[0] % 26 + ord('a')) for s in splits])
-        plaintext = beaufort_decipher(sanitised_message, key)
+        plaintext = beaufort_decipher(message, key)
         fit = fitness(plaintext)
-        logger.debug('Beaufort key length of {0} ({1}) gives fit of {2}'.
-                     format(trial_length, key, fit))
-        if fit > best_fit:
-            best_fit = fit
-            best_key = key
-    logger.info('Beaufort break best fit with key {0} gives fit '
-                'of {1} and decrypt starting: {2}'.format(best_key, 
-                    best_fit, sanitise(
-                        beaufort_decipher(message, best_key))[:50]))
-    return best_key, best_fit
-
+        return key, fit
+    sanitised_message = sanitise(message)
+    results = starmap(worker, [(sanitised_message, i, fitness) 
+        for i in range(1, max_key_length+1)])
+    return max(results, key=lambda k: k[1])
 
 
 def plot_frequency_histogram(freqs, sort_key=None):
index cca64e86cd51a8a3caff61b2a3a95caa9b9dc1e6..58b27f6fb3bf780c92e4204cb7822336d9aee014 100644 (file)
@@ -51,7 +51,7 @@ a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t |
 --|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|--
 b | e | h | k | n | q | t | w | z | c | f | i | l | o | r | u | x | a | d | g | j | m | p | s | v | y
 
-----
+---
 
 # Duplicate and extend your `caesar_break()` function
 
index 1a09109d9920a0f07cf70174300d05d61aaadaf8..30f3900f7525cb694ce2fdbcb79df60d40b7f9e5 100644 (file)
@@ -101,7 +101,7 @@ World's oldest algorithm.
 
 _a_ = _qb_ + _r_ ; gcd(_a_, _b_) = gcd(_qb_ + _r_, _b_) = gcd(_r_, _b_) = gcd(_b_, _r_)
 
-Repeatedly apply these steps until _r_ = 0, when the other number = gcd(a,b). For instance, _a_ = 81, _b_ = 57
+Repeatedly apply these steps until _r_ = 0, when the other number = gcd(_a_, _b_). For instance, _a_ = 81, _b_ = 57
 
 * 81 = 1 × 57 + 24
 * 57 = 2 × 24 + 9
@@ -130,6 +130,8 @@ Can we do this in one pass?
 
 # Triple constraints
 
+.float-right[![right-aligned GCD](fast-good-cheap.gif)]
+
 ## Fast, cheap, good: pick two
 
 ## Programmer time, execution time, space: pick one, get some of another.
diff --git a/slides/fast-good-cheap.gif b/slides/fast-good-cheap.gif
new file mode 100644 (file)
index 0000000..63411f0
Binary files /dev/null and b/slides/fast-good-cheap.gif differ
index 4a2772c03ddfad8a2ece4bfee7b8f5b086c4be47..49160bbb4fe394a3a5f9d88a05a797c62b732372 100644 (file)
@@ -86,6 +86,28 @@ But before we get there, a couple of diversions...
 
 ---
 
+# DRYing code
+
+Three cipher breaking tasks so far.
+
+All working on the same principle:
+
+```
+find a way to enumerate all the possible keys
+initialise 'best so far'
+for each key:
+    decipher message with this key
+    score it
+    if it's better than the best so far:
+        update best so far
+```
+
+Repetition of code is a bad smell.
+
+Separate the 'try all keys, keep the best' logic from the 'score this one key' logic.
+
+---
+
 # map()
 
 A common task is to apply a function to each item in a sequence, returning a sequence of the results.