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 = {}