Crack Vigenere-ciphered text

crackvigenere.png

The last part of the console output of a successful cryptographic attack.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
Crack Vigenere
~~~~~~~~~~~~~~

Searches for the key (length must be given) to a text encrypted
with the `Vigenere cipher`_.  Cracking works by analyzing the
frequency of occurences of letters.  When the keyword is found,
the cipher is decrypted.

I'm not sure if it's absolutely correct, but it succeeded in
solving my tasks.

:Copyright: 2004-2008 Jochen Kupperschmidt
:Date: 11-Apr-2008
:License: MIT

.. _Vigenere cipher: http://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher
"""

from collections import defaultdict
from itertools import chain, imap, izip


# Average letter occurence chances in English text.
p = {
    'A': .082, 'B': .015, 'C': .028, 'D': .043,
    'E': .127, 'F': .022, 'G': .020, 'H': .061,
    'I': .070, 'J': .002, 'K': .008, 'L': .040,
    'M': .024, 'N': .067, 'O': .075, 'P': .019,
    'Q': .001, 'R': .060, 'S': .063, 'T': .091,
    'U': .028, 'V': .010, 'W': .023, 'X': .001,
    'Y': .020, 'Z': .001
}

def crack(cipher, m, details=False):
    """Crack."""
    cipher = cipher.upper()

    # Split cipher.
    y = defaultdict(list)
    for i, char in enumerate(cipher):
        y[i % m].append(char)

    # Analyze sections separately.
    for si in xrange(len(y)):
        section = y[si]
        section_len = len(section)

        show_headline('section %d' % (si + 1))
        print wrap(section)
        print ' -> length: ' , section_len

        # Count letters.
        c = dict((key, 0) for key in p)
        for char in section:
            c[char] += 1

        # Compute letter dispersion.
        h = dict((key, float(c[key]) / section_len) for key in c)

        # All the magic happens here :)
        gs = []
        for g in xrange(26):
            r = 0
            letter_g = chr(g + 65)
            for i in xrange(26):
                p_i = p[chr(i + 65)]
                h_ig = h[chr(((i + g) % 26) + 65)]
                r += p_i * h_ig
            if details:
                print " -> '%c' = %.3f" % (letter_g, r)
            gs.append((letter_g, r))

        # Fetch best suiting value.
        desirable = .065
        nearest_value = 999
        nearest_index = 0
        for i, g in enumerate(gs):
            difference = abs(desirable - g[1])
            if difference < nearest_value:
                nearest_value = difference
                nearest_index = i
        print " -> nearest: '%c' by %.3f" % gs[nearest_index]
        yield gs[nearest_index][0]

def decipher(cipher, keyword):
    """Decipher the text using ``keyword``."""
    cipher = cipher.upper()
    keyword = keyword.upper()
    keyword_values = map(ord, keyword)
    keyword_len = len(keyword)
    for i, char in enumerate(cipher):
        char = ord(char) - keyword_values[i % keyword_len] + 65
        if char < 65:
            char += 26
        char += 32  # Lowercase; this way instead of `.upper()`.
        yield chr(char)

def show_headline(title, width=47, fillchar='='):
    """Display a formatted headline with fixed width."""
    beginning = (fillchar * 3) + ('( %s )' % title)
    print '\n' + beginning.ljust(width, fillchar)

def group(iterable, n, padvalue=''):
    """Group ``iterable`` into chunks of ``n`` items.

    The last chunk is padded with ``padvalue``, if necessary.
    """
    return izip(*[chain(iterable, [padvalue] * (n - 1))] * n)

def wrap(string, blocks_per_line=8, block_width=5):
    """Wrap long lines into cute little blocks."""
    blocks = imap(''.join, group(string, block_width))
    lines = imap(' '.join, group(blocks, blocks_per_line))
    return '\n'.join(lines)

def main(cipher, kw_len):
    print 'Cracking the Vigenere cipher.\n'
    details = (raw_input('Show details? [y/N] ') in ('y', 'Y'))
    show_headline('cipher')
    print wrap(cipher)
    print ' -> assumed keyword length:', kw_len
    keyword = ''.join(crack(cipher, kw_len, details))
    show_headline('deciphered')
    print wrap(decipher(cipher, keyword))
    print ' -> keyword: "%s"\n' % keyword

if __name__ == '__main__':
    # Example 1
    cipher = 'KCCPKBGUFDPHQTYAVINRRTMVGRKDNBVFDETDGILTXRGUDDKOTFMBPVGEGLTGCKQRACQCWDNAWCRXIZAKFTLEWRPTYCQKYVXCHKFTPONCQQRHJVAJUWETMCMSPKQDYHJVDAHCTRLSVSKCGCZQQDZXGSFRLSWCWSJTBHAFSIASPRJAHKJRJUMVGKMITZHFPDISPZLVLGWTFPLKKEBDPGCEBSHCTJRWXBAFSPEZQNRWXCVYCGAONWDDKACKAWBBIKFTIOVKCGGHJVLNHIFFSQESVYCLACNVRWBBIREPBBVFEXOSCDYGZWPFDTKFQIYCWHJVLNHIQIBTKHJVNPIST'
    kw_len = 6

    # Example 2
    ##cipher = 'CHREEVOAHMAERATBIAXXWTNXBEEOPHBSBQMQEQERBWRVXUOAKXAOSXXWEAHBWGJMMQMNKGRFVGXWTRZXWIAKLXFPSKAUTEMNDCMGTSXMXBTUIADNGMGPSRELXNJELXVRVPRTULHDNQWTWDTYGBPHXTFALJHASVBFXNGLLCHRZBWELEKMSJIKNBHWRJGNMGJSGLXFEYPHAGNRBIEQJTAMRVLCRREMNDGLXRRIMGNSNRWCHRQHAEYEVTAQEBBIPEEWEVKAKOEWADREMXMTBHHCHRTKDNVRZCHRCLQOHPWQAIIWXNRMGWOIIFKEE'
    ##kw_len = 5

    main(cipher, kw_len)