From fc7f35452ada6c16962f47545648026e124d4644 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 16:52:28 -0500 Subject: [PATCH 01/19] init --- onetimepad.py | 0 setup.py | 0 test_onetimepad.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 onetimepad.py create mode 100644 setup.py create mode 100644 test_onetimepad.py diff --git a/onetimepad.py b/onetimepad.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..e69de29 diff --git a/test_onetimepad.py b/test_onetimepad.py new file mode 100644 index 0000000..e69de29 From a2ef4ba0b11a6632514fb29818a6c9f2621e72ec Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 17:10:32 -0500 Subject: [PATCH 02/19] first failing version :) --- .gitignore | 1 + onetimepad.py | 8 ++++++++ setup.py | 23 +++++++++++++++++++++++ test_onetimepad.py | 21 +++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/onetimepad.py b/onetimepad.py index e69de29..90d81bf 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -0,0 +1,8 @@ + + +def encode(msg, pad): + return msg + + +def decode(msg, pad): + return msg diff --git a/setup.py b/setup.py index e69de29..dc9b021 100644 --- a/setup.py +++ b/setup.py @@ -0,0 +1,23 @@ +import sys +from distutils.core import setup + + +SETUP_ARGS = dict( + name='OneTimePad', + version='0.1.0', + author='Dan Buch', + author_email='daniel.buch@gmail.com', + url='http://github.com/meatballhat/OneTimePad', + description='one-time pad cryptosystem described in "Cryptonomicon"', + long_description='', + py_modules=['onetimepad'], +) + + +def main(): + setup(**SETUP_ARGS) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/test_onetimepad.py b/test_onetimepad.py index e69de29..eccb25f 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -0,0 +1,21 @@ +import random + +import unittest +import onetimepad as OT + + +class TestOneTimePad(unittest.TestCase): + pad = 'THISISMYONETIMEPADANDITISNOTESPECIALLYBIG' \ + 'BUTITISBIGENOUGHFORMYUSESIFYOUDONTMINDTHX' + + def test_encode(self): + enc = OT.encode(IN_MSG, self.pad) + self.assertEqual(EXPECTED_ENCODED, enc) + + def test_decode(self): + dec = OT.decode(EXPECTED_ENCODED, self.pad) + self.assertEqual(IN_MSG, dec) + + +IN_MSG = 'TWOBYFOURBOARDSONEHUNDREDCOUNTLENGTHEIGHTFEET' +EXPECTED_ENCODED = 'huteshusoahutseohusoaeutoaehusoat' From dca18ca02297fffcc952f4c8076aa71b26878d93 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 20:22:41 -0500 Subject: [PATCH 03/19] implemented the creation of random one-time pads of specified length, defaulting to width of 72 --- onetimepad.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ test_onetimepad.py | 19 ++++++++--------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index 90d81bf..ca626b9 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -1,3 +1,4 @@ +import random def encode(msg, pad): @@ -6,3 +7,53 @@ def encode(msg, pad): def decode(msg, pad): return msg + + +def create_pad(length, width=72): + chars = _create_chars_for_pad(length) + lines = _chunk_chars_into_lines(chars, width=width) + return lines + + +def _create_chars_for_pad(length): + chars = [] + for char in range(length): + chars.append(_AS_ALPHA[random.randint(1, 25)]) + return ''.join(chars) + + +def _chunk_chars_into_lines(chars, width=72): + lines = [] + for chunk in _as_line_chunks(chars, width=width): + lines.append(chunk) + return '\n\n\n'.join(lines) + + +def _as_line_chunks(chars, width=72): + chunk = [] + for char in chars: + chunk.append(char) + if len(chunk) == width: + yield ''.join(chunk) + chunk = [] + if len(chunk) < width: + chunk += (['.'] * (width - len(chunk))) + yield ''.join(chunk) + + +def _mk_as_alpha(): + as_alpha = dict() + a_chr = ord('A') + past_j = False + + for char in range(26): + 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 + return as_alpha + + +_AS_ALPHA = _mk_as_alpha() diff --git a/test_onetimepad.py b/test_onetimepad.py index eccb25f..b068032 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -5,17 +5,14 @@ import onetimepad as OT class TestOneTimePad(unittest.TestCase): - pad = 'THISISMYONETIMEPADANDITISNOTESPECIALLYBIG' \ - 'BUTITISBIGENOUGHFORMYUSESIFYOUDONTMINDTHX' - def test_encode(self): - enc = OT.encode(IN_MSG, self.pad) - self.assertEqual(EXPECTED_ENCODED, enc) + def test_mk_as_alpha_excludes_j(self): + self.assertTrue('J' not in OT._AS_ALPHA.values()) - def test_decode(self): - dec = OT.decode(EXPECTED_ENCODED, self.pad) - self.assertEqual(IN_MSG, dec) + def test_mk_as_alpha_dict_has_25_members(self): + self.assertEqual(25, len(OT._AS_ALPHA.items())) - -IN_MSG = 'TWOBYFOURBOARDSONEHUNDREDCOUNTLENGTHEIGHTFEET' -EXPECTED_ENCODED = 'huteshusoahutseohusoaeutoaehusoat' + def test_creates_pad_of_desired_length(self): + for width in (72, 33, 99, 111): + pad = OT.create_pad(2000, width=width) + self.assertEqual(2000, len((''.join(pad.split())).strip('.'))) From 5023469466ad57cff85091c591a4a91b466c10b8 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 20:53:04 -0500 Subject: [PATCH 04/19] adding test to make sure every second and third line on a blank pad is empty --- test_onetimepad.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test_onetimepad.py b/test_onetimepad.py index b068032..dfc3079 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -15,4 +15,16 @@ class TestOneTimePad(unittest.TestCase): def test_creates_pad_of_desired_length(self): for width in (72, 33, 99, 111): pad = OT.create_pad(2000, width=width) - self.assertEqual(2000, len((''.join(pad.split())).strip('.'))) + lines = [line.strip('.') for line in pad.split('\n\n\n')] + self.assertEqual(2000, len(''.join(lines))) + + def test_two_out_of_every_three_lines_are_empty_on_new_pad(self): + pad = OT.create_pad(2000) + for i, line in enumerate(pad.splitlines()): + lineno = i + 3 + if not lineno % 3: + self.assertTrue(bool(len(line)), + 'line {0} is non-empty'.format(lineno)) + else: + self.assertFalse(bool(len(line)), + 'line {0} is empty'.format(lineno)) From 1b94d6a463a81b05daf3e9ec5fb01ab3e2c37708 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 20:54:37 -0500 Subject: [PATCH 05/19] adding __name__ == "__main__" magic to run test suite with just unittest --- test_onetimepad.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test_onetimepad.py b/test_onetimepad.py index dfc3079..fd27e97 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -28,3 +28,7 @@ class TestOneTimePad(unittest.TestCase): else: self.assertFalse(bool(len(line)), 'line {0} is empty'.format(lineno)) + + +if __name__ == '__main__': + unittest.main() From 998bb596ef49cad87a6fe9153956bf99df54a9bf Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Wed, 25 Nov 2009 23:12:55 -0500 Subject: [PATCH 06/19] starting to implement a pad filler + tests --- onetimepad.py | 31 +++++++++++++++++++++++++++++-- test_onetimepad.py | 38 ++++++++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index ca626b9..a535d41 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -12,7 +12,7 @@ def decode(msg, pad): def create_pad(length, width=72): chars = _create_chars_for_pad(length) lines = _chunk_chars_into_lines(chars, width=width) - return lines + return '\n\n\n'.join(lines) def _create_chars_for_pad(length): @@ -26,7 +26,7 @@ def _chunk_chars_into_lines(chars, width=72): lines = [] for chunk in _as_line_chunks(chars, width=width): lines.append(chunk) - return '\n\n\n'.join(lines) + return lines def _as_line_chunks(chars, width=72): @@ -41,6 +41,33 @@ def _as_line_chunks(chars, width=72): yield ''.join(chunk) +def _padfill(text, pad): + padlines = pad.splitlines() + textlines = _chunk_chars_into_lines(text, _get_textwidth(pad)) + for lineno, padline in enumerate(padlines[:]): + if _is_padline(lineno) or _is_cypherline(lineno): + continue + else: + padlines[lineno] = textlines.pop(0) if textlines[1:] else '' + return '\n'.join(padlines) + + +def _get_textwidth(text): + return max([len(line) for line in text.splitlines()]) + + +def _is_padline(lineno): + return not lineno % 3 + + +def _is_txtline(lineno): + return (lineno % 2 and not lineno % 3) + + +def _is_cypherline(lineno): + return lineno % 3 + + def _mk_as_alpha(): as_alpha = dict() a_chr = ord('A') diff --git a/test_onetimepad.py b/test_onetimepad.py index fd27e97..680b83a 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -1,10 +1,10 @@ -import random - import unittest import onetimepad as OT class TestOneTimePad(unittest.TestCase): + msg = ('HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFA' + 'WOODCHUCKCOULDCHUCKWOOD' * 50) def test_mk_as_alpha_excludes_j(self): self.assertTrue('J' not in OT._AS_ALPHA.values()) @@ -20,14 +20,40 @@ class TestOneTimePad(unittest.TestCase): def test_two_out_of_every_three_lines_are_empty_on_new_pad(self): pad = OT.create_pad(2000) - for i, line in enumerate(pad.splitlines()): - lineno = i + 3 - if not lineno % 3: + for lineno, line in enumerate(pad.splitlines()): + line = line.strip() + if OT._is_padline(lineno): self.assertTrue(bool(len(line)), - 'line {0} is non-empty'.format(lineno)) + 'pad line {0} is non-empty'.format(lineno)) + elif OT._is_txtline(lineno): + self.assertFalse(bool(len(line)), + 'text line {0} is empty'.format(lineno)) + elif OT._is_cypherline(lineno): + self.assertFalse(bool(len(line)), + 'cypher line {0} is empty'.format(lineno)) else: + raise NotImplementedError(lineno) + + def test_padfill_leaves_every_third_line_empty(self): + msg = self.msg[:] + pad = OT.create_pad(len(msg)) + filled = OT._padfill(msg, pad) + # print filled + # raise Exception + self.assertTrue(bool(filled)) + for lineno, line in enumerate(filled.splitlines()): + line = line.strip() + if OT._is_cypherline(lineno): self.assertFalse(bool(len(line)), 'line {0} is empty'.format(lineno)) + elif OT._is_txtline(lineno): + self.assertTrue(bool(len(line)), + 'text line {0} is non-empty'.format(lineno)) + elif OT._is_padline(lineno): + self.assertTrue(bool(len(line)), + 'pad line {0} is non-empty'.format(lineno)) + else: + raise NotImplementedError(lineno) if __name__ == '__main__': From 40d222c164cff13091a91098ad34c349f310cc6f Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 26 Nov 2009 08:30:33 -0500 Subject: [PATCH 07/19] returning a list from padfill to cut down on all the str.join ops --- onetimepad.py | 2 +- test_onetimepad.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index a535d41..00e96ea 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -49,7 +49,7 @@ def _padfill(text, pad): continue else: padlines[lineno] = textlines.pop(0) if textlines[1:] else '' - return '\n'.join(padlines) + return padlines def _get_textwidth(text): diff --git a/test_onetimepad.py b/test_onetimepad.py index 680b83a..55eb536 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -38,10 +38,8 @@ class TestOneTimePad(unittest.TestCase): msg = self.msg[:] pad = OT.create_pad(len(msg)) filled = OT._padfill(msg, pad) - # print filled - # raise Exception self.assertTrue(bool(filled)) - for lineno, line in enumerate(filled.splitlines()): + for lineno, line in enumerate(filled): line = line.strip() if OT._is_cypherline(lineno): self.assertFalse(bool(len(line)), From 8cf3df0e3e360f46b872a3c2a671aed5d49279e5 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 26 Nov 2009 08:42:57 -0500 Subject: [PATCH 08/19] s/cypher/cipher/g ... (sigh) I must have been sleepy :P --- onetimepad.py | 6 +++--- test_onetimepad.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index 00e96ea..f027170 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -45,7 +45,7 @@ def _padfill(text, pad): padlines = pad.splitlines() textlines = _chunk_chars_into_lines(text, _get_textwidth(pad)) for lineno, padline in enumerate(padlines[:]): - if _is_padline(lineno) or _is_cypherline(lineno): + if _is_padline(lineno) or _is_cipherline(lineno): continue else: padlines[lineno] = textlines.pop(0) if textlines[1:] else '' @@ -64,8 +64,8 @@ def _is_txtline(lineno): return (lineno % 2 and not lineno % 3) -def _is_cypherline(lineno): - return lineno % 3 +def _is_cipherline(lineno): + return lineno % PAD_MODULO def _mk_as_alpha(): diff --git a/test_onetimepad.py b/test_onetimepad.py index 55eb536..ca8480d 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -28,9 +28,9 @@ class TestOneTimePad(unittest.TestCase): elif OT._is_txtline(lineno): self.assertFalse(bool(len(line)), 'text line {0} is empty'.format(lineno)) - elif OT._is_cypherline(lineno): + elif OT._is_cipherline(lineno): self.assertFalse(bool(len(line)), - 'cypher line {0} is empty'.format(lineno)) + 'cipher line {0} is empty'.format(lineno)) else: raise NotImplementedError(lineno) @@ -41,7 +41,7 @@ class TestOneTimePad(unittest.TestCase): self.assertTrue(bool(filled)) for lineno, line in enumerate(filled): line = line.strip() - if OT._is_cypherline(lineno): + if OT._is_cipherline(lineno): self.assertFalse(bool(len(line)), 'line {0} is empty'.format(lineno)) elif OT._is_txtline(lineno): From ca11fa5f32b87dc515e494624e4f5b7b47a3a7f9 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Thu, 26 Nov 2009 08:44:05 -0500 Subject: [PATCH 09/19] replacing magic numbers with consts, opting for list-based rather than string-based stuff in most places --- onetimepad.py | 28 ++++++++++++++++++++-------- test_onetimepad.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index f027170..ab0e120 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -1,5 +1,10 @@ import random +PAD_MODULO = 3 +DEFAULT_PAD_WIDTH = 72 +CIPHER_FLOOR = 1 +CIPHER_CEIL = 25 + def encode(msg, pad): return msg @@ -9,27 +14,34 @@ def decode(msg, pad): return msg -def create_pad(length, width=72): +def create_pad(length, width=DEFAULT_PAD_WIDTH): + return '\n'.join(create_pad_lines(length, width=width)) + + +def create_pad_lines(length, width=DEFAULT_PAD_WIDTH): chars = _create_chars_for_pad(length) lines = _chunk_chars_into_lines(chars, width=width) - return '\n\n\n'.join(lines) + for line in lines: + yield line + yield '' + yield '' def _create_chars_for_pad(length): chars = [] for char in range(length): - chars.append(_AS_ALPHA[random.randint(1, 25)]) + chars.append(_AS_ALPHA[random.randint(CIPHER_FLOOR, CIPHER_CEIL)]) return ''.join(chars) -def _chunk_chars_into_lines(chars, width=72): +def _chunk_chars_into_lines(chars, width=DEFAULT_PAD_WIDTH): lines = [] for chunk in _as_line_chunks(chars, width=width): lines.append(chunk) return lines -def _as_line_chunks(chars, width=72): +def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): chunk = [] for char in chars: chunk.append(char) @@ -57,11 +69,11 @@ def _get_textwidth(text): def _is_padline(lineno): - return not lineno % 3 + return not lineno % PAD_MODULO def _is_txtline(lineno): - return (lineno % 2 and not lineno % 3) + return (lineno % 2 and not lineno % PAD_MODULO) def _is_cipherline(lineno): @@ -73,7 +85,7 @@ def _mk_as_alpha(): a_chr = ord('A') past_j = False - for char in range(26): + for char in range(CIPHER_CEIL + 1): letter = chr(a_chr + char) if letter != 'J': key = char + (1 if not past_j else 0) diff --git a/test_onetimepad.py b/test_onetimepad.py index ca8480d..a1f4d8f 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -5,6 +5,7 @@ import onetimepad as OT class TestOneTimePad(unittest.TestCase): msg = ('HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFA' 'WOODCHUCKCOULDCHUCKWOOD' * 50) + _padsize = 2000 def test_mk_as_alpha_excludes_j(self): self.assertTrue('J' not in OT._AS_ALPHA.values()) @@ -14,13 +15,17 @@ class TestOneTimePad(unittest.TestCase): def test_creates_pad_of_desired_length(self): for width in (72, 33, 99, 111): - pad = OT.create_pad(2000, width=width) - lines = [line.strip('.') for line in pad.split('\n\n\n')] - self.assertEqual(2000, len(''.join(lines))) + pad = OT.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_two_out_of_every_three_lines_are_empty_on_new_pad(self): - pad = OT.create_pad(2000) - for lineno, line in enumerate(pad.splitlines()): + pad = OT.create_pad_lines(2000) + for lineno, line in enumerate(pad): line = line.strip() if OT._is_padline(lineno): self.assertTrue(bool(len(line)), From 76437746a2e9d992d10ef909619df3af2665d3b6 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 13:53:24 -0500 Subject: [PATCH 10/19] fixed determination of pad/text/cipher lines by number --- onetimepad.py | 65 +++++++++++++++++++++++++++++++----------- test_onetimepad.py | 71 ++++++++++++++++++++++++---------------------- 2 files changed, 85 insertions(+), 51 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index ab0e120..62dfc0e 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -1,6 +1,9 @@ import random +from itertools import izip PAD_MODULO = 3 +PADLINE_OFFSET = -2 +TEXTLINE_OFFSET = -1 DEFAULT_PAD_WIDTH = 72 CIPHER_FLOOR = 1 CIPHER_CEIL = 25 @@ -53,35 +56,59 @@ def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): yield ''.join(chunk) -def _padfill(text, pad): - padlines = pad.splitlines() - textlines = _chunk_chars_into_lines(text, _get_textwidth(pad)) - for lineno, padline in enumerate(padlines[:]): - if _is_padline(lineno) or _is_cipherline(lineno): - continue +def _padfill(text, padlines): + padlines = [line.strip() for line in padlines if line.strip()] + textlines = _chunk_chars_into_lines(text, _get_textwidth(padlines)) + for textline, padline in izip(textlines, padlines): + yield padline + yield textline + yield '' + + +def _cipherfill(padfilled_lines): + padline = textline = None + for i, line in enumerate(padfilled_lines): + if _is_padline(i): + padline = line + elif _is_textline(i): + textline = line else: - padlines[lineno] = textlines.pop(0) if textlines[1:] else '' - return padlines + cipherline = \ + _get_cipherline_from_padline_and_textline(padline, textline) + padfilled_lines[i] = cipherline + + return padfilled_lines + + +def _get_cipherline_from_padline_and_textline(padline, textline): + ret = [] + for padchar, textchar in izip(padline, textline): + idx = (_AS_NUMS[padchar] + _AS_NUMS[textchar]) % CIPHER_CEIL + ret.append(_AS_ALPHA[idx]) + return ''.join(ret) def _get_textwidth(text): - return max([len(line) for line in text.splitlines()]) + if isinstance(text, basestring): + text = text.splitlines() + return max([len(line) for line in text]) def _is_padline(lineno): - return not lineno % PAD_MODULO + return _is_cipherline(lineno + abs(PADLINE_OFFSET)) -def _is_txtline(lineno): - return (lineno % 2 and not lineno % PAD_MODULO) +def _is_textline(lineno): + return _is_cipherline(lineno + abs(TEXTLINE_OFFSET)) def _is_cipherline(lineno): - return lineno % PAD_MODULO + return not lineno % PAD_MODULO -def _mk_as_alpha(): - as_alpha = dict() +def _mk_as_alpha_as_nums(): + as_alpha = {} + as_nums = {} a_chr = ord('A') past_j = False @@ -92,7 +119,11 @@ def _mk_as_alpha(): as_alpha[key] = letter else: past_j = True - return as_alpha + + for key, val in as_alpha.iteritems(): + as_nums[val] = key + + return as_alpha, as_nums -_AS_ALPHA = _mk_as_alpha() +_AS_ALPHA, _AS_NUMS = _mk_as_alpha_as_nums() diff --git a/test_onetimepad.py b/test_onetimepad.py index a1f4d8f..b5b6faf 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -23,40 +23,43 @@ class TestOneTimePad(unittest.TestCase): '{1}, actual={2}'.format(self._padsize, width, actual)) - def test_two_out_of_every_three_lines_are_empty_on_new_pad(self): - pad = OT.create_pad_lines(2000) - for lineno, line in enumerate(pad): - line = line.strip() - if OT._is_padline(lineno): - self.assertTrue(bool(len(line)), - 'pad line {0} is non-empty'.format(lineno)) - elif OT._is_txtline(lineno): - self.assertFalse(bool(len(line)), - 'text line {0} is empty'.format(lineno)) - elif OT._is_cipherline(lineno): - self.assertFalse(bool(len(line)), - 'cipher line {0} is empty'.format(lineno)) - else: - raise NotImplementedError(lineno) - - def test_padfill_leaves_every_third_line_empty(self): - msg = self.msg[:] - pad = OT.create_pad(len(msg)) - filled = OT._padfill(msg, pad) - self.assertTrue(bool(filled)) - for lineno, line in enumerate(filled): - line = line.strip() - if OT._is_cipherline(lineno): - self.assertFalse(bool(len(line)), - 'line {0} is empty'.format(lineno)) - elif OT._is_txtline(lineno): - self.assertTrue(bool(len(line)), - 'text line {0} is non-empty'.format(lineno)) - elif OT._is_padline(lineno): - self.assertTrue(bool(len(line)), - 'pad line {0} is non-empty'.format(lineno)) - else: - raise NotImplementedError(lineno) + def test_is_padline(self): + for lineno in PADLINES: + self.assertTrue(OT._is_padline(lineno), + 'line {0} is padline'.format(lineno)) + for lineno in TEXTLINES: + self.assertFalse(OT._is_padline(lineno), + 'line {0} is not padline'.format(lineno)) + for lineno in CIPHERLINES: + self.assertFalse(OT._is_padline(lineno), + 'line {0} is not padline'.format(lineno)) + + def test_is_textline(self): + for lineno in TEXTLINES: + self.assertTrue(OT._is_textline(lineno), + 'line {0} is textline'.format(lineno)) + for lineno in PADLINES: + self.assertFalse(OT._is_textline(lineno), + 'line {0} is not textline'.format(lineno)) + for lineno in CIPHERLINES: + self.assertFalse(OT._is_textline(lineno), + 'line {0} is not textline'.format(lineno)) + + def test_is_cipherline(self): + for lineno in CIPHERLINES: + self.assertTrue(OT._is_cipherline(lineno), + 'line {0} is cipherline'.format(lineno)) + for lineno in PADLINES: + self.assertFalse(OT._is_cipherline(lineno), + 'line {0} is not cipherline'.format(lineno)) + for lineno in TEXTLINES: + self.assertFalse(OT._is_cipherline(lineno), + 'line {0} is not cipherline'.format(lineno)) + + +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) if __name__ == '__main__': From 91b711bea6bc35b5a8fb5f45c45c4a9e04ede128 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 14:40:54 -0500 Subject: [PATCH 11/19] filling in tests for creation of cipherlines from pad+text and text from cipher-pad --- onetimepad.py | 43 +++++++++++++++++++++++++++++-------------- test_onetimepad.py | 13 +++++++++++++ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index 62dfc0e..50cbe9b 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -7,6 +7,7 @@ TEXTLINE_OFFSET = -1 DEFAULT_PAD_WIDTH = 72 CIPHER_FLOOR = 1 CIPHER_CEIL = 25 +NULLCHAR = '.' def encode(msg, pad): @@ -52,7 +53,7 @@ def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): yield ''.join(chunk) chunk = [] if len(chunk) < width: - chunk += (['.'] * (width - len(chunk))) + chunk += ([NULLCHAR] * (width - len(chunk))) yield ''.join(chunk) @@ -66,24 +67,38 @@ def _padfill(text, padlines): def _cipherfill(padfilled_lines): - padline = textline = None for i, line in enumerate(padfilled_lines): - if _is_padline(i): - padline = line - elif _is_textline(i): - textline = line - else: - cipherline = \ - _get_cipherline_from_padline_and_textline(padline, textline) - padfilled_lines[i] = cipherline - - return padfilled_lines + lineno = i + 1 + if _is_cipherline(lineno): + padline = padfilled_lines[i - abs(PADLINE_OFFSET)] + textline = padfilled_lines[i - abs(TEXTLINE_OFFSET)] + yield padline + yield textline + yield _cipherline_from_padline_and_textline(padline, textline) -def _get_cipherline_from_padline_and_textline(padline, textline): +def _cipherline_from_padline_and_textline(padline, textline): ret = [] for padchar, textchar in izip(padline, textline): - idx = (_AS_NUMS[padchar] + _AS_NUMS[textchar]) % CIPHER_CEIL + if textchar == NULLCHAR: + ret.append(NULLCHAR) + continue + charnum = _AS_NUMS[padchar] + _AS_NUMS[textchar] + idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL + ret.append(_AS_ALPHA[idx]) + return ''.join(ret) + + +def _textline_from_cipherline_and_padline(cipherline, padline): + ret = [] + for ciphercar, padchar in izip(cipherline, padline): + if ciphercar == NULLCHAR: + ret.append(NULLCHAR) + continue + charnum = _AS_NUMS[ciphercar] - _AS_NUMS[padchar] + idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL + if idx < 0: + idx = CIPHER_CEIL + idx ret.append(_AS_ALPHA[idx]) return ''.join(ret) diff --git a/test_onetimepad.py b/test_onetimepad.py index b5b6faf..b6c78ca 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -56,11 +56,24 @@ class TestOneTimePad(unittest.TestCase): self.assertFalse(OT._is_cipherline(lineno), 'line {0} is not cipherline'.format(lineno)) + def test_make_cipherline_from_padline_and_textline(self): + actual = OT._cipherline_from_padline_and_textline(TEST_PADLINE, + TEST_TEXTLINE) + self.assertEqual(TEST_CIPHERLINE, actual) + + def test_make_textline_from_cipherline_and_padline(self): + actual = OT._textline_from_cipherline_and_padline(TEST_CIPHERLINE, + TEST_PADLINE) + self.assertEqual(TEST_TEXTLINE, actual) + 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' if __name__ == '__main__': unittest.main() From 5fbb4bfd72f29c6ed185f66925c4e8eaa70e87a4 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 15:33:02 -0500 Subject: [PATCH 12/19] finished implementing encode and decode funcs --- onetimepad.py | 54 +++++++++++++++++++++----- test_onetimepad.py | 96 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 9 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index 50cbe9b..5842e93 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -11,11 +11,23 @@ NULLCHAR = '.' def encode(msg, pad): - return msg + ret = [] + filled = list(_text_padfill(msg, pad)) + for i, line in enumerate(_text_cipherfill(filled)): + lineno = i + 1 + if _is_cipherline(lineno): + ret.append(line) + return ''.join(ret).strip(NULLCHAR) -def decode(msg, pad): - return msg +def decode(ciphertext, pad): + ret = [] + filled = list(_cipher_padfill(ciphertext, pad)) + for i, line in enumerate(_cipher_textfill(filled)): + lineno = i + 1 + if _is_textline(lineno): + ret.append(line) + return ''.join(ret).strip(NULLCHAR) def create_pad(length, width=DEFAULT_PAD_WIDTH): @@ -47,7 +59,7 @@ def _chunk_chars_into_lines(chars, width=DEFAULT_PAD_WIDTH): def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): chunk = [] - for char in chars: + for char in chars.replace('\n', ''): chunk.append(char) if len(chunk) == width: yield ''.join(chunk) @@ -57,7 +69,9 @@ def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): yield ''.join(chunk) -def _padfill(text, padlines): +def _text_padfill(text, padlines): + if isinstance(padlines, basestring): + padlines = padlines.splitlines() padlines = [line.strip() for line in padlines if line.strip()] textlines = _chunk_chars_into_lines(text, _get_textwidth(padlines)) for textline, padline in izip(textlines, padlines): @@ -66,17 +80,39 @@ def _padfill(text, padlines): yield '' -def _cipherfill(padfilled_lines): - for i, line in enumerate(padfilled_lines): +def _cipher_padfill(ciphertext, padlines): + if isinstance(padlines, basestring): + padlines = padlines.splitlines() + padlines = [line.strip() for line in padlines if line.strip()] + cipherlines = _chunk_chars_into_lines(ciphertext, _get_textwidth(padlines)) + for cipherline, padline in izip(cipherlines, padlines): + yield padline + yield '' + yield cipherline + + +def _text_cipherfill(padfilled_text_lines): + for i, line in enumerate(padfilled_text_lines): lineno = i + 1 if _is_cipherline(lineno): - padline = padfilled_lines[i - abs(PADLINE_OFFSET)] - textline = padfilled_lines[i - abs(TEXTLINE_OFFSET)] + padline = padfilled_text_lines[i - abs(PADLINE_OFFSET)] + textline = padfilled_text_lines[i - abs(TEXTLINE_OFFSET)] yield padline yield textline yield _cipherline_from_padline_and_textline(padline, textline) +def _cipher_textfill(padfilled_cipher_lines): + for i, line in enumerate(padfilled_cipher_lines): + lineno = i + 1 + if _is_cipherline(lineno): + padline = padfilled_cipher_lines[i - abs(PADLINE_OFFSET)] + textline = padfilled_cipher_lines[i - abs(TEXTLINE_OFFSET)] + yield padline + yield _textline_from_cipherline_and_padline(line, padline) + yield line + + def _cipherline_from_padline_and_textline(padline, textline): ret = [] for padchar, textchar in izip(padline, textline): diff --git a/test_onetimepad.py b/test_onetimepad.py index b6c78ca..f88e0a4 100644 --- a/test_onetimepad.py +++ b/test_onetimepad.py @@ -66,6 +66,14 @@ class TestOneTimePad(unittest.TestCase): TEST_PADLINE) self.assertEqual(TEST_TEXTLINE, actual) + def test_encode_equals_expected(self): + ciphertext = OT.encode(TEST_MSG, TEST_PAD) + self.assertEqual(TEST_CIPHER, ciphertext) + + def test_decode_equals_expected(self): + text = OT.decode(TEST_CIPHER, TEST_PAD) + self.assertEqual(TEST_MSG, text) + PADLINES = (1, 4, 7, 10, 13, 16, 19, 22, 25) TEXTLINES = (2, 5, 8, 11, 14, 17, 20, 23, 26) @@ -75,5 +83,93 @@ 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........................................................ + +""" + + if __name__ == '__main__': unittest.main() From feb7a29b19ba00f181cf604ed61c6c16688875f0 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 23:12:19 -0500 Subject: [PATCH 13/19] removing unused code branch, brought test coverage up to 100% :) --- onetimepad.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/onetimepad.py b/onetimepad.py index 5842e93..8e392b6 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -139,10 +139,8 @@ def _textline_from_cipherline_and_padline(cipherline, padline): return ''.join(ret) -def _get_textwidth(text): - if isinstance(text, basestring): - text = text.splitlines() - return max([len(line) for line in text]) +def _get_textwidth(textlines): + return max([len(line) for line in textlines]) def _is_padline(lineno): From 60cb6b67d431e5005539b6a098ddb44edab936c0 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 23:12:33 -0500 Subject: [PATCH 14/19] ignoring .coverage file(s) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0d20b64..004a8d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ *.pyc +.coverage From a7a732e1f9a98d2a46b26679160e734350006a64 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 23:23:45 -0500 Subject: [PATCH 15/19] referencing wikipedia entry in setup info --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index dc9b021..d2e947a 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ SETUP_ARGS = dict( author='Dan Buch', author_email='daniel.buch@gmail.com', url='http://github.com/meatballhat/OneTimePad', - description='one-time pad cryptosystem described in "Cryptonomicon"', + description='like so: http://en.wikipedia.org/wiki/One-time_pad', long_description='', py_modules=['onetimepad'], ) From e0d5e608f9870cbf5de844f0a13fbb4c8f801ce6 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Fri, 27 Nov 2009 23:52:06 -0500 Subject: [PATCH 16/19] changed test to itest because we're opening files (well, devices ...) using /dev/urandom if available else stdlib random, testing failure scenarios --- test_onetimepad.py => itest_onetimepad.py | 11 ++++++++ onetimepad.py | 31 +++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) rename test_onetimepad.py => itest_onetimepad.py (93%) diff --git a/test_onetimepad.py b/itest_onetimepad.py similarity index 93% rename from test_onetimepad.py rename to itest_onetimepad.py index f88e0a4..0c45e53 100644 --- a/test_onetimepad.py +++ b/itest_onetimepad.py @@ -23,6 +23,17 @@ class TestOneTimePad(unittest.TestCase): '{1}, actual={2}'.format(self._padsize, width, actual)) + def test_get_randint_on_nonexistent_randomness_file_fails_ioerror(self): + OT._DEV_RANDOM['fp'] = None + self.assertRaises(IOError, OT._get_randint, + randomness_file='/foo/bar/busted/borken', + fallback_to_fake=False) + + def test_get_randint_on_nonexistent_randomness_file_uses_fake(self): + OT._DEV_RANDOM['fp'] = None + random_int = OT._get_randint(randomness_file='/broke/as/joke') + self.assertTrue(random_int in range(0, 255)) + def test_is_padline(self): for lineno in PADLINES: self.assertTrue(OT._is_padline(lineno), diff --git a/onetimepad.py b/onetimepad.py index 8e392b6..92febdf 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -1,4 +1,3 @@ -import random from itertools import izip PAD_MODULO = 3 @@ -46,7 +45,7 @@ def create_pad_lines(length, width=DEFAULT_PAD_WIDTH): def _create_chars_for_pad(length): chars = [] for char in range(length): - chars.append(_AS_ALPHA[random.randint(CIPHER_FLOOR, CIPHER_CEIL)]) + chars.append(_AS_ALPHA[_get_randint()]) return ''.join(chars) @@ -155,6 +154,34 @@ def _is_cipherline(lineno): return not lineno % PAD_MODULO +def _get_randint(randomness_file='/dev/urandom', fallback_to_fake=True): + if not _DEV_RANDOM.get('fp'): + _get_urandom_fp(randomness_file=randomness_file, + fallback_to_fake=fallback_to_fake) + ret = ord(_DEV_RANDOM['fp'].read(1)) % CIPHER_CEIL + return ret if ret != 0 else _get_randint() + + +class _FakeDevUrandom(object): + + def read(self, nchars): + import random + return ''.join([chr(random.randint(0, 255)) for i in range(0, nchars)]) + + +def _get_urandom_fp(randomness_file='/dev/urandom', fallback_to_fake=True): + try: + _DEV_RANDOM['fp'] = open(randomness_file) + except (OSError, IOError): + if fallback_to_fake: + _DEV_RANDOM['fp'] = _FakeDevUrandom() + else: + raise + + +_DEV_RANDOM = {} + + def _mk_as_alpha_as_nums(): as_alpha = {} as_nums = {} From b932174e34f2d44a348cc40f651b5933f77bed69 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 28 Nov 2009 14:11:40 -0500 Subject: [PATCH 17/19] beginning process of "making it right", enclosing mod 25 one-time pad stuff in its own class --- itest_onetimepad.py | 261 ++++++++++++++++----------------- onetimepad.py | 343 +++++++++++++++++++++++--------------------- 2 files changed, 312 insertions(+), 292 deletions(-) diff --git a/itest_onetimepad.py b/itest_onetimepad.py index 0c45e53..45a6180 100644 --- a/itest_onetimepad.py +++ b/itest_onetimepad.py @@ -2,184 +2,189 @@ import unittest import onetimepad as OT -class TestOneTimePad(unittest.TestCase): +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) - def test_mk_as_alpha_excludes_j(self): - self.assertTrue('J' not in OT._AS_ALPHA.values()) + TEST_PADLINE = 'HUCHUGGHOBUXADDHOHPGQBKXQKAOLRL' + TEST_TEXTLINE = 'THISCAKEISUNSATISFACTORYTOMEYES' + TEST_CIPHERLINE = 'BCMAXHRNXUPLTEXRGOQKKQBWKYNTKWD' - def test_mk_as_alpha_dict_has_25_members(self): - self.assertEqual(25, len(OT._AS_ALPHA.items())) + TEST_MSG = \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOODCHUCKCOULDCHUCKWOOD' \ + 'HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFAWOO' - def test_creates_pad_of_desired_length(self): - for width in (72, 33, 99, 111): - pad = OT.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)) + TEST_CIPHER = \ + 'FMHZRGHRITAMHVAOLBANWMNYPBYDCFCAVXAEEKXUTYXLUCYQVYPWZFSKYI' \ + 'LWKCHZSXLFCMZIEFHXKVUXVNBIQVSNYPTFVIZHSVTDCKWUSZHEHUPLZSUY' \ + 'BTPSMEISGTYDWCXVUCPGHHKXDOUHGCVDTDUQVKFDRDCHFIKTNFGDFODCQS' \ + 'RIWGZBLPTCSXLMPQODYLHQNGWNSCTHNDOZEUXWCEQAZMRGZIQUTUYSKOGW' \ + 'YGYPEDNUZDWBEPUHLWWBHLGADYTCNGEUFKPLQSAMMXCTXWQEOYBNKQAHBV' \ + 'PDKTMQSHDFCFBCVKXVTRSXRTBHAGKNLFASNVNENHHRNGCKXALMIBNZKOTL' \ + 'BZMWGSNMTLDNPXKVQZZUAZCSPWFNRKAMTNMCPHWGLGBVSNFDXSKVUFBZBW' \ + 'WICBUPCWZDUXCZTXHKQBMQNTBHAAGMBPKUIDTHITUEWEPPMACPXERXRYND' \ + 'RWQSBQQMSQRVWZMAQQKUGHLELKFLKHGNFFDHIWUZPDIMIOELSIHMUIZQCW' \ + 'KLPZQIFAXNDRRIIIHOYBGSCIYBIVSRTECEOZSBLYTNCXGQXEWMGIXHKBIL' \ + 'TOKWHGABPMXNAHEWFKIMOQPHVHSYZABWLMLSSDEHQIBNEMEMHCWOUBIIZS' \ + 'XPFWXMQNOTEANYGQBVMZAXNPAMWSUOGFAFFYUTKDZYHFTPYGOMDHESKOZI' \ + 'IPDXAMSAQOTGAQKYPYWWUYGHSBVODAHDSKDVPMLWRCXQXGAXQZBFNKXFKU' \ + 'WUDRWNRKECIQIUEEMQNCAZDDDOQOEGUVFVCTKLTISNOQILEKUYXCYZNHHZ' \ + 'OFHCSLOPPQAFLHHDZANGTXNQSDZSAFLLSGWULVHIDERMTZZHTDHTZPHEFN' \ + 'ORDEIYOVTTAWVTOOXSLZBOPMRCCKSMHQOVFERCOVUMWXFSNEMFALNWGRQC' \ + 'NLWELLXNCHZGXQEUGEVNCWMHRNGUTBKTVEXGASBRCEBSFITOEFSNIBTLME' \ + 'MERMOFVBNTFCTTWEGAIGRZQBBFOKOPXNCWXFBK' - def test_get_randint_on_nonexistent_randomness_file_fails_ioerror(self): - OT._DEV_RANDOM['fp'] = None - self.assertRaises(IOError, OT._get_randint, - randomness_file='/foo/bar/busted/borken', - fallback_to_fake=False) + TEST_PAD = """\ + XXLNWDZOAYXBEGFCGADYHHKQUYOAULZQMRZHQVTRLDUARODERVGBWVVVKECHNQNWKUCLZBWU - def test_get_randint_on_nonexistent_randomness_file_uses_fake(self): - OT._DEV_RANDOM['fp'] = None - random_int = OT._get_randint(randomness_file='/broke/as/joke') - self.assertTrue(random_int in range(0, 255)) - def test_is_padline(self): - for lineno in PADLINES: - self.assertTrue(OT._is_padline(lineno), - 'line {0} is padline'.format(lineno)) - for lineno in TEXTLINES: - self.assertFalse(OT._is_padline(lineno), - 'line {0} is not padline'.format(lineno)) - for lineno in CIPHERLINES: - self.assertFalse(OT._is_padline(lineno), - 'line {0} is not padline'.format(lineno)) + KUDWNGFTSEGFFSKSVEKZUMLTOSLIZZTFXODBZZMACDFUTESFRBAPYYVTTOCKQBSSTDGPILKE - def test_is_textline(self): - for lineno in TEXTLINES: - self.assertTrue(OT._is_textline(lineno), - 'line {0} is textline'.format(lineno)) - for lineno in PADLINES: - self.assertFalse(OT._is_textline(lineno), - 'line {0} is not textline'.format(lineno)) - for lineno in CIPHERLINES: - self.assertFalse(OT._is_textline(lineno), - 'line {0} is not textline'.format(lineno)) - def test_is_cipherline(self): - for lineno in CIPHERLINES: - self.assertTrue(OT._is_cipherline(lineno), - 'line {0} is cipherline'.format(lineno)) - for lineno in PADLINES: - self.assertFalse(OT._is_cipherline(lineno), - 'line {0} is not cipherline'.format(lineno)) - for lineno in TEXTLINES: - self.assertFalse(OT._is_cipherline(lineno), - 'line {0} is not cipherline'.format(lineno)) + YHSTKXTTGVBAIIZXCUPHICYICDGOBOIUZUEYCMLHPNHXUEKCBWTMKYBKHZLNKTETDXIHYBHF - def test_make_cipherline_from_padline_and_textline(self): - actual = OT._cipherline_from_padline_and_textline(TEST_PADLINE, - TEST_TEXTLINE) - self.assertEqual(TEST_CIPHERLINE, actual) - def test_make_textline_from_cipherline_and_padline(self): - actual = OT._textline_from_cipherline_and_padline(TEST_CIPHERLINE, - TEST_PADLINE) - self.assertEqual(TEST_TEXTLINE, actual) + WBOSEXMRLZVHNZSSQSBCKAERRITRBAZWGVZNTGDSIVIZEMBKWDOOBDWIDCZIUHVTKVTSGFDT - def test_encode_equals_expected(self): - ciphertext = OT.encode(TEST_MSG, TEST_PAD) - self.assertEqual(TEST_CIPHER, ciphertext) - def test_decode_equals_expected(self): - text = OT.decode(TEST_CIPHER, TEST_PAD) - self.assertEqual(TEST_MSG, text) + NRGPNGRNKEVLZVYOAYTUWCDTOLGEQDBSHVRMMYYQIEZWKWZVCPGIAGKPNZEGTLPKMPEILQAC -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) + MIPKMYCFMVZKUTVKIPXBKGLFATSDCMYLPYLSTPBARVELNSOUFPZMUTRIRNZLYMDITNXMKLGE -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' + QXYRYEAOHGETEQMKTUMARPYMPKONUKYZIHTFGNHIKVOLTLRPMPNFSDHWQGVHBNDCWZCLUHQW -TEST_CIPHER = \ - 'FMHZRGHRITAMHVAOLBANWMNYPBYDCFCAVXAEEKXUTYXLUCYQVYPWZFSKYI' \ - 'LWKCHZSXLFCMZIEFHXKVUXVNBIQVSNYPTFVIZHSVTDCKWUSZHEHUPLZSUY' \ - 'BTPSMEISGTYDWCXVUCPGHHKXDOUHGCVDTDUQVKFDRDCHFIKTNFGDFODCQS' \ - 'RIWGZBLPTCSXLMPQODYLHQNGWNSCTHNDOZEUXWCEQAZMRGZIQUTUYSKOGW' \ - 'YGYPEDNUZDWBEPUHLWWBHLGADYTCNGEUFKPLQSAMMXCTXWQEOYBNKQAHBV' \ - 'PDKTMQSHDFCFBCVKXVTRSXRTBHAGKNLFASNVNENHHRNGCKXALMIBNZKOTL' \ - 'BZMWGSNMTLDNPXKVQZZUAZCSPWFNRKAMTNMCPHWGLGBVSNFDXSKVUFBZBW' \ - 'WICBUPCWZDUXCZTXHKQBMQNTBHAAGMBPKUIDTHITUEWEPPMACPXERXRYND' \ - 'RWQSBQQMSQRVWZMAQQKUGHLELKFLKHGNFFDHIWUZPDIMIOELSIHMUIZQCW' \ - 'KLPZQIFAXNDRRIIIHOYBGSCIYBIVSRTECEOZSBLYTNCXGQXEWMGIXHKBIL' \ - 'TOKWHGABPMXNAHEWFKIMOQPHVHSYZABWLMLSSDEHQIBNEMEMHCWOUBIIZS' \ - 'XPFWXMQNOTEANYGQBVMZAXNPAMWSUOGFAFFYUTKDZYHFTPYGOMDHESKOZI' \ - 'IPDXAMSAQOTGAQKYPYWWUYGHSBVODAHDSKDVPMLWRCXQXGAXQZBFNKXFKU' \ - 'WUDRWNRKECIQIUEEMQNCAZDDDOQOEGUVFVCTKLTISNOQILEKUYXCYZNHHZ' \ - 'OFHCSLOPPQAFLHHDZANGTXNQSDZSAFLLSGWULVHIDERMTZZHTDHTZPHEFN' \ - 'ORDEIYOVTTAWVTOOXSLZBOPMRCCKSMHQOVFERCOVUMWXFSNEMFALNWGRQC' \ - 'NLWELLXNCHZGXQEUGEVNCWMHRNGUTBKTVEXGASBRCEBSFITOEFSNIBTLME' \ - 'MERMOFVBNTFCTTWEGAIGRZQBBFOKOPXNCWXFBK' -TEST_PAD = """\ -XXLNWDZOAYXBEGFCGADYHHKQUYOAULZQMRZHQVTRLDUARODERVGBWVVVKECHNQNWKUCLZBWU + GIFBFZKZOFZRRYCBOSBWSNVFXXPSAGOUOXDNBNSOZADYYSKWQUTYNCDNGVLSZNDBCTSIYOUX -KUDWNGFTSEGFFSKSVEKZUMLTOSLIZZTFXODBZZMACDFUTESFRBAPYYVTTOCKQBSSTDGPILKE + NNUGLZNKNDSYGRUCXTKLBIMXZMMZAEHVRFYMBFKVDPAEHOYCBXKADZOTRRMULOPAIKCIHKFY -YHSTKXTTGVBAIIZXCUPHICYICDGOBOIUZUEYCMLHPNHXUEKCBWTMKYBKHZLNKTETDXIHYBHF + BQKKMEXUPLMTKGFIMPMTDVRZEBFEFARDEVQADVKIVNBHNZLEAAGLFIKXHTQWXBPNLXZHFUDZ -WBOSEXMRLZVHNZSSQSBCKAERRITRBAZWGVZNTGDSIVIZEMBKWDOOBDWIDCZIUHVTKVTSGFDT + XYLLVFETIDCYAXGTIHUFUSFMMWTLKZARVQOFGEBKIGWHFFFFKTHPQOMVAVILFLWMRLWPBWVW -NRGPNGRNKEVLZVYOAYTUWCDTOLGEQDBSHVRMMYYQIEZWKWZVCPGIAGKPNZEGTLPKMPEILQAC + PFKSLFFWKYQVPHVPQTTVFRLQXHFMGVXVHTNSVZQSETKHXAPPSLHAIAVXWGDFVKOBQLEWPAZY -MIPKMYCFMVZKUTVKIPXBKGLFATSDCMYLPYLSTPBARVELNSOUFPZMUTRIRNZLYMDITNXMKLGE + WELQRIFCGSOVFSLYXMSETCTROLNKMDWZSGKREFEPEHCOKSMRTNCDSTHCSQKMKCBYEWZSQHPK -QXYRYEAOHGETEQMKTUMARPYMPKONUKYZIHTFGNHIKVOLTLRPMPNFSDHWQGVHBNDCWZCLUHQW + UNWWUBKICDYYOSIZWKWRLGGIMYWKMDXOUKYHCUYCACKSFRWWXADQUZTCNYEYCSQEBTCZMSCV -GIFBFZKZOFZRRYCBOSBWSNVFXXPSAGOUOXDNBNSOZADYYSKWQUTYNCDNGVLSZNDBCTSIYOUX + NTGCDGFUUCTQWINV........................................................ + """ -NNUGLZNKNDSYGRUCXTKLBIMXZMMZAEHVRFYMBFKVDPAEHOYCBXKADZOTRRMULOPAIKCIHKFY + 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()) -BQKKMEXUPLMTKGFIMPMTDVRZEBFEFARDEVQADVKIVNBHNZLEAAGLFIKXHTQWXBPNLXZHFUDZ + 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)) -XYLLVFETIDCYAXGTIHUFUSFMMWTLKZARVQOFGEBKIGWHFFFFKTHPQOMVAVILFLWMRLWPBWVW + 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)) -PFKSLFFWKYQVPHVPQTTVFRLQXHFMGVXVHTNSVZQSETKHXAPPSLHAIAVXWGDFVKOBQLEWPAZY + 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) -WELQRIFCGSOVFSLYXMSETCTROLNKMDWZSGKREFEPEHCOKSMRTNCDSTHCSQKMKCBYEWZSQHPK + 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) -UNWWUBKICDYYOSIZWKWRLGGIMYWKMDXOUKYHCUYCACKSFRWWXADQUZTCNYEYCSQEBTCZMSCV +class TestOneTimePad(unittest.TestCase): -NTGCDGFUUCTQWINV........................................................ + 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__': diff --git a/onetimepad.py b/onetimepad.py index 92febdf..1ea151f 100644 --- a/onetimepad.py +++ b/onetimepad.py @@ -1,62 +1,177 @@ from itertools import izip -PAD_MODULO = 3 -PADLINE_OFFSET = -2 -TEXTLINE_OFFSET = -1 DEFAULT_PAD_WIDTH = 72 -CIPHER_FLOOR = 1 -CIPHER_CEIL = 25 -NULLCHAR = '.' -def encode(msg, pad): - ret = [] - filled = list(_text_padfill(msg, pad)) - for i, line in enumerate(_text_cipherfill(filled)): - lineno = i + 1 - if _is_cipherline(lineno): - ret.append(line) - return ''.join(ret).strip(NULLCHAR) - - -def decode(ciphertext, pad): - ret = [] - filled = list(_cipher_padfill(ciphertext, pad)) - for i, line in enumerate(_cipher_textfill(filled)): - lineno = i + 1 - if _is_textline(lineno): - ret.append(line) - return ''.join(ret).strip(NULLCHAR) - - -def create_pad(length, width=DEFAULT_PAD_WIDTH): - return '\n'.join(create_pad_lines(length, width=width)) - - -def create_pad_lines(length, width=DEFAULT_PAD_WIDTH): - chars = _create_chars_for_pad(length) - lines = _chunk_chars_into_lines(chars, width=width) - for line in lines: - yield line - yield '' - yield '' - - -def _create_chars_for_pad(length): - chars = [] - for char in range(length): - chars.append(_AS_ALPHA[_get_randint()]) - return ''.join(chars) - - -def _chunk_chars_into_lines(chars, width=DEFAULT_PAD_WIDTH): +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, width=width): + for chunk in _as_line_chunks(chars, nullchar, width=width): lines.append(chunk) return lines -def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): +def _as_line_chunks(chars, nullchar, width=DEFAULT_PAD_WIDTH): chunk = [] for char in chars.replace('\n', ''): chunk.append(char) @@ -64,142 +179,42 @@ def _as_line_chunks(chars, width=DEFAULT_PAD_WIDTH): yield ''.join(chunk) chunk = [] if len(chunk) < width: - chunk += ([NULLCHAR] * (width - len(chunk))) + chunk += ([nullchar] * (width - len(chunk))) yield ''.join(chunk) -def _text_padfill(text, padlines): - if isinstance(padlines, basestring): - padlines = padlines.splitlines() - padlines = [line.strip() for line in padlines if line.strip()] - textlines = _chunk_chars_into_lines(text, _get_textwidth(padlines)) - for textline, padline in izip(textlines, padlines): - yield padline - yield textline - yield '' - - -def _cipher_padfill(ciphertext, padlines): - if isinstance(padlines, basestring): - padlines = padlines.splitlines() - padlines = [line.strip() for line in padlines if line.strip()] - cipherlines = _chunk_chars_into_lines(ciphertext, _get_textwidth(padlines)) - for cipherline, padline in izip(cipherlines, padlines): - yield padline - yield '' - yield cipherline - - -def _text_cipherfill(padfilled_text_lines): - for i, line in enumerate(padfilled_text_lines): - lineno = i + 1 - if _is_cipherline(lineno): - padline = padfilled_text_lines[i - abs(PADLINE_OFFSET)] - textline = padfilled_text_lines[i - abs(TEXTLINE_OFFSET)] - yield padline - yield textline - yield _cipherline_from_padline_and_textline(padline, textline) - - -def _cipher_textfill(padfilled_cipher_lines): - for i, line in enumerate(padfilled_cipher_lines): - lineno = i + 1 - if _is_cipherline(lineno): - padline = padfilled_cipher_lines[i - abs(PADLINE_OFFSET)] - textline = padfilled_cipher_lines[i - abs(TEXTLINE_OFFSET)] - yield padline - yield _textline_from_cipherline_and_padline(line, padline) - yield line - - -def _cipherline_from_padline_and_textline(padline, textline): - ret = [] - for padchar, textchar in izip(padline, textline): - if textchar == NULLCHAR: - ret.append(NULLCHAR) - continue - charnum = _AS_NUMS[padchar] + _AS_NUMS[textchar] - idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL - ret.append(_AS_ALPHA[idx]) - return ''.join(ret) - - -def _textline_from_cipherline_and_padline(cipherline, padline): - ret = [] - for ciphercar, padchar in izip(cipherline, padline): - if ciphercar == NULLCHAR: - ret.append(NULLCHAR) - continue - charnum = _AS_NUMS[ciphercar] - _AS_NUMS[padchar] - idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL - if idx < 0: - idx = CIPHER_CEIL + idx - ret.append(_AS_ALPHA[idx]) - return ''.join(ret) - - def _get_textwidth(textlines): return max([len(line) for line in textlines]) -def _is_padline(lineno): - return _is_cipherline(lineno + abs(PADLINE_OFFSET)) - - -def _is_textline(lineno): - return _is_cipherline(lineno + abs(TEXTLINE_OFFSET)) - - -def _is_cipherline(lineno): - return not lineno % PAD_MODULO - - -def _get_randint(randomness_file='/dev/urandom', fallback_to_fake=True): - if not _DEV_RANDOM.get('fp'): +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_RANDOM['fp'].read(1)) % CIPHER_CEIL - return ret if ret != 0 else _get_randint() + ret = ord(_DEV_URANDOM['fp'].read(1)) % modulo + return ret if ret != 0 else _get_randint(modulo) class _FakeDevUrandom(object): - def read(self, nchars): + @staticmethod + def read(nchars): import random - return ''.join([chr(random.randint(0, 255)) for i in range(0, nchars)]) + 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_RANDOM['fp'] = open(randomness_file) + _DEV_URANDOM['fp'] = open(randomness_file) except (OSError, IOError): if fallback_to_fake: - _DEV_RANDOM['fp'] = _FakeDevUrandom() + _DEV_URANDOM['fp'] = _FakeDevUrandom() else: raise -_DEV_RANDOM = {} - - -def _mk_as_alpha_as_nums(): - as_alpha = {} - as_nums = {} - a_chr = ord('A') - past_j = False - - for char in range(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 - - return as_alpha, as_nums - - -_AS_ALPHA, _AS_NUMS = _mk_as_alpha_as_nums() +_DEV_URANDOM = {} From 5ca2f9de342cf834bd7942ae0c62b380c112d671 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 28 Nov 2009 14:16:01 -0500 Subject: [PATCH 18/19] converting from distutils to paver --- .gitignore | 5 +++++ MANIFEST.in | 1 + setup.py => pavement.py | 16 ++++++++-------- 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 MANIFEST.in rename setup.py => pavement.py (60%) diff --git a/.gitignore b/.gitignore index 004a8d7..fab747d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ *.pyc .coverage +*.egg-info +build +dist +setup.py +paver-minilib.zip diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..305c839 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include *.py *.zip diff --git a/setup.py b/pavement.py similarity index 60% rename from setup.py rename to pavement.py index d2e947a..d2d1baa 100644 --- a/setup.py +++ b/pavement.py @@ -1,8 +1,8 @@ -import sys -from distutils.core import setup +from paver.easy import * +from paver.setuputils import setup -SETUP_ARGS = dict( +SETUP_ARGS = Bunch( name='OneTimePad', version='0.1.0', author='Dan Buch', @@ -14,10 +14,10 @@ SETUP_ARGS = dict( ) -def main(): - setup(**SETUP_ARGS) - return 0 +setup(**SETUP_ARGS) -if __name__ == '__main__': - sys.exit(main()) +@task +@needs(['generate_setup', 'minilib', 'setuptools.command.sdist']) +def sdist(): + pass From 29fbe8e32cc6afb3773b778f8c621bd9c62b7eb8 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Sat, 28 Nov 2009 14:19:54 -0500 Subject: [PATCH 19/19] filling in more crap for paver-i-fication --- README | 3 +++ pavement.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 README diff --git a/README b/README new file mode 100644 index 0000000..89a723f --- /dev/null +++ b/README @@ -0,0 +1,3 @@ +Sure, they aren't especially practical, but one-time pads are academically interesting :) + +.. vim:filetype=rst diff --git a/pavement.py b/pavement.py index d2d1baa..ca5722c 100644 --- a/pavement.py +++ b/pavement.py @@ -2,6 +2,7 @@ from paver.easy import * from paver.setuputils import setup +README = path.getcwd()/'README' SETUP_ARGS = Bunch( name='OneTimePad', version='0.1.0', @@ -9,8 +10,10 @@ SETUP_ARGS = Bunch( 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='', + long_description=README.bytes(), py_modules=['onetimepad'], + license='MIT', + platforms=['any'], )