Danger

Nothing here should be used for any security purposes.

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

  • 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 printable 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 zeroth 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)[source]#

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

Parameters:
  • key (str)

  • alphabet (Alphabet | str | None, default: None)

property alphabet: Alphabet#

The Alphabet for this cipher.

crypt(text: str, mode: str) str[source]#

{en,de}crypts text depending on mode

Parameters:
  • text (str)

  • mode (str)

Return type:

str

decrypt(ciphertext: str) str[source]#

Returns plaintext.

Parameters:

ciphertext (str)

Return type:

str

encrypt(plaintext: str) str[source]#

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, pre_baked: str | None = None)[source]#
Parameters:
  • alphabet (Optional[str], default: None) – Sequence of characters to be used to construct alphabet.

  • pre_baked (Optional[str], default: None) – Name of pre_baked (hardcoded) alphabets to use.

Raises:

ValueError – if both alphabet and pre_baked are used.

Warning

This does not check if the alphabet is sensible. In particular, you may get very peculiar results if the alphabet contains duplicate elements.

CAPS_ONLY: str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'#

‘A’ through ‘Z’ in order.

DEFAULT: str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'#

Default alphabet to use.

PREBAKED: Sequence[str] = ['default', 'caps', 'printable']#

Names of pre-baked alphabets

PRINTABLE: str = '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: Mapping[str, int]#

Deprecated since version 0.6: Renamed. Use inverse_map() instead.

add(a: str, b: str) str[source]#

Returns the modular sum of two characters.

Invalid input may lead to a KeyError

Parameters:
  • a (str)

  • b (str)

Return type:

str

property alphabet: str#

The underlying alphabet.

inverse(c: str) str[source]#

Returns the additive inverse of character c.

Invalid input may lead to a KeyError

Parameters:

c (str)

Return type:

str

property inverse_map: Mapping[str, int]#

Dictionary of Letter to position in the alphabet.

property modulus: int#

The modulus.

subtract(a: str, b: str) str[source]#

Returns the character corresponding to a - b.

Invalid input may lead to a KeyError

Parameters:
  • a (str)

  • b (str)

Return type:

str

Cryptanalysis tools#

Some tools (currently just one, but more may be coming) to assist in breaking Vigenère.

toy_crypto.vigenere.BitSimilarity#

A float in [-4.0, 4.0] the bit similarity per byte. -4 indicates that all bits are different. +4 indicates that all bits are the same.

alias of Annotated[float, ValueRange(min=-4.0, max=4.0)]

class toy_crypto.vigenere.SimilarityScores None[source]#

A dictionary of keysize : dict[int, list[BitSimilarity]].

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[source]#

Assesses likelihood for key length of ciphertext.

Parameters:
  • ciphertext (bytes | str) – The ciphertext.

  • min_size (int, default: 3) – The minimum key length to try.

  • max_size (int, default: 40) – The maximum key length to try.

  • trial_pairs (int, default: 1) – The number of pairs of blocks to test.

Return type:

SimilarityScores

Returns:

Returns list sorted by scores of (keysize, score)

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, challenge 6.