Danger

Nothing here should be used for any security purposes.

  • If you need cryptographic tools in a Python environment use pyca.

  • If you need efficient and reliable abstract math utilities in a Python-like environment consider using SageMath.

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'
class toy_crypto.vigenere.Cipher(key: str, alphabet: Alphabet | str | None = None)

A Vigenère Cipher is a key and an alphabet.

Parameters:
property alphabet: Alphabet

The Alphabet for this cipher.

crypt(text: str, mode: str) str

{en,de}crypts text depending on mode

Parameters:
Return type:

str

decrypt(ciphertext: str) str

Returns plaintext.

Parameters:

ciphertext (str)

Return type:

str

encrypt(plaintext: str) str

Returns ciphertext.

Parameters:

plaintext (str)

Return type:

str

property key: str

Shhh! This is the key. Keep it secret.

property modulus: int

The modulus.

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.

Parameters:
  • alphabet (str | None)

  • prebaked (str | None)

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.

property abc2idx: dict[str, int]

Dictionary of letter to position in the alphabet.

add(a: str, b: str) str

Returns the modular sum of two characters.

Parameters:
Return type:

str

property alphabet: str

The underlying alphabet.

inverse(c: str) str

Returns the additive inverse of character c

Parameters:

c (str)

Return type:

str

property modulus: int

The modulus.

subtract(a: str, b: str) str

Returns the character corresponding to a - b.

Parameters:
Return type:

str

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:
  • ciphertext (bytes | str) – The ciphertext.

  • min_size (int) – The minimum key length to try.

  • max_size (int) – The maximum key length to try.

  • trial_pairs (int)

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.