Danger
Nothing here should be used for any security purposes.
Vigenère cipher¶
This module is imported with:
import toy_crypto.vigenere
The Vigenère cipher is a historic paper and pencil cipher that when used properly can be easily broken by machine and can be broken by hand though a tedious process. With improper use it is easy to break by hand.
from toy_crypto import vigenere
alphabet = vigenere.Alphabet.CAPS_ONLY
cipher = vigenere.Cipher("RAVEN", alphabet)
plaintext = "ONCE UPON A MIDNIGHT DREARY"
encrypted = cipher.encrypt(plaintext)
assert encrypted == "FNXI HGOI E ZZDIMTYT YVRRRT"
assert cipher.decrypt(encrypted) == plaintext
Proper use (which merely makes this annoying to break by hand instead of easy to break by hand) requires removing any character from the plaintext that is not in the Vigenère alphabet.
from toy_crypto import vigenere
alphabet = vigenere.Alphabet.CAPS_ONLY
cipher = vigenere.Cipher("RAVEN", alphabet)
plaintext = "ONCE UPON A MIDNIGHT DREARY"
plaintext = [c for c in plaintext if c in alphabet]
plaintext = ''.join(plaintext)
encrypted = cipher.encrypt(plaintext)
assert encrypted == "FNXIHGOIEZZDIMTYTYVRRRT"
decrypted = cipher.decrypt(encrypted)
print(decrypted)
ONCEUPONAMIDNIGHTDREARY
Using Alphabet.PRINTABLE
will preserve more of the input, as it includes most printiable 7-bit ASCII characters.
The Cipher
class¶
A new cipher is created from a key and an alphabet.
If no alphabet is specified the Alphabet.DEFAULT
is used.
>>> cipher = vigenere.Cipher("RAVEN")
>>> plaintext = "ONCE UPON A MIDNIGHT DREARY"
>>> encrypted = cipher.encrypt(plaintext)
>>> encrypted
'FNXI HGOI E ZZDIMTYT YVRRRT'
>>> cipher.decrypt(encrypted)
'ONCE UPON A MIDNIGHT DREARY'
While a Cipher instance persists the key and the alphabet,
the Cipher.encrypt()
method starts over at the 0-th element of the key.
>>> cipher = vigenere.Cipher("DEADBEEF", alphabet= "0123456789ABCDEF")
>>> zero_message = "00000000000000000000"
>>> encrypted = cipher.encrypt(zero_message)
>>> encrypted
'DEADBEEFDEADBEEFDEAD'
We can use cipher defined above to decrypt.
>>> new_encrypted = cipher.decrypt("887703")
>>> new_encrypted
'BADA55'
The Alphebet
class¶
- class toy_crypto.vigenere.Alphabet(alphabet: str | None = None, prebaked: str | None = None)¶
An alphabet.
This does not check if the alphabet is sensible. In particular, you may get very peculiar results if the alphabet contains duplicate elements.
Instances of this class are conventionally immutable.
- CAPS_ONLY = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'¶
‘A’ through ‘Z’ in order.
- DEFAULT = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'¶
CAPS_ONLY is the default.
- PRINTABLE = 'JDi-Km9247oBEctS%Isxz{<;=W^fL,[Y3Mgd6HV(kR8:_CF"*\')>|#~Xay!]N+1vnqTl/}j$A.@0b ZGe`UPhp?Ow&ru5Q'¶
Printable 7-bit ASCII in a fixed scrambled order.
It does not include the backslash character, and the scrambled order is hardcoded.
Cryptanalysis tools¶
Some tools (currently just one, but more may be comming) to assist in breaking Vigenère.
At the moment, I am choosing not to include statistical analyses, as I want to minimize package dependencies and not importing scipy.stats. Thus functions here are very statistically naïve.
- toy_crypto.vigenere.probable_keysize(ciphertext: bytes | str, min_size: int = 3, max_size: int = 40, trial_pairs: int = 1) SimilarityScores ¶
Assesses likelihood for key length of ciphertext.
- Parameters:
- Param:
trial_pairs: The number of pairs of blocks to test.
- Returns:
Returns list sorted by scores of (keysize, score)
- Return type:
SimilarityScores
Scores are scaled 0 (least likely) to 1 (most likely), but they do not directly represent probabilities.
The algorithm has a long history, but I’ve lifted it from Cryptopals set 1, chalange 6.