diff --git a/one-time-pad/.gitignore b/one-time-pad/.gitignore new file mode 100644 index 0000000..fab747d --- /dev/null +++ b/one-time-pad/.gitignore @@ -0,0 +1,7 @@ +*.pyc +.coverage +*.egg-info +build +dist +setup.py +paver-minilib.zip diff --git a/one-time-pad/MANIFEST.in b/one-time-pad/MANIFEST.in new file mode 100644 index 0000000..305c839 --- /dev/null +++ b/one-time-pad/MANIFEST.in @@ -0,0 +1 @@ +include *.py *.zip diff --git a/one-time-pad/README b/one-time-pad/README new file mode 100644 index 0000000..89a723f --- /dev/null +++ b/one-time-pad/README @@ -0,0 +1,3 @@ +Sure, they aren't especially practical, but one-time pads are academically interesting :) + +.. vim:filetype=rst diff --git a/one-time-pad/itest_onetimepad.py b/one-time-pad/itest_onetimepad.py new file mode 100644 index 0000000..45a6180 --- /dev/null +++ b/one-time-pad/itest_onetimepad.py @@ -0,0 +1,191 @@ +import unittest +import onetimepad as OT + + +class TestMod25(unittest.TestCase): + msg = ('HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFA' + 'WOODCHUCKCOULDCHUCKWOOD' * 50) + _padsize = 2000 + PADLINES = (1, 4, 7, 10, 13, 16, 19, 22, 25) + TEXTLINES = (2, 5, 8, 11, 14, 17, 20, 23, 26) + CIPHERLINES = (3, 6, 9, 12, 15, 18, 21, 24, 27) + + TEST_PADLINE = 'HUCHUGGHOBUXADDHOHPGQBKXQKAOLRL' + TEST_TEXTLINE = 'THISCAKEISUNSATISFACTORYTOMEYES' + TEST_CIPHERLINE = 'BCMAXHRNXUPLTEXRGOQKKQBWKYNTKWD' + + TEST_MSG = \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOO' + + TEST_CIPHER = \ + 'FMHZRGHRITAMHVAOLBANWMNYPBYDCFCAVXAEEKXUTYXLUCYQVYPWZFSKYI' \ + 'LWKCHZSXLFCMZIEFHXKVUXVNBIQVSNYPTFVIZHSVTDCKWUSZHEHUPLZSUY' \ + 'BTPSMEISGTYDWCXVUCPGHHKXDOUHGCVDTDUQVKFDRDCHFIKTNFGDFODCQS' \ + 'RIWGZBLPTCSXLMPQODYLHQNGWNSCTHNDOZEUXWCEQAZMRGZIQUTUYSKOGW' \ + 'YGYPEDNUZDWBEPUHLWWBHLGADYTCNGEUFKPLQSAMMXCTXWQEOYBNKQAHBV' \ + 'PDKTMQSHDFCFBCVKXVTRSXRTBHAGKNLFASNVNENHHRNGCKXALMIBNZKOTL' \ + 'BZMWGSNMTLDNPXKVQZZUAZCSPWFNRKAMTNMCPHWGLGBVSNFDXSKVUFBZBW' \ + 'WICBUPCWZDUXCZTXHKQBMQNTBHAAGMBPKUIDTHITUEWEPPMACPXERXRYND' \ + 'RWQSBQQMSQRVWZMAQQKUGHLELKFLKHGNFFDHIWUZPDIMIOELSIHMUIZQCW' \ + 'KLPZQIFAXNDRRIIIHOYBGSCIYBIVSRTECEOZSBLYTNCXGQXEWMGIXHKBIL' \ + 'TOKWHGABPMXNAHEWFKIMOQPHVHSYZABWLMLSSDEHQIBNEMEMHCWOUBIIZS' \ + 'XPFWXMQNOTEANYGQBVMZAXNPAMWSUOGFAFFYUTKDZYHFTPYGOMDHESKOZI' \ + 'IPDXAMSAQOTGAQKYPYWWUYGHSBVODAHDSKDVPMLWRCXQXGAXQZBFNKXFKU' \ + 'WUDRWNRKECIQIUEEMQNCAZDDDOQOEGUVFVCTKLTISNOQILEKUYXCYZNHHZ' \ + 'OFHCSLOPPQAFLHHDZANGTXNQSDZSAFLLSGWULVHIDERMTZZHTDHTZPHEFN' \ + 'ORDEIYOVTTAWVTOOXSLZBOPMRCCKSMHQOVFERCOVUMWXFSNEMFALNWGRQC' \ + 'NLWELLXNCHZGXQEUGEVNCWMHRNGUTBKTVEXGASBRCEBSFITOEFSNIBTLME' \ + 'MERMOFVBNTFCTTWEGAIGRZQBBFOKOPXNCWXFBK' + + TEST_PAD = """\ + XXLNWDZOAYXBEGFCGADYHHKQUYOAULZQMRZHQVTRLDUARODERVGBWVVVKECHNQNWKUCLZBWU + + + KUDWNGFTSEGFFSKSVEKZUMLTOSLIZZTFXODBZZMACDFUTESFRBAPYYVTTOCKQBSSTDGPILKE + + + YHSTKXTTGVBAIIZXCUPHICYICDGOBOIUZUEYCMLHPNHXUEKCBWTMKYBKHZLNKTETDXIHYBHF + + + WBOSEXMRLZVHNZSSQSBCKAERRITRBAZWGVZNTGDSIVIZEMBKWDOOBDWIDCZIUHVTKVTSGFDT + + + NRGPNGRNKEVLZVYOAYTUWCDTOLGEQDBSHVRMMYYQIEZWKWZVCPGIAGKPNZEGTLPKMPEILQAC + + + MIPKMYCFMVZKUTVKIPXBKGLFATSDCMYLPYLSTPBARVELNSOUFPZMUTRIRNZLYMDITNXMKLGE + + + QXYRYEAOHGETEQMKTUMARPYMPKONUKYZIHTFGNHIKVOLTLRPMPNFSDHWQGVHBNDCWZCLUHQW + + + GIFBFZKZOFZRRYCBOSBWSNVFXXPSAGOUOXDNBNSOZADYYSKWQUTYNCDNGVLSZNDBCTSIYOUX + + + NNUGLZNKNDSYGRUCXTKLBIMXZMMZAEHVRFYMBFKVDPAEHOYCBXKADZOTRRMULOPAIKCIHKFY + + + BQKKMEXUPLMTKGFIMPMTDVRZEBFEFARDEVQADVKIVNBHNZLEAAGLFIKXHTQWXBPNLXZHFUDZ + + + XYLLVFETIDCYAXGTIHUFUSFMMWTLKZARVQOFGEBKIGWHFFFFKTHPQOMVAVILFLWMRLWPBWVW + + + PFKSLFFWKYQVPHVPQTTVFRLQXHFMGVXVHTNSVZQSETKHXAPPSLHAIAVXWGDFVKOBQLEWPAZY + + + WELQRIFCGSOVFSLYXMSETCTROLNKMDWZSGKREFEPEHCOKSMRTNCDSTHCSQKMKCBYEWZSQHPK + + + UNWWUBKICDYYOSIZWKWRLGGIMYWKMDXOUKYHCUYCACKSFRWWXADQUZTCNYEYCSQEBTCZMSCV + + + NTGCDGFUUCTQWINV........................................................ + + """ + + def setUp(self): + self.m25 = OT.Mod25() + + def test_mk_as_alpha_excludes_j(self): + self.assertTrue('J' not in self.m25._as_alpha.values()) + + def test_mk_as_alpha_dict_has_25_members(self): + self.assertEqual(25, len(self.m25._as_alpha.items())) + + def test_creates_pad_of_desired_length(self): + for width in (72, 33, 99, 111): + pad = self.m25.create_pad(self._padsize, width=width) + lines = [line.strip('.') for line in pad.split()] + actual = len(''.join(lines)) + self.assertEqual(self._padsize, len(''.join(lines)), + 'pad of {0} chars created at width ' + '{1}, actual={2}'.format(self._padsize, + width, actual)) + + def test_is_padline(self): + for lineno in self.PADLINES: + self.assertTrue(self.m25._is_padline(lineno), + 'line {0} is padline'.format(lineno)) + for lineno in self.TEXTLINES: + self.assertFalse(self.m25._is_padline(lineno), + 'line {0} is not padline'.format(lineno)) + for lineno in self.CIPHERLINES: + self.assertFalse(self.m25._is_padline(lineno), + 'line {0} is not padline'.format(lineno)) + + def test_is_textline(self): + for lineno in self.TEXTLINES: + self.assertTrue(self.m25._is_textline(lineno), + 'line {0} is textline'.format(lineno)) + for lineno in self.PADLINES: + self.assertFalse(self.m25._is_textline(lineno), + 'line {0} is not textline'.format(lineno)) + for lineno in self.CIPHERLINES: + self.assertFalse(self.m25._is_textline(lineno), + 'line {0} is not textline'.format(lineno)) + + def test_is_cipherline(self): + for lineno in self.CIPHERLINES: + self.assertTrue(self.m25._is_cipherline(lineno), + 'line {0} is cipherline'.format(lineno)) + for lineno in self.PADLINES: + self.assertFalse(self.m25._is_cipherline(lineno), + 'line {0} is not cipherline'.format(lineno)) + for lineno in self.TEXTLINES: + self.assertFalse(self.m25._is_cipherline(lineno), + 'line {0} is not cipherline'.format(lineno)) + + def test_make_cipherline_from_padline_and_textline(self): + actual = \ + self.m25._cipherline_from_padline_and_textline(self.TEST_PADLINE, + self.TEST_TEXTLINE) + self.assertEqual(self.TEST_CIPHERLINE, actual) + + def test_make_textline_from_cipherline_and_padline(self): + actual = self.m25._textline_from_cipherline_and_padline( + self.TEST_CIPHERLINE, self.TEST_PADLINE) + self.assertEqual(self.TEST_TEXTLINE, actual) + + def test_encode_equals_expected(self): + ciphertext = OT.mod25encode(self.TEST_MSG, self.TEST_PAD) + self.assertEqual(self.TEST_CIPHER, ciphertext) + + def test_decode_equals_expected(self): + text = OT.mod25decode(self.TEST_CIPHER, self.TEST_PAD) + self.assertEqual(self.TEST_MSG, text) + + +class TestOneTimePad(unittest.TestCase): + + def test_get_randint_on_nonexistent_randomness_file_fails_ioerror(self): + OT._DEV_URANDOM['fp'] = None + self.assertRaises(IOError, OT._get_randint, + 25, randomness_file='/foo/bar/busted/borken', + fallback_to_fake=False) + + def test_get_randint_on_nonexistent_randomness_file_uses_fake(self): + OT._DEV_URANDOM['fp'] = None + random_int = OT._get_randint(32, randomness_file='/broke/as/joke') + self.assertTrue(random_int in range(0, 255)) + + +if __name__ == '__main__': + unittest.main() diff --git a/one-time-pad/onetimepad.py b/one-time-pad/onetimepad.py new file mode 100644 index 0000000..1ea151f --- /dev/null +++ b/one-time-pad/onetimepad.py @@ -0,0 +1,220 @@ +from itertools import izip + +DEFAULT_PAD_WIDTH = 72 + + +def mod25encode(msg, pad): + mod25 = Mod25(pad=pad) + return mod25.encode(msg) + + +def mod25decode(msg, pad): + mod25 = Mod25(pad=pad) + return mod25.decode(msg) + + +class Mod25(object): + _pad_modulo = 3 + _padline_offset = -2 + _textline_offset = -1 + _cipher_floor = 1 + _cipher_ceil = 25 + _nullchar = '.' + + def __init__(self, pad=''): + self.pad = pad + self._as_alpha = {} + self._as_nums = {} + self._mk_as_alpha_as_nums() + + def _mk_as_alpha_as_nums(self): + as_alpha = {} + as_nums = {} + a_chr = ord('A') + past_j = False + + for char in range(self._cipher_ceil + 1): + letter = chr(a_chr + char) + if letter != 'J': + key = char + (1 if not past_j else 0) + as_alpha[key] = letter + else: + past_j = True + + for key, val in as_alpha.iteritems(): + as_nums[val] = key + + self._as_alpha.update(as_alpha) + self._as_nums.update(as_nums) + + def encode(self, msg): + ret = [] + filled = list(self._text_padfill(msg)) + for i, line in enumerate(self._text_cipherfill(filled)): + lineno = i + 1 + if self._is_cipherline(lineno): + ret.append(line) + return ''.join(ret).strip(self._nullchar) + + def decode(self, ciphertext): + ret = [] + filled = list(self._cipher_padfill(ciphertext)) + for i, line in enumerate(self._cipher_textfill(filled)): + lineno = i + 1 + if self._is_textline(lineno): + ret.append(line) + return ''.join(ret).strip(self._nullchar) + + def create_pad(self, length, width=DEFAULT_PAD_WIDTH): + return '\n'.join(self.create_pad_lines(length, width=width)) + + def create_pad_lines(self, length, width=DEFAULT_PAD_WIDTH): + chars = self._create_chars_for_pad(length) + lines = _chunk_chars_into_lines(chars, self._nullchar, width=width) + for line in lines: + yield line + yield '' + yield '' + + def _create_chars_for_pad(self, length): + chars = [] + for char in range(length): + chars.append(self._as_alpha[_get_randint(self._cipher_ceil)]) + return ''.join(chars) + + def _ensure_pad_is_lines(self): + if isinstance(self.pad, basestring): + self.pad = self.pad.splitlines() + + def _text_padfill(self, text): + self._ensure_pad_is_lines() + padlines = [line.strip() for line in self.pad if line.strip()] + textlines = _chunk_chars_into_lines(text, self._nullchar, + _get_textwidth(padlines)) + for textline, padline in izip(textlines, padlines): + yield padline + yield textline + yield '' + + def _cipher_padfill(self, ciphertext): + self._ensure_pad_is_lines() + padlines = [line.strip() for line in self.pad if line.strip()] + cipherlines = _chunk_chars_into_lines(ciphertext, self._nullchar, + _get_textwidth(padlines)) + for cipherline, padline in izip(cipherlines, padlines): + yield padline + yield '' + yield cipherline + + def _text_cipherfill(self, padfilled_text_lines): + for i, line in enumerate(padfilled_text_lines): + lineno = i + 1 + if self._is_cipherline(lineno): + padline = padfilled_text_lines[i - abs(self._padline_offset)] + textline = padfilled_text_lines[i - abs(self._textline_offset)] + yield padline + yield textline + yield self._cipherline_from_padline_and_textline(padline, + textline) + + def _cipher_textfill(self, padfilled_cipher_lines): + for i, line in enumerate(padfilled_cipher_lines): + lineno = i + 1 + if self._is_cipherline(lineno): + padline = padfilled_cipher_lines[i - abs(self._padline_offset)] + textline = \ + padfilled_cipher_lines[i - abs(self._textline_offset)] + yield padline + yield self._textline_from_cipherline_and_padline(line, padline) + yield line + + def _cipherline_from_padline_and_textline(self, padline, textline): + ret = [] + for padchar, textchar in izip(padline, textline): + if textchar == self._nullchar: + ret.append(self._nullchar) + continue + charnum = self._as_nums[padchar] + self._as_nums[textchar] + idx = charnum if charnum <= self._cipher_ceil else \ + charnum % self._cipher_ceil + ret.append(self._as_alpha[idx]) + return ''.join(ret) + + def _textline_from_cipherline_and_padline(self, cipherline, padline): + ret = [] + for ciphercar, padchar in izip(cipherline, padline): + if ciphercar == self._nullchar: + ret.append(self._nullchar) + continue + charnum = self._as_nums[ciphercar] - self._as_nums[padchar] + idx = charnum if charnum <= self._cipher_ceil else \ + charnum % self._cipher_ceil + if idx < 0: + idx = self._cipher_ceil + idx + ret.append(self._as_alpha[idx]) + return ''.join(ret) + + def _is_padline(self, lineno): + return self._is_cipherline(lineno + abs(self._padline_offset)) + + def _is_textline(self, lineno): + return self._is_cipherline(lineno + abs(self._textline_offset)) + + def _is_cipherline(self, lineno): + return not lineno % self._pad_modulo + + +def _chunk_chars_into_lines(chars, nullchar, width=DEFAULT_PAD_WIDTH): + lines = [] + for chunk in _as_line_chunks(chars, nullchar, width=width): + lines.append(chunk) + return lines + + +def _as_line_chunks(chars, nullchar, width=DEFAULT_PAD_WIDTH): + chunk = [] + for char in chars.replace('\n', ''): + chunk.append(char) + if len(chunk) == width: + yield ''.join(chunk) + chunk = [] + if len(chunk) < width: + chunk += ([nullchar] * (width - len(chunk))) + yield ''.join(chunk) + + +def _get_textwidth(textlines): + return max([len(line) for line in textlines]) + + +def _get_randint(modulo, randomness_file='/dev/urandom', + fallback_to_fake=True): + if not _DEV_URANDOM.get('fp'): + _get_urandom_fp(randomness_file=randomness_file, + fallback_to_fake=fallback_to_fake) + ret = ord(_DEV_URANDOM['fp'].read(1)) % modulo + return ret if ret != 0 else _get_randint(modulo) + + +class _FakeDevUrandom(object): + + @staticmethod + def read(nchars): + import random + ret = [] + for i in range(0, nchars): + ret.append(chr(random.randint(0, 255))) + return ''.join(ret) + + +def _get_urandom_fp(randomness_file='/dev/urandom', fallback_to_fake=True): + try: + _DEV_URANDOM['fp'] = open(randomness_file) + except (OSError, IOError): + if fallback_to_fake: + _DEV_URANDOM['fp'] = _FakeDevUrandom() + else: + raise + + +_DEV_URANDOM = {} diff --git a/one-time-pad/pavement.py b/one-time-pad/pavement.py new file mode 100644 index 0000000..ca5722c --- /dev/null +++ b/one-time-pad/pavement.py @@ -0,0 +1,26 @@ +from paver.easy import * +from paver.setuputils import setup + + +README = path.getcwd()/'README' +SETUP_ARGS = Bunch( + name='OneTimePad', + version='0.1.0', + author='Dan Buch', + author_email='daniel.buch@gmail.com', + url='http://github.com/meatballhat/OneTimePad', + description='like so: http://en.wikipedia.org/wiki/One-time_pad', + long_description=README.bytes(), + py_modules=['onetimepad'], + license='MIT', + platforms=['any'], +) + + +setup(**SETUP_ARGS) + + +@task +@needs(['generate_setup', 'minilib', 'setuptools.command.sdist']) +def sdist(): + pass