Breaking hill ciphers done, challenge 6 done.
[cipher-training.git] / cipherbreak.py
index e563b81c071410c0bfc2350687d955942076da1f..b1af48c2e40d1ba60b948e75c758ab162f242233 100644 (file)
@@ -416,6 +416,19 @@ def scytale_break_mp(message, max_key_length=20,
 scytale_break = scytale_break_mp
 
 
+def railfence_break(message, max_key_length=20,
+                     fitness=Pletters, chunksize=500):
+    """Breaks a hill cipher using a matrix of given rank and letter frequencies
+
+    
+    """
+    
+    sanitised_message = sanitise(message)
+    results = starmap(worker, [(sanitised_message, i, fitness)
+                               for i in range(2, max_key_length+1)])
+    return max(results, key=lambda k: k[1])
+
+
 def railfence_break(message, max_key_length=20,
                      fitness=Pbigrams, chunksize=500):
     """Breaks a railfence cipher using a range of lengths and
@@ -445,13 +458,45 @@ def railfence_break(message, max_key_length=20,
         plaintext = railfence_decipher(message, height)
         fit = fitness(plaintext)
         return height, fit
-        
+
     sanitised_message = sanitise(message)
     results = starmap(worker, [(sanitised_message, i, fitness)
                                for i in range(2, max_key_length+1)])
     return max(results, key=lambda k: k[1])
 
 
+def hill_break(message, matrix_size=2, fitness=Pletters, 
+    number_of_solutions=1, chunksize=500):
+
+    all_matrices = [np.matrix(list(m)) 
+        for m in itertools.product([list(r) 
+            for r in itertools.product(range(26), repeat=matrix_size)], 
+        repeat=matrix_size)]
+    valid_matrices = [m for m, d in 
+        zip(all_matrices, (int(round(linalg.det(m))) for m in all_matrices))
+                  if d != 0
+                  if d % 2 != 0
+                  if d % 13 != 0 ]
+    with Pool() as pool:
+        helper_args = [(message, matrix, fitness)
+                       for matrix in valid_matrices]
+        # Gotcha: the helper function here needs to be defined at the top level
+        #   (limitation of Pool.starmap)
+        breaks = pool.starmap(hill_break_worker, helper_args, chunksize)
+        if number_of_solutions == 1:
+            return max(breaks, key=lambda k: k[1])
+        else:
+            return sorted(breaks, key=lambda k: k[1], reverse=True)[:number_of_solutions]
+
+def hill_break_worker(message, matrix, fitness):
+    plaintext = hill_decipher(matrix, message)
+    fit = fitness(plaintext)
+    logger.debug('Hill cipher break attempt using key {0} gives fit of '
+                 '{1} and decrypt starting: {2}'.format(matrix, 
+                     fit, sanitise(plaintext)[:50]))
+    return matrix, fit
+
+
 def pocket_enigma_break_by_crib(message, wheel_spec, crib, crib_position):
     """Break a pocket enigma using a crib (some plaintext that's expected to
     be in a certain position). Returns a list of possible starting wheel