Vigenere cipher breaking now with frequency analysis
[cipher-tools.git] / break.py
index 08b7301e5e6b09d0b85391e911012c9e90b807c0..7c27216daf0be2d680168302ecb1f379b65c8fdd 100644 (file)
--- a/break.py
+++ b/break.py
@@ -3,7 +3,7 @@ import collections
 import norms
 import logging
 from itertools import zip_longest, cycle
-from segment import segment
+from segment import segment, Pwords
 from multiprocessing import Pool
 
 from cipher import *
@@ -89,13 +89,13 @@ def caesar_break(message,
     
     >>> caesar_break('ibxcsyorsaqcheyklxivoexlevmrimwxsfiqevvmihrsasrxliwyrh' \
           'ecjsppsamrkwleppfmergefifvmhixscsymjcsyqeoixlm') # doctest: +ELLIPSIS
-    (4, 0.31863952890183...)
+    (4, 0.080345432737...)
     >>> caesar_break('wxwmaxdgheetgwuxztgptedbgznitgwwhpguxyhkxbmhvvtlbhgtee' \
           'raxlmhiixweblmxgxwmhmaxybkbgztgwztsxwbgmxgmert') # doctest: +ELLIPSIS
-    (19, 0.42152901235832...)
+    (19, 0.11189290326...)
     >>> caesar_break('yltbbqnqnzvguvaxurorgenafsbezqvagbnornfgsbevpnaabjurer' \
           'svaquvzyvxrnznazlybequrvfohgriraabjtbaruraprur') # doctest: +ELLIPSIS
-    (13, 0.316029208075451...)
+    (13, 0.08293968842...)
     """
     sanitised_message = sanitise(message)
     best_shift = 0
@@ -126,7 +126,7 @@ def affine_break(message,
           '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.') # doctest: +ELLIPSIS
-    ((15, 22, True), 0.23570361818655...)
+    ((15, 22, True), 0.0598745365924...)
     """
     sanitised_message = sanitise(message)
     best_multiplier = 0
@@ -167,7 +167,7 @@ def keyword_break(message,
     >>> keyword_break(keyword_encipher('this is a test message for the ' \
           'keyword decipherment', 'elephant', 1), \
           wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    (('elephant', 1), 0.41643991598441...)
+    (('elephant', 1), 0.1066453448861...)
     """
     best_keyword = ''
     best_wrap_alphabet = True
@@ -204,7 +204,7 @@ def keyword_break_mp(message,
     >>> keyword_break_mp(keyword_encipher('this is a test message for the ' \
           'keyword decipherment', 'elephant', 1), \
           wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    (('elephant', 1), 0.41643991598441...)
+    (('elephant', 1), 0.106645344886...)
     """
     with Pool() as pool:
         helper_args = [(message, word, wrap, metric, target_counts, 
@@ -234,7 +234,7 @@ def scytale_break(message,
     >>> scytale_break('tfeulchtrtteehwahsdehneoifeayfsondmwpltmaoalhikotoere' \
            'dcweatehiplwxsnhooacgorrcrcraotohsgullasenylrendaianeplscdriioto' \
            'aek') # doctest: +ELLIPSIS
-    (6, 0.83453041115025...)
+    (6, 0.092599933059...)
     """
     best_key = 0
     best_fit = float("inf")
@@ -265,22 +265,30 @@ def column_transposition_break(message,
     n-gram frequency analysis
 
     >>> column_transposition_break(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, \
-        when homosexual acts were still illegal in the United Kingdom. "), \
+            "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), 0.898128626285...)
+    ((2, 0, 5, 3, 1, 4, 6), 0.0628106372...)
     >>> column_transposition_break(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, " \
-        "when homosexual acts were still illegal in the United Kingdom."), \
+            "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), 1.1958792913127...)
+    ((2, 0, 5, 3, 1, 4, 6), 0.0592259560...)
     """
     best_transposition = ''
     best_fit = float("inf")
@@ -317,22 +325,30 @@ def column_transposition_break_mp(message,
     n-gram frequency analysis
 
     >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, \
-        when homosexual acts were still illegal in the United Kingdom. "), \
+            "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), 0.898128626285...)
+    ((2, 0, 5, 3, 1, 4, 6), 0.0628106372...)
     >>> column_transposition_break_mp(column_transposition_encipher(sanitise( \
-        "Turing's homosexuality resulted in a criminal prosecution in 1952, " \
-        "when homosexual acts were still illegal in the United Kingdom."), \
+            "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), 1.1958792913127...)
+    ((2, 0, 5, 3, 1, 4, 6), 0.0592259560...)
     """
     ngram_length = len(next(iter(target_counts.keys())))
     with Pool() as pool:
@@ -364,10 +380,10 @@ def vigenere_keyword_break(message,
     """Breaks a vigenere cipher using a dictionary and 
     frequency analysis
     
-    >>> vigenere_keyword_break(keyword_encipher('this is a test message for the ' \
-             'keyword decipherment', 'elephant', 1), \
+    >>> vigenere_keyword_break(vigenere_encipher(sanitise('this is a test ' \
+             'message for the vigenere decipherment'), 'cat'), \
              wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    ('elephant', 0.7166585201707...)
+    ('cat', 0.15965224935...)
     """
     best_keyword = ''
     best_fit = float("inf")
@@ -397,10 +413,10 @@ def vigenere_keyword_break_mp(message,
     """Breaks a vigenere cipher using a dictionary and 
     frequency analysis
 
-    >>> vigenere_keyword_break_mp(keyword_encipher('this is a test message for the ' \
-             'keyword decipherment', 'elephant', 1), \
+    >>> vigenere_keyword_break_mp(vigenere_encipher(sanitise('this is a test ' \
+             'message for the vigenere decipherment'), 'cat'), \
              wordlist=['cat', 'elephant', 'kangaroo']) # doctest: +ELLIPSIS
-    ('elephant', 0.7166585201707...)
+    ('cat', 0.159652249358...)
     """
     with Pool() as pool:
         helper_args = [(message, word, metric, target_counts, 
@@ -422,6 +438,41 @@ def vigenere_keyword_break_worker(message, keyword, metric, target_counts,
     return keyword, fit
 
 
+
+def vigenere_frequency_break(message,
+                  metric=norms.euclidean_distance, 
+                  target_counts=normalised_english_counts, 
+                  message_frequency_scaling=norms.normalise):
+    """Breaks a Vigenere cipher with frequency analysis
+
+    >>> vigenere_frequency_break(vigenere_encipher(sanitise("It is time to " \
+            "run. She is ready and so am I. I stole Daniel's pocketbook this " \
+            "afternoon when he left his jacket hanging on the easel in the " \
+            "attic."), 'florence')) # doctest: +ELLIPSIS
+    ('florence', 0.077657073...)
+    """
+    best_fit = float("inf")
+    best_key = ''
+    sanitised_message = sanitise(message)
+    for trial_length in range(1, 20):
+        splits = every_nth(sanitised_message, trial_length)
+        key = ''.join([chr(caesar_break(s, target_counts=target_counts)[0] + ord('a')) for s in splits])
+        plaintext = vigenere_decipher(sanitised_message, key)
+        counts = message_frequency_scaling(frequencies(plaintext))
+        fit = metric(target_counts, counts)
+        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
+
+
+
 if __name__ == "__main__":
     import doctest
     doctest.testmod()