Rearranged files, added import paths
[cipher-tools.git] / cipher / amsco.py
diff --git a/cipher/amsco.py b/cipher/amsco.py
new file mode 100644 (file)
index 0000000..4eeeb59
--- /dev/null
@@ -0,0 +1,204 @@
+from enum import Enum
+import multiprocessing 
+import itertools
+
+from utilities import *
+from language_models import *
+from column_transposition import transpositions
+
+from logger import logger
+
+# Where each piece of text ends up in the AMSCO transpositon cipher.
+# 'index' shows where the slice appears in the plaintext, with the slice
+# from 'start' to 'end'
+AmscoSlice = collections.namedtuple('AmscoSlice', ['index', 'start', 'end'])
+
+class AmscoFillStyle(Enum):
+    continuous = 1
+    same_each_row = 2
+    reverse_each_row = 3
+
+def amsco_transposition_positions(message, keyword, 
+      fillpattern=(1, 2),
+      fillstyle=AmscoFillStyle.continuous,
+      fillcolumnwise=False,
+      emptycolumnwise=True):
+    """Creates the grid for the AMSCO transposition cipher. Each element in the
+    grid shows the index of that slice and the start and end positions of the
+    plaintext that go to make it up.
+
+    >>> amsco_transposition_positions(string.ascii_lowercase, 'freddy', \
+        fillpattern=(1, 2)) # doctest:  +NORMALIZE_WHITESPACE
+    [[AmscoSlice(index=3, start=4, end=6),
+     AmscoSlice(index=2, start=3, end=4),
+     AmscoSlice(index=0, start=0, end=1),
+     AmscoSlice(index=1, start=1, end=3),
+     AmscoSlice(index=4, start=6, end=7)],
+    [AmscoSlice(index=8, start=12, end=13),
+     AmscoSlice(index=7, start=10, end=12),
+     AmscoSlice(index=5, start=7, end=9),
+     AmscoSlice(index=6, start=9, end=10),
+     AmscoSlice(index=9, start=13, end=15)],
+    [AmscoSlice(index=13, start=19, end=21),
+     AmscoSlice(index=12, start=18, end=19),
+     AmscoSlice(index=10, start=15, end=16),
+     AmscoSlice(index=11, start=16, end=18),
+     AmscoSlice(index=14, start=21, end=22)],
+    [AmscoSlice(index=18, start=27, end=28),
+     AmscoSlice(index=17, start=25, end=27),
+     AmscoSlice(index=15, start=22, end=24),
+     AmscoSlice(index=16, start=24, end=25),
+     AmscoSlice(index=19, start=28, end=30)]]
+    """
+    transpositions = transpositions_of(keyword)
+    fill_iterator = itertools.cycle(fillpattern)
+    indices = itertools.count()
+    message_length = len(message)
+
+    current_position = 0
+    grid = []
+    current_fillpattern = fillpattern
+    while current_position < message_length:
+        row = []
+        if fillstyle == AmscoFillStyle.same_each_row:
+            fill_iterator = itertools.cycle(fillpattern)
+        if fillstyle == AmscoFillStyle.reverse_each_row:
+            fill_iterator = itertools.cycle(current_fillpattern)
+        for _ in range(len(transpositions)):
+            index = next(indices)
+            gap = next(fill_iterator)
+            row += [AmscoSlice(index, current_position, current_position + gap)]
+            current_position += gap
+        grid += [row]
+        if fillstyle == AmscoFillStyle.reverse_each_row:
+            current_fillpattern = list(reversed(current_fillpattern))
+    return [transpose(r, transpositions) for r in grid]
+
+def amsco_transposition_encipher(message, keyword, 
+    fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
+    """AMSCO transposition encipher.
+
+    >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(1, 2))
+    'hoteelhler'
+    >>> amsco_transposition_encipher('hellothere', 'abc', fillpattern=(2, 1))
+    'hetelhelor'
+    >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(1, 2))
+    'hotelerelh'
+    >>> amsco_transposition_encipher('hellothere', 'acb', fillpattern=(2, 1))
+    'hetelorlhe'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'encode')
+    'etecstthhomoerereenisxip'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2))
+    'hetcsoeisterereipexthomn'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
+    'hecsoisttererteipexhomen'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(2, 1))
+    'heecisoosttrrtepeixhemen'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2))
+    'hxtomephescieretoeisnter'
+    >>> amsco_transposition_encipher('hereissometexttoencipher', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
+    'hxomeiphscerettoisenteer'
+    """
+    grid = amsco_transposition_positions(message, keyword, 
+        fillpattern=fillpattern, fillstyle=fillstyle)
+    ct_as_grid = [[message[s.start:s.end] for s in r] for r in grid]
+    return combine_every_nth(ct_as_grid)
+
+
+def amsco_transposition_decipher(message, keyword, 
+    fillpattern=(1,2), fillstyle=AmscoFillStyle.reverse_each_row):
+    """AMSCO transposition decipher
+
+    >>> amsco_transposition_decipher('hoteelhler', 'abc', fillpattern=(1, 2))
+    'hellothere'
+    >>> amsco_transposition_decipher('hetelhelor', 'abc', fillpattern=(2, 1))
+    'hellothere'
+    >>> amsco_transposition_decipher('hotelerelh', 'acb', fillpattern=(1, 2))
+    'hellothere'
+    >>> amsco_transposition_decipher('hetelorlhe', 'acb', fillpattern=(2, 1))
+    'hellothere'
+    >>> amsco_transposition_decipher('etecstthhomoerereenisxip', 'encode')
+    'hereissometexttoencipher'
+    >>> amsco_transposition_decipher('hetcsoeisterereipexthomn', 'cipher', fillpattern=(1, 2))
+    'hereissometexttoencipher'
+    >>> amsco_transposition_decipher('hecsoisttererteipexhomen', 'cipher', fillpattern=(1, 2), fillstyle=AmscoFillStyle.continuous)
+    'hereissometexttoencipher'
+    >>> amsco_transposition_decipher('heecisoosttrrtepeixhemen', 'cipher', fillpattern=(2, 1))
+    'hereissometexttoencipher'
+    >>> amsco_transposition_decipher('hxtomephescieretoeisnter', 'cipher', fillpattern=(1, 3, 2))
+    'hereissometexttoencipher'
+    >>> amsco_transposition_decipher('hxomeiphscerettoisenteer', 'cipher', fillpattern=(1, 3, 2), fillstyle=AmscoFillStyle.continuous)
+    'hereissometexttoencipher'
+    """
+
+    grid = amsco_transposition_positions(message, keyword, 
+        fillpattern=fillpattern, fillstyle=fillstyle)
+    transposed_sections = [s for c in [l for l in zip(*grid)] for s in c]
+    plaintext_list = [''] * len(transposed_sections)
+    current_pos = 0
+    for slice in transposed_sections:
+        plaintext_list[slice.index] = message[current_pos:current_pos-slice.start+slice.end][:len(message[slice.start:slice.end])]
+        current_pos += len(message[slice.start:slice.end])
+    return cat(plaintext_list)
+
+
+def amsco_break(message, translist=transpositions, patterns = [(1, 2), (2, 1)],
+                                  fillstyles = [AmscoFillStyle.continuous, 
+                                                AmscoFillStyle.same_each_row, 
+                                                AmscoFillStyle.reverse_each_row],
+                                  fitness=Pbigrams, 
+                                  chunksize=500):
+    """Breaks an AMSCO transposition cipher using a dictionary and
+    n-gram frequency analysis
+
+    >>> amsco_break(amsco_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']}, \
+        patterns=[(1, 2)]) # doctest: +ELLIPSIS
+    (((2, 0, 5, 3, 1, 4, 6), (1, 2), <AmscoFillStyle.continuous: 1>), -709.4646722...)
+    >>> amsco_break(amsco_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', fillpattern=(2, 1)), \
+        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']}, \
+        patterns=[(1, 2), (2, 1)], fitness=Ptrigrams) # doctest: +ELLIPSIS
+    (((2, 0, 5, 3, 1, 4, 6), (2, 1), <AmscoFillStyle.continuous: 1>), -997.0129085...)
+    """
+    with multiprocessing.Pool() as pool:
+        helper_args = [(message, trans, pattern, fillstyle, fitness)
+                       for trans in translist
+                       for pattern in patterns
+                       for fillstyle in fillstyles]
+        # Gotcha: the helper function here needs to be defined at the top level
+        #   (limitation of Pool.starmap)
+        breaks = pool.starmap(amsco_break_worker, helper_args, chunksize) 
+        return max(breaks, key=lambda k: k[1])
+
+def amsco_break_worker(message, transposition,
+        pattern, fillstyle, fitness):
+    plaintext = amsco_transposition_decipher(message, transposition,
+        fillpattern=pattern, fillstyle=fillstyle)
+    fit = fitness(sanitise(plaintext))
+    logger.debug('AMSCO transposition break attempt using key {0} and pattern'
+                         '{1} ({2}) gives fit of {3} and decrypt starting: '
+                         '{4}'.format(
+                             transposition, pattern, fillstyle, fit, 
+                             sanitise(plaintext)[:50]))
+    return (transposition, pattern, fillstyle), fit
+
+if __name__ == "__main__":
+    import doctest
\ No newline at end of file