beginning process of "making it right", enclosing mod 25 one-time pad stuff in its own class

cat-town
Dan Buch 15 years ago
parent e0d5e608f9
commit b932174e34

@ -2,90 +2,10 @@ import unittest
import onetimepad as OT import onetimepad as OT
class TestOneTimePad(unittest.TestCase): class TestMod25(unittest.TestCase):
msg = ('HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFA' msg = ('HOWMUCHCHUCKCOULDAWOODCHUCKCHUCKIFA'
'WOODCHUCKCOULDCHUCKWOOD' * 50) 'WOODCHUCKCOULDCHUCKWOOD' * 50)
_padsize = 2000 _padsize = 2000
def test_mk_as_alpha_excludes_j(self):
self.assertTrue('J' not in OT._AS_ALPHA.values())
def test_mk_as_alpha_dict_has_25_members(self):
self.assertEqual(25, len(OT._AS_ALPHA.items()))
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))
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),
'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))
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)
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) PADLINES = (1, 4, 7, 10, 13, 16, 19, 22, 25)
TEXTLINES = (2, 5, 8, 11, 14, 17, 20, 23, 26) TEXTLINES = (2, 5, 8, 11, 14, 17, 20, 23, 26)
CIPHERLINES = (3, 6, 9, 12, 15, 18, 21, 24, 27) CIPHERLINES = (3, 6, 9, 12, 15, 18, 21, 24, 27)
@ -181,6 +101,91 @@ 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__': if __name__ == '__main__':
unittest.main() unittest.main()

@ -1,205 +1,220 @@
from itertools import izip from itertools import izip
PAD_MODULO = 3
PADLINE_OFFSET = -2
TEXTLINE_OFFSET = -1
DEFAULT_PAD_WIDTH = 72 DEFAULT_PAD_WIDTH = 72
CIPHER_FLOOR = 1
CIPHER_CEIL = 25
NULLCHAR = '.'
def encode(msg, pad): 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 = [] ret = []
filled = list(_text_padfill(msg, pad)) filled = list(self._text_padfill(msg))
for i, line in enumerate(_text_cipherfill(filled)): for i, line in enumerate(self._text_cipherfill(filled)):
lineno = i + 1 lineno = i + 1
if _is_cipherline(lineno): if self._is_cipherline(lineno):
ret.append(line) ret.append(line)
return ''.join(ret).strip(NULLCHAR) return ''.join(ret).strip(self._nullchar)
def decode(ciphertext, pad): def decode(self, ciphertext):
ret = [] ret = []
filled = list(_cipher_padfill(ciphertext, pad)) filled = list(self._cipher_padfill(ciphertext))
for i, line in enumerate(_cipher_textfill(filled)): for i, line in enumerate(self._cipher_textfill(filled)):
lineno = i + 1 lineno = i + 1
if _is_textline(lineno): if self._is_textline(lineno):
ret.append(line) ret.append(line)
return ''.join(ret).strip(NULLCHAR) return ''.join(ret).strip(self._nullchar)
def create_pad(length, width=DEFAULT_PAD_WIDTH): def create_pad(self, length, width=DEFAULT_PAD_WIDTH):
return '\n'.join(create_pad_lines(length, width=width)) return '\n'.join(self.create_pad_lines(length, width=width))
def create_pad_lines(self, length, width=DEFAULT_PAD_WIDTH):
def create_pad_lines(length, width=DEFAULT_PAD_WIDTH): chars = self._create_chars_for_pad(length)
chars = _create_chars_for_pad(length) lines = _chunk_chars_into_lines(chars, self._nullchar, width=width)
lines = _chunk_chars_into_lines(chars, width=width)
for line in lines: for line in lines:
yield line yield line
yield '' yield ''
yield '' yield ''
def _create_chars_for_pad(self, length):
def _create_chars_for_pad(length):
chars = [] chars = []
for char in range(length): for char in range(length):
chars.append(_AS_ALPHA[_get_randint()]) chars.append(self._as_alpha[_get_randint(self._cipher_ceil)])
return ''.join(chars) return ''.join(chars)
def _ensure_pad_is_lines(self):
if isinstance(self.pad, basestring):
self.pad = self.pad.splitlines()
def _chunk_chars_into_lines(chars, width=DEFAULT_PAD_WIDTH): def _text_padfill(self, text):
lines = [] self._ensure_pad_is_lines()
for chunk in _as_line_chunks(chars, width=width): padlines = [line.strip() for line in self.pad if line.strip()]
lines.append(chunk) textlines = _chunk_chars_into_lines(text, self._nullchar,
return lines _get_textwidth(padlines))
def _as_line_chunks(chars, 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 _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): for textline, padline in izip(textlines, padlines):
yield padline yield padline
yield textline yield textline
yield '' yield ''
def _cipher_padfill(self, ciphertext):
def _cipher_padfill(ciphertext, padlines): self._ensure_pad_is_lines()
if isinstance(padlines, basestring): padlines = [line.strip() for line in self.pad if line.strip()]
padlines = padlines.splitlines() cipherlines = _chunk_chars_into_lines(ciphertext, self._nullchar,
padlines = [line.strip() for line in padlines if line.strip()] _get_textwidth(padlines))
cipherlines = _chunk_chars_into_lines(ciphertext, _get_textwidth(padlines))
for cipherline, padline in izip(cipherlines, padlines): for cipherline, padline in izip(cipherlines, padlines):
yield padline yield padline
yield '' yield ''
yield cipherline yield cipherline
def _text_cipherfill(self, padfilled_text_lines):
def _text_cipherfill(padfilled_text_lines):
for i, line in enumerate(padfilled_text_lines): for i, line in enumerate(padfilled_text_lines):
lineno = i + 1 lineno = i + 1
if _is_cipherline(lineno): if self._is_cipherline(lineno):
padline = padfilled_text_lines[i - abs(PADLINE_OFFSET)] padline = padfilled_text_lines[i - abs(self._padline_offset)]
textline = padfilled_text_lines[i - abs(TEXTLINE_OFFSET)] textline = padfilled_text_lines[i - abs(self._textline_offset)]
yield padline yield padline
yield textline yield textline
yield _cipherline_from_padline_and_textline(padline, textline) yield self._cipherline_from_padline_and_textline(padline,
textline)
def _cipher_textfill(padfilled_cipher_lines): def _cipher_textfill(self, padfilled_cipher_lines):
for i, line in enumerate(padfilled_cipher_lines): for i, line in enumerate(padfilled_cipher_lines):
lineno = i + 1 lineno = i + 1
if _is_cipherline(lineno): if self._is_cipherline(lineno):
padline = padfilled_cipher_lines[i - abs(PADLINE_OFFSET)] padline = padfilled_cipher_lines[i - abs(self._padline_offset)]
textline = padfilled_cipher_lines[i - abs(TEXTLINE_OFFSET)] textline = \
padfilled_cipher_lines[i - abs(self._textline_offset)]
yield padline yield padline
yield _textline_from_cipherline_and_padline(line, padline) yield self._textline_from_cipherline_and_padline(line, padline)
yield line yield line
def _cipherline_from_padline_and_textline(self, padline, textline):
def _cipherline_from_padline_and_textline(padline, textline):
ret = [] ret = []
for padchar, textchar in izip(padline, textline): for padchar, textchar in izip(padline, textline):
if textchar == NULLCHAR: if textchar == self._nullchar:
ret.append(NULLCHAR) ret.append(self._nullchar)
continue continue
charnum = _AS_NUMS[padchar] + _AS_NUMS[textchar] charnum = self._as_nums[padchar] + self._as_nums[textchar]
idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL idx = charnum if charnum <= self._cipher_ceil else \
ret.append(_AS_ALPHA[idx]) charnum % self._cipher_ceil
ret.append(self._as_alpha[idx])
return ''.join(ret) return ''.join(ret)
def _textline_from_cipherline_and_padline(self, cipherline, padline):
def _textline_from_cipherline_and_padline(cipherline, padline):
ret = [] ret = []
for ciphercar, padchar in izip(cipherline, padline): for ciphercar, padchar in izip(cipherline, padline):
if ciphercar == NULLCHAR: if ciphercar == self._nullchar:
ret.append(NULLCHAR) ret.append(self._nullchar)
continue continue
charnum = _AS_NUMS[ciphercar] - _AS_NUMS[padchar] charnum = self._as_nums[ciphercar] - self._as_nums[padchar]
idx = charnum if charnum <= CIPHER_CEIL else charnum % CIPHER_CEIL idx = charnum if charnum <= self._cipher_ceil else \
charnum % self._cipher_ceil
if idx < 0: if idx < 0:
idx = CIPHER_CEIL + idx idx = self._cipher_ceil + idx
ret.append(_AS_ALPHA[idx]) ret.append(self._as_alpha[idx])
return ''.join(ret) return ''.join(ret)
def _is_padline(self, lineno):
return self._is_cipherline(lineno + abs(self._padline_offset))
def _get_textwidth(textlines): def _is_textline(self, lineno):
return max([len(line) for line in textlines]) return self._is_cipherline(lineno + abs(self._textline_offset))
def _is_cipherline(self, lineno):
return not lineno % self._pad_modulo
def _is_padline(lineno):
return _is_cipherline(lineno + abs(PADLINE_OFFSET))
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 _is_textline(lineno):
return _is_cipherline(lineno + abs(TEXTLINE_OFFSET))
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 _is_cipherline(lineno):
return not lineno % PAD_MODULO
def _get_textwidth(textlines):
return max([len(line) for line in textlines])
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, _get_urandom_fp(randomness_file=randomness_file,
fallback_to_fake=fallback_to_fake) fallback_to_fake=fallback_to_fake)
ret = ord(_DEV_RANDOM['fp'].read(1)) % CIPHER_CEIL ret = ord(_DEV_URANDOM['fp'].read(1)) % modulo
return ret if ret != 0 else _get_randint() return ret if ret != 0 else _get_randint(modulo)
class _FakeDevUrandom(object): class _FakeDevUrandom(object):
def read(self, nchars): @staticmethod
def read(nchars):
import random 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): def _get_urandom_fp(randomness_file='/dev/urandom', fallback_to_fake=True):
try: try:
_DEV_RANDOM['fp'] = open(randomness_file) _DEV_URANDOM['fp'] = open(randomness_file)
except (OSError, IOError): except (OSError, IOError):
if fallback_to_fake: if fallback_to_fake:
_DEV_RANDOM['fp'] = _FakeDevUrandom() _DEV_URANDOM['fp'] = _FakeDevUrandom()
else: else:
raise raise
_DEV_RANDOM = {} _DEV_URANDOM = {}
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()

Loading…
Cancel
Save