137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
'''
|
|
Implementation of various substitution cyphers, some of which are
|
|
already in the standard library but lack features/options I require.
|
|
|
|
Terminology:
|
|
1. "Simple": simple substitution cyphers operates on single letters
|
|
2. "Polygraphic": polygraphic substitution cyphers operate on groups
|
|
of letters larger than length 1.
|
|
3. "Monoalphabetic cypher": a monoalphabetic cypher applies the same fixed
|
|
substitutions to the entire message (ie ROT13)
|
|
4. "Polyalphabetic cypher": a polyalphabetic cypher applies different
|
|
substitutions to the entire message (ie vigenere)
|
|
'''
|
|
|
|
from celeste.constants import ALPHA_LOWER, ALPHA_UPPER
|
|
|
|
'''
|
|
Constant Declarations
|
|
'''
|
|
# Error Message Format Strings
|
|
ERRMSG_CHARSET_DUPL = 'Bad charsets for {0}: found duplicate character \'{1}\''
|
|
ERRMSG_NOT_IN_CHARSET = 'No charset for {0} has character \'{1}\''
|
|
# Charsets
|
|
CHARSET_SUB_DEFAULT = [ALPHA_LOWER, ALPHA_UPPER]
|
|
|
|
'''
|
|
==========================================
|
|
Simple Monoalphabetic Substitution Cyphers
|
|
==========================================
|
|
'''
|
|
def ROT(plaintext: str | list,
|
|
n: int,
|
|
charsets: list[str] | list[list] = CHARSET_SUB_DEFAULT,
|
|
ignore_noncharset: bool = True,
|
|
as_string: bool = True) -> str:
|
|
cyphertext = []
|
|
for c in plaintext:
|
|
i = None
|
|
active_charset = None
|
|
for charset in charsets:
|
|
try:
|
|
i = charset.index(c)
|
|
# if we reached here then no ValueError occured
|
|
if active_charset is not None:
|
|
raise ValueError(ERRMSG_CHARSET_DUPL.format('ROT', c))
|
|
active_charset = charset
|
|
except ValueError: pass
|
|
|
|
if i is not None:
|
|
cyphertext.append(active_charset[(i + n) % len(active_charset)])
|
|
elif not ignore_noncharset:
|
|
raise ValueError(ERRMSG_NOT_IN_CHARSET.format('ROT', c))
|
|
else:
|
|
# if ignore_noncharset then just append c
|
|
cyphertext.append(c)
|
|
if as_string:
|
|
return ''.join(cyphertext)
|
|
return cyphertext
|
|
|
|
# Common ROT aliases
|
|
def ROT13(plaintext: str, charsets: list[str] = CHARSET_SUB_DEFAULT) -> str:
|
|
return ROT(plaintext, 13, charsets=charsets)
|
|
def caesar(plaintext: str, charsets: list[str] = CHARSET_SUB_DEFAULT) -> str:
|
|
return ROT(plaintext, 1, charsets=charsets)
|
|
|
|
# The Atbash Cypher maps a character to its reverse (ie a -> z, b -> y, etc)
|
|
def atbash(plaintext: str | list,
|
|
charsets: list[str] | list[list] = CHARSET_SUB_DEFAULT,
|
|
ignore_noncharset: bool = True,
|
|
as_string: bool = True) -> str:
|
|
cyphertext = []
|
|
for c in plaintext:
|
|
i = None
|
|
active_charset = None
|
|
for charset in charsets:
|
|
try:
|
|
i = charset.index(c)
|
|
# if we reached here then no ValueError occured
|
|
if active_charset is not None:
|
|
raise ValueError(ERRMSG_CHARSET_DUPL.format('Atbash', c))
|
|
active_charset = charset
|
|
except ValueError: pass
|
|
|
|
if i is not None:
|
|
L = len(active_charset)
|
|
cyphertext.append(active_charset[(L - i - 1) % L])
|
|
elif not ignore_noncharset:
|
|
raise ValueError(ERRMSG_NOT_IN_CHARSET.format('Atbash', c))
|
|
else:
|
|
# if ignore_noncharset then just append c
|
|
cyphertext.append(c)
|
|
if as_string:
|
|
return ''.join(cyphertext)
|
|
return cyphertext
|
|
|
|
'''
|
|
=========================================
|
|
Simple Polyalphabetic Substituion Cyphers
|
|
=========================================
|
|
'''
|
|
def vigenere(plaintext: str | list,
|
|
key: str | list,
|
|
charsets: list[str] | list[list] = CHARSET_SUB_DEFAULT,
|
|
key_charset: str | list = ALPHA_UPPER,
|
|
decrypt: bool = False,
|
|
ignore_noncharset: bool = True,
|
|
as_string: bool = True) -> str:
|
|
cyphertext = []
|
|
# map the key characters to their rotations
|
|
rots = [key_charset.index(k) for k in key]
|
|
pos = 0
|
|
for c in plaintext:
|
|
i = None
|
|
active_charset = None
|
|
for charset in charsets:
|
|
try:
|
|
i = charset.index(c)
|
|
# if we reached here then no ValueError occured
|
|
if active_charset is not None:
|
|
raise ValueError(ERRMSG_CHARSET_DUPL.format('Vigenere', c))
|
|
active_charset = charset
|
|
except ValueError: pass
|
|
|
|
if i is not None:
|
|
rot = rots[pos % len(rots)]
|
|
if decrypt: rot = -rot
|
|
cyphertext.append(active_charset[(i + rot) % len(active_charset)])
|
|
pos += 1
|
|
elif not ignore_noncharset:
|
|
raise ValueError(ERRMSG_NOT_IN_CHARSET.format('Vigenere', c))
|
|
else:
|
|
# if ignore_noncharset then just append c
|
|
cyphertext.append(c)
|
|
if as_string:
|
|
return ''.join(cyphertext)
|
|
return cyphertext
|
|
|