Module cipher_solver.simple
Expand source code
import random
from collections import Counter
from string import ascii_lowercase, ascii_uppercase
import numpy as np
from cipher_solver.consts import (
DIGRAM_MATRIX_ENGLISH,
ENGLISH_LETTERS_BY_FREQUENCY,
RANDOM_INDEX_DISTRIBUTION,
STANDARD_ALPHABET_SIZE,
)
from cipher_solver.utils import common_to_alphabetical_key
class SimpleSolver:
"""Solver for simple monoalphabetic substitution ciphers.
This solver is based on the paper
"A Fast Method for the Cryptanalysis of Substitution Ciphers" by Thomas Jakobsen.
The details of the algorithm is described in the solve method docstrings.
The following terminology is used:
"ciphertext" : The encrypted text we want to solve to get a plaintext.
"plaintext" : The decrypted plaintext using a certain decryption key.
"common key" : Key used to generate a plaintext, ordered by most common letter.
"alphabetical key" : Decryption key, but ordered alphabetically.
"decryption key" : Assumed to mean a common key if nothing else is specified.
"digram" : A pair of letters, e.g. "aa", "cd" etc.
"digram matrix" : An (n x n) matrix, where n is the length of the used alphabet,
created from a given text, where the frequency of each digram
relative to the text length is saved to the corresponding index
pair, e.g. (0, 0) for "aa", (0, 1) for "ab" etc.
"distance sum" : The method used to score solutions, see ._score() for details.
"""
def __init__(self, ciphertext):
"""Create new solver.
Creates a new cipher solver from an initial ciphertext.
Parameters
----------
ciphertext : str
The ciphertext to solve.
Raises
------
ValueError
If the passed ciphertext is not a string.
If the passed ciphertext is empty.
"""
if not isinstance(ciphertext, str):
raise ValueError(f"{ciphertext} is not a string.")
if len(ciphertext) < 1:
raise ValueError("Ciphertext cannot be empty.")
# The decryption key is a list of letters that determines how the ciphertext is
# converted to plaintext. The key is equal in length to the English alphabet and
# is assumed to be in frequency order. In other words, the first letter is the
# letter in the ciphertext that should be translated to an "e", the second
# which one should be converted to a "t", and so on.
self._decryption_key = self._get_initial_key(ciphertext)
self._ciphertext = ciphertext
def _get_initial_key(self, ciphertext):
"""Construct the initial decryption key.
The initial decryption key is based on the letter frequencies in the ciphertext,
meaning an assumption that the most common letter in the ciphertext translates
to the most common letter in the English language, and so on. Any letters not
present in the ciphertext will be added alphabetically at the end of the key.
Parameters
----------
ciphertext : str
The ciphertext to generate an initial decryption key from.
Returns
-------
decryption_key : list
The initial decryption key.
Raises
------
ValueError
If the passed ciphertext is not a string.
If the passed ciphertext is empty.
"""
if not isinstance(ciphertext, str):
raise ValueError(f"{ciphertext} is not a string.")
if len(ciphertext) < 1:
raise ValueError("Ciphertext cannot be empty.")
decryption_key = self._get_common_letters(ciphertext)
for c in ascii_lowercase:
if c not in decryption_key:
decryption_key += c
return decryption_key
def _get_common_letters(self, text):
"""Get all unique letters of the passed text, sorted by frequency.
Parameters
----------
text : str
The text to find most common letters for.
Returns
-------
common_letters : list
The letters of the text ordered by frequency.
Raises
------
If the passed text is not a string.
If the passed text is empty.
"""
if not isinstance(text, str):
raise ValueError("{text} is not a string.")
if len(text) < 1:
raise ValueError("Text must not be empty.")
c = Counter(text)
return [letter[0] for letter in c.most_common() if letter[0] in ascii_lowercase]
def _get_digram_matrix(self, text):
"""Generate digram matrix for the passed text.
Parameters
----------
text : str
Text to generate digram frequency matrix for.
Returns
-------
digram_matrix : numpy.array
An array of digram frequencies indexed by [first][second] letter.
Raises
------
ValueError
If the passed text is not a string.
If the passed text does not contain at least one digram.
"""
if not isinstance(text, str):
raise ValueError(f"{text} is not a string.")
if len(text) < 2:
raise ValueError("Text must contain at least one digram.")
digram_matrix = np.zeros((STANDARD_ALPHABET_SIZE, STANDARD_ALPHABET_SIZE))
text = text.lower()
text_length = len(text)
# First, count the number of occurrences of each letter and save to the index
# that corresponds to the letter pair based on where it is in the English
# alphabet in frequency order.
num_digrams = 0
for i in range(0, text_length - 1):
char1 = text[i]
char2 = text[i + 1]
# Only count true digrams, i.e. where both chars are actual letters.
if char1 not in ascii_lowercase or char2 not in ascii_lowercase:
continue
a = ENGLISH_LETTERS_BY_FREQUENCY.index(char1)
b = ENGLISH_LETTERS_BY_FREQUENCY.index(char2)
digram_matrix[a, b] += 1
num_digrams += 1
# Replace each entry with a percentage of the total number of digrams, to get
# the same format as the English digram matrix.
rows, columns = digram_matrix.shape
for i in range(rows):
for j in range(columns):
# All digram frequencies are in percentages, so convert it here too.
digram_matrix[i, j] = 100 * digram_matrix[i, j] / num_digrams
return digram_matrix
def _score(self, matrix1, matrix2=DIGRAM_MATRIX_ENGLISH):
"""Calculate a score for passed digram matrices using the distance sum method.
The score is defined as the sum of all the absolute differences between each
corresponding element in the two matrices.
Parameters
----------
matrix1 : numpy.array
The first matrix to use in the comparison.
matrix2 : numpy.array
The second matrix to use in the comparison. Defaults to English digrams.
Returns
-------
score : float
The distance sum of the two matrices.
Raises
------
ValueError
If the passed matrices don't have the same number of rows and columns.
"""
if matrix1.shape != matrix2.shape:
raise ValueError("Digram matrices must have the same dimensions")
return abs(matrix1 - matrix2).sum()
def _swap_matrix(self, matrix, index1, index2):
"""Swap the matrix rows and columns at the given indices.
Parameters
----------
matrix : numpy.array
The matrix to modify in-place.
index1 : int
The first index to swap between.
index2 : int
The second index to swap between.
Raises
------
ValueError
If the passed matrix is not square.
"""
rows, columns = matrix.shape
if rows != columns:
raise ValueError("Matrix must be square.")
# Swap rows:
matrix[[index1, index2]] = matrix[[index2, index1]]
# Swap columns:
matrix[:, [index1, index2]] = matrix[:, [index2, index1]]
def _get_plaintext(self, decryption_key):
"""Return a plaintext using the passed decryption key.
Parameters
----------
decryption_key : list
The decryption key to use for generating the plaintext.
Returns
-------
plaintext : str
Plaintext from decrypting the ciphertext using the passed decryption key.
Raises
------
ValueError
If the passed decryption key does not contain all letters of the alphabet.
"""
if len(set(decryption_key)) != STANDARD_ALPHABET_SIZE:
raise ValueError("Key must include all letters of the alphabet.")
# The decryption key will be in order of most common first, so we need to
# construct a list of indices where to insert each to get an alphabetical key.
indices = [
ascii_lowercase.index(letter) for letter in ENGLISH_LETTERS_BY_FREQUENCY
]
translation_table = {}
for key_letter, index in zip(decryption_key, indices):
plain_letter = ascii_lowercase[index]
translation_table[key_letter] = plain_letter
plaintext = ""
for c in self._ciphertext:
is_upper = c in ascii_uppercase
letter = translation_table.get(c.lower(), c)
if is_upper:
letter = letter.upper()
plaintext += letter
return plaintext
def _weighted_random_index_pair(self):
"""Return a random index pair for swapping, weighted by letter frequency.
Instead of just picking random indices between zero and the alphabet length,
which will suggest very unlikely swaps, the indices are randomised with a weight
according to English letter frequency. In other words, low indices
(corresponding to common letters) will be suggested more often than high indices
(corresponding to uncommon letters).
Returns
-------
index_pair : list
A pair of random indices between zero and the alphabet length.
"""
return random.sample(RANDOM_INDEX_DISTRIBUTION, 2)
def _solve_deterministic(self):
"""Solve the cipher using predefined, structured digram matrix swaps.
This is the algorithm described by Jakobsen. It is based on the insight that
swapping rows and columns in a digram matrix is equivalent to swapping the
elements at the same indices in the key that was used to generate the plaintext
that was used to generate the digram matrix.
The algorithm works as follows:
1. Create an initial key that is the ciphertext letters ordered by frequency.
2. Generate a putative plaintext using this key.
3. Generate a digram matrix from this plaintext.
4. Calculate a score from this digram matrix using the distance sum method.
5. Repeat the following steps:
6a. Make a copy of the digram matrix.
6b. Swap rows/elements of this putative digram matrix at index (0, 1),
(1, 2), (2, 3) etc. until the last index of the pair reaches the
alphabet length. Then swap rows/columns at index (0, 2), (1, 3), (2, 4)
etc. until the last index in the pair reaches the alphabet length. The
last swap in this nested loop will be (0, 25).
6c. After each swap, calculate a score from the modified digram matrix.
6d. If the score improved, save the modified digram matrix as the new best
digram matrix, make the same swap in the key and save it as the new best
key, and save the improved score as the new best score.
7. The algorithm is done when all swaps have been made.
"""
# We need this as a list so we can modify it in-place.
key = self._decryption_key[:]
# Generate digram matrix from the corresponding plaintext.
putative_plaintext = self._get_plaintext(key)
digram_matrix = self._get_digram_matrix(putative_plaintext)
# Calculate initial score.
best_score = self._score(digram_matrix)
# Loop and swap rows/columns in digram matrix.
for i in range(1, STANDARD_ALPHABET_SIZE):
for j in range(STANDARD_ALPHABET_SIZE - i):
# Try a potential swap in the digram matrix.
d = np.copy(digram_matrix)
self._swap_matrix(d, j, j + i)
score = self._score(d)
if score < best_score:
# The score improved, so commit this change in both the digram
# matrix and the key.
digram_matrix = np.copy(d)
key[j], key[j + i] = key[j + i], key[j]
best_score = score
self._decryption_key = key[:]
def _solve_random(self):
"""Solve the cipher using random key swaps.
This is the algorithm described by Jakobsen, but using random key swaps instead
of the original structured swaps according to a certain pattern.
The algorithm works as follows:
1. Create an initial key that is the ciphertext letters ordered by frequency.
2. Generate a putative plaintext using this key.
3. Generate a digram matrix from this plaintext.
4. Calculate a score from this digram matrix using the distance sum method.
5. Repeat the following steps:
6a. Make a copy of the digram matrix.
6b. Swap two rows/column at random in this putative digram matrix.
6c. Calculate a score for the putative digram matrix.
6d. If the score improved, save the putative digram matrix as the new best
digram matrix, make the same swap in the key, and save the improved
score as the new best score.
7. The algorithm is done when the score hasn't improved for 2,000 iterations.
"""
# We need the key as a list so we can modify it in-place.
key = self._decryption_key[:]
# Generate an initial digram matrix.
putative_plaintext = self._get_plaintext(key)
digram_matrix = self._get_digram_matrix(putative_plaintext)
best_score = self._score(digram_matrix)
iterations_since_last_improvement = 0
# Loop and swap elements in the key at random until the score hasn't improved
# for 1,000 iterations.
while iterations_since_last_improvement < 2000:
a, b = self._weighted_random_index_pair()
putative_digram_matrix = np.copy(digram_matrix)
self._swap_matrix(putative_digram_matrix, a, b)
score = self._score(putative_digram_matrix)
if score < best_score:
best_score = score
digram_matrix = np.copy(putative_digram_matrix)
key[a], key[b] = key[b], key[a]
iterations_since_last_improvement = 0
else:
iterations_since_last_improvement += 1
self._decryption_key = key[:]
def solve(self, method="random"):
"""Solve the cipher.
Run the solver and save the resulting decryption key.
Parameters
----------
method : str
The method to use when solving, currently "random" or "deterministic".
Raises
------
ValueError
If the passed method is unknown.
"""
if method == "random":
self._solve_random()
elif method == "deterministic":
self._solve_deterministic()
else:
raise ValueError(f"Unknown method {method}")
def plaintext(self):
"""Return a plaintext using the current decryption key.
Returns
-------
plaintext : str
Plaintext from decrypting the ciphertext using the current decryption key.
"""
return self._get_plaintext(self._decryption_key)
def decryption_key(self):
"""Return the current alphabetical decryption key.
Returns
-------
alphabetical_key : str
The current decryption key as a string in alphabetical form.
"""
return common_to_alphabetical_key(self._decryption_key)
def reset(self):
"""Reset the solver to its initial state.
Set the decryption key to its initial state, effectively starting over.
"""
self._decryption_key = self._get_initial_key(self._ciphertext)
Classes
class SimpleSolver (ciphertext)
-
Solver for simple monoalphabetic substitution ciphers.
This solver is based on the paper "A Fast Method for the Cryptanalysis of Substitution Ciphers" by Thomas Jakobsen. The details of the algorithm is described in the solve method docstrings.
The following terminology is used:
"ciphertext" : The encrypted text we want to solve to get a plaintext. "plaintext" : The decrypted plaintext using a certain decryption key. "common key" : Key used to generate a plaintext, ordered by most common letter. "alphabetical key" : Decryption key, but ordered alphabetically. "decryption key" : Assumed to mean a common key if nothing else is specified. "digram" : A pair of letters, e.g. "aa", "cd" etc. "digram matrix" : An (n x n) matrix, where n is the length of the used alphabet, created from a given text, where the frequency of each digram relative to the text length is saved to the corresponding index pair, e.g. (0, 0) for "aa", (0, 1) for "ab" etc. "distance sum" : The method used to score solutions, see ._score() for details.
Create new solver.
Creates a new cipher solver from an initial ciphertext.
Parameters
ciphertext
:str
- The ciphertext to solve.
Raises
ValueError
- If the passed ciphertext is not a string. If the passed ciphertext is empty.
Expand source code
class SimpleSolver: """Solver for simple monoalphabetic substitution ciphers. This solver is based on the paper "A Fast Method for the Cryptanalysis of Substitution Ciphers" by Thomas Jakobsen. The details of the algorithm is described in the solve method docstrings. The following terminology is used: "ciphertext" : The encrypted text we want to solve to get a plaintext. "plaintext" : The decrypted plaintext using a certain decryption key. "common key" : Key used to generate a plaintext, ordered by most common letter. "alphabetical key" : Decryption key, but ordered alphabetically. "decryption key" : Assumed to mean a common key if nothing else is specified. "digram" : A pair of letters, e.g. "aa", "cd" etc. "digram matrix" : An (n x n) matrix, where n is the length of the used alphabet, created from a given text, where the frequency of each digram relative to the text length is saved to the corresponding index pair, e.g. (0, 0) for "aa", (0, 1) for "ab" etc. "distance sum" : The method used to score solutions, see ._score() for details. """ def __init__(self, ciphertext): """Create new solver. Creates a new cipher solver from an initial ciphertext. Parameters ---------- ciphertext : str The ciphertext to solve. Raises ------ ValueError If the passed ciphertext is not a string. If the passed ciphertext is empty. """ if not isinstance(ciphertext, str): raise ValueError(f"{ciphertext} is not a string.") if len(ciphertext) < 1: raise ValueError("Ciphertext cannot be empty.") # The decryption key is a list of letters that determines how the ciphertext is # converted to plaintext. The key is equal in length to the English alphabet and # is assumed to be in frequency order. In other words, the first letter is the # letter in the ciphertext that should be translated to an "e", the second # which one should be converted to a "t", and so on. self._decryption_key = self._get_initial_key(ciphertext) self._ciphertext = ciphertext def _get_initial_key(self, ciphertext): """Construct the initial decryption key. The initial decryption key is based on the letter frequencies in the ciphertext, meaning an assumption that the most common letter in the ciphertext translates to the most common letter in the English language, and so on. Any letters not present in the ciphertext will be added alphabetically at the end of the key. Parameters ---------- ciphertext : str The ciphertext to generate an initial decryption key from. Returns ------- decryption_key : list The initial decryption key. Raises ------ ValueError If the passed ciphertext is not a string. If the passed ciphertext is empty. """ if not isinstance(ciphertext, str): raise ValueError(f"{ciphertext} is not a string.") if len(ciphertext) < 1: raise ValueError("Ciphertext cannot be empty.") decryption_key = self._get_common_letters(ciphertext) for c in ascii_lowercase: if c not in decryption_key: decryption_key += c return decryption_key def _get_common_letters(self, text): """Get all unique letters of the passed text, sorted by frequency. Parameters ---------- text : str The text to find most common letters for. Returns ------- common_letters : list The letters of the text ordered by frequency. Raises ------ If the passed text is not a string. If the passed text is empty. """ if not isinstance(text, str): raise ValueError("{text} is not a string.") if len(text) < 1: raise ValueError("Text must not be empty.") c = Counter(text) return [letter[0] for letter in c.most_common() if letter[0] in ascii_lowercase] def _get_digram_matrix(self, text): """Generate digram matrix for the passed text. Parameters ---------- text : str Text to generate digram frequency matrix for. Returns ------- digram_matrix : numpy.array An array of digram frequencies indexed by [first][second] letter. Raises ------ ValueError If the passed text is not a string. If the passed text does not contain at least one digram. """ if not isinstance(text, str): raise ValueError(f"{text} is not a string.") if len(text) < 2: raise ValueError("Text must contain at least one digram.") digram_matrix = np.zeros((STANDARD_ALPHABET_SIZE, STANDARD_ALPHABET_SIZE)) text = text.lower() text_length = len(text) # First, count the number of occurrences of each letter and save to the index # that corresponds to the letter pair based on where it is in the English # alphabet in frequency order. num_digrams = 0 for i in range(0, text_length - 1): char1 = text[i] char2 = text[i + 1] # Only count true digrams, i.e. where both chars are actual letters. if char1 not in ascii_lowercase or char2 not in ascii_lowercase: continue a = ENGLISH_LETTERS_BY_FREQUENCY.index(char1) b = ENGLISH_LETTERS_BY_FREQUENCY.index(char2) digram_matrix[a, b] += 1 num_digrams += 1 # Replace each entry with a percentage of the total number of digrams, to get # the same format as the English digram matrix. rows, columns = digram_matrix.shape for i in range(rows): for j in range(columns): # All digram frequencies are in percentages, so convert it here too. digram_matrix[i, j] = 100 * digram_matrix[i, j] / num_digrams return digram_matrix def _score(self, matrix1, matrix2=DIGRAM_MATRIX_ENGLISH): """Calculate a score for passed digram matrices using the distance sum method. The score is defined as the sum of all the absolute differences between each corresponding element in the two matrices. Parameters ---------- matrix1 : numpy.array The first matrix to use in the comparison. matrix2 : numpy.array The second matrix to use in the comparison. Defaults to English digrams. Returns ------- score : float The distance sum of the two matrices. Raises ------ ValueError If the passed matrices don't have the same number of rows and columns. """ if matrix1.shape != matrix2.shape: raise ValueError("Digram matrices must have the same dimensions") return abs(matrix1 - matrix2).sum() def _swap_matrix(self, matrix, index1, index2): """Swap the matrix rows and columns at the given indices. Parameters ---------- matrix : numpy.array The matrix to modify in-place. index1 : int The first index to swap between. index2 : int The second index to swap between. Raises ------ ValueError If the passed matrix is not square. """ rows, columns = matrix.shape if rows != columns: raise ValueError("Matrix must be square.") # Swap rows: matrix[[index1, index2]] = matrix[[index2, index1]] # Swap columns: matrix[:, [index1, index2]] = matrix[:, [index2, index1]] def _get_plaintext(self, decryption_key): """Return a plaintext using the passed decryption key. Parameters ---------- decryption_key : list The decryption key to use for generating the plaintext. Returns ------- plaintext : str Plaintext from decrypting the ciphertext using the passed decryption key. Raises ------ ValueError If the passed decryption key does not contain all letters of the alphabet. """ if len(set(decryption_key)) != STANDARD_ALPHABET_SIZE: raise ValueError("Key must include all letters of the alphabet.") # The decryption key will be in order of most common first, so we need to # construct a list of indices where to insert each to get an alphabetical key. indices = [ ascii_lowercase.index(letter) for letter in ENGLISH_LETTERS_BY_FREQUENCY ] translation_table = {} for key_letter, index in zip(decryption_key, indices): plain_letter = ascii_lowercase[index] translation_table[key_letter] = plain_letter plaintext = "" for c in self._ciphertext: is_upper = c in ascii_uppercase letter = translation_table.get(c.lower(), c) if is_upper: letter = letter.upper() plaintext += letter return plaintext def _weighted_random_index_pair(self): """Return a random index pair for swapping, weighted by letter frequency. Instead of just picking random indices between zero and the alphabet length, which will suggest very unlikely swaps, the indices are randomised with a weight according to English letter frequency. In other words, low indices (corresponding to common letters) will be suggested more often than high indices (corresponding to uncommon letters). Returns ------- index_pair : list A pair of random indices between zero and the alphabet length. """ return random.sample(RANDOM_INDEX_DISTRIBUTION, 2) def _solve_deterministic(self): """Solve the cipher using predefined, structured digram matrix swaps. This is the algorithm described by Jakobsen. It is based on the insight that swapping rows and columns in a digram matrix is equivalent to swapping the elements at the same indices in the key that was used to generate the plaintext that was used to generate the digram matrix. The algorithm works as follows: 1. Create an initial key that is the ciphertext letters ordered by frequency. 2. Generate a putative plaintext using this key. 3. Generate a digram matrix from this plaintext. 4. Calculate a score from this digram matrix using the distance sum method. 5. Repeat the following steps: 6a. Make a copy of the digram matrix. 6b. Swap rows/elements of this putative digram matrix at index (0, 1), (1, 2), (2, 3) etc. until the last index of the pair reaches the alphabet length. Then swap rows/columns at index (0, 2), (1, 3), (2, 4) etc. until the last index in the pair reaches the alphabet length. The last swap in this nested loop will be (0, 25). 6c. After each swap, calculate a score from the modified digram matrix. 6d. If the score improved, save the modified digram matrix as the new best digram matrix, make the same swap in the key and save it as the new best key, and save the improved score as the new best score. 7. The algorithm is done when all swaps have been made. """ # We need this as a list so we can modify it in-place. key = self._decryption_key[:] # Generate digram matrix from the corresponding plaintext. putative_plaintext = self._get_plaintext(key) digram_matrix = self._get_digram_matrix(putative_plaintext) # Calculate initial score. best_score = self._score(digram_matrix) # Loop and swap rows/columns in digram matrix. for i in range(1, STANDARD_ALPHABET_SIZE): for j in range(STANDARD_ALPHABET_SIZE - i): # Try a potential swap in the digram matrix. d = np.copy(digram_matrix) self._swap_matrix(d, j, j + i) score = self._score(d) if score < best_score: # The score improved, so commit this change in both the digram # matrix and the key. digram_matrix = np.copy(d) key[j], key[j + i] = key[j + i], key[j] best_score = score self._decryption_key = key[:] def _solve_random(self): """Solve the cipher using random key swaps. This is the algorithm described by Jakobsen, but using random key swaps instead of the original structured swaps according to a certain pattern. The algorithm works as follows: 1. Create an initial key that is the ciphertext letters ordered by frequency. 2. Generate a putative plaintext using this key. 3. Generate a digram matrix from this plaintext. 4. Calculate a score from this digram matrix using the distance sum method. 5. Repeat the following steps: 6a. Make a copy of the digram matrix. 6b. Swap two rows/column at random in this putative digram matrix. 6c. Calculate a score for the putative digram matrix. 6d. If the score improved, save the putative digram matrix as the new best digram matrix, make the same swap in the key, and save the improved score as the new best score. 7. The algorithm is done when the score hasn't improved for 2,000 iterations. """ # We need the key as a list so we can modify it in-place. key = self._decryption_key[:] # Generate an initial digram matrix. putative_plaintext = self._get_plaintext(key) digram_matrix = self._get_digram_matrix(putative_plaintext) best_score = self._score(digram_matrix) iterations_since_last_improvement = 0 # Loop and swap elements in the key at random until the score hasn't improved # for 1,000 iterations. while iterations_since_last_improvement < 2000: a, b = self._weighted_random_index_pair() putative_digram_matrix = np.copy(digram_matrix) self._swap_matrix(putative_digram_matrix, a, b) score = self._score(putative_digram_matrix) if score < best_score: best_score = score digram_matrix = np.copy(putative_digram_matrix) key[a], key[b] = key[b], key[a] iterations_since_last_improvement = 0 else: iterations_since_last_improvement += 1 self._decryption_key = key[:] def solve(self, method="random"): """Solve the cipher. Run the solver and save the resulting decryption key. Parameters ---------- method : str The method to use when solving, currently "random" or "deterministic". Raises ------ ValueError If the passed method is unknown. """ if method == "random": self._solve_random() elif method == "deterministic": self._solve_deterministic() else: raise ValueError(f"Unknown method {method}") def plaintext(self): """Return a plaintext using the current decryption key. Returns ------- plaintext : str Plaintext from decrypting the ciphertext using the current decryption key. """ return self._get_plaintext(self._decryption_key) def decryption_key(self): """Return the current alphabetical decryption key. Returns ------- alphabetical_key : str The current decryption key as a string in alphabetical form. """ return common_to_alphabetical_key(self._decryption_key) def reset(self): """Reset the solver to its initial state. Set the decryption key to its initial state, effectively starting over. """ self._decryption_key = self._get_initial_key(self._ciphertext)
Methods
def solve(self, method='random')
-
Solve the cipher.
Run the solver and save the resulting decryption key.
Parameters
method
:str
- The method to use when solving, currently "random" or "deterministic".
Raises
ValueError
- If the passed method is unknown.
Expand source code
def solve(self, method="random"): """Solve the cipher. Run the solver and save the resulting decryption key. Parameters ---------- method : str The method to use when solving, currently "random" or "deterministic". Raises ------ ValueError If the passed method is unknown. """ if method == "random": self._solve_random() elif method == "deterministic": self._solve_deterministic() else: raise ValueError(f"Unknown method {method}")
def plaintext(self)
-
Return a plaintext using the current decryption key.
Returns
plaintext
:str
- Plaintext from decrypting the ciphertext using the current decryption key.
Expand source code
def plaintext(self): """Return a plaintext using the current decryption key. Returns ------- plaintext : str Plaintext from decrypting the ciphertext using the current decryption key. """ return self._get_plaintext(self._decryption_key)
def decryption_key(self)
-
Return the current alphabetical decryption key.
Returns
alphabetical_key
:str
- The current decryption key as a string in alphabetical form.
Expand source code
def decryption_key(self): """Return the current alphabetical decryption key. Returns ------- alphabetical_key : str The current decryption key as a string in alphabetical form. """ return common_to_alphabetical_key(self._decryption_key)
def reset(self)
-
Reset the solver to its initial state.
Set the decryption key to its initial state, effectively starting over.
Expand source code
def reset(self): """Reset the solver to its initial state. Set the decryption key to its initial state, effectively starting over. """ self._decryption_key = self._get_initial_key(self._ciphertext)