-from support.utilities import *
-from support.language_models import *
-from cipher.keyword_cipher import KeywordWrapAlphabet, keyword_cipher_alphabet_of
-from cipher.polybius import polybius_grid
+"""Enciphering and deciphering using the [Playfair cipher](https://en.wikipedia.org/wiki/Playfair_cipher).
+Also attempts to break messages that use a Playfair cipher.
+"""
+from szyfrow.support.utilities import *
+from szyfrow.support.language_models import *
+from szyfrow.keyword_cipher import KeywordWrapAlphabet, keyword_cipher_alphabet_of
+from szyfrow.polybius import polybius_grid
import multiprocessing
-from logger import logger
-
def playfair_wrap(n, lowest, highest):
+ """Ensures _n_ is between _lowest_ and _highest_ (inclusive at both ends).
+ """
skip = highest - lowest + 1
while n > highest or n < lowest:
if n > highest:
return n
def playfair_encipher_bigram(ab, grid, padding_letter='x'):
+ """Encipher a single pair of letters using the Playfair method."""
a, b = ab
max_row = max(c[0] for c in grid.values())
max_col = max(c[1] for c in grid.values())
return c + d
def playfair_decipher_bigram(ab, grid, padding_letter='x'):
+ """Decipher a single pair of letters using the Playfair method."""
a, b = ab
max_row = max(c[0] for c in grid.values())
max_col = max(c[1] for c in grid.values())
return c + d
def playfair_bigrams(text, padding_letter='x', padding_replaces_repeat=True):
+ """Find all the bigrams in a method to be enciphered.
+
+ If both letters are the same, the `padding_letter` is used instead of the
+ second letter. If `padding_replaces_repeat` is `True`, the `padding_letter`
+ replaces the second letter of the bigram. If `padding_replaces_repeat` is
+ `False`, the second letter of this bigram becomes the first letter of the
+ next, effectively lengthening the message by one letter.
+ """
i = 0
bigrams = []
while i < len(text):
def playfair_encipher(message, keyword, padding_letter='x',
padding_replaces_repeat=False, letters_to_merge=None,
wrap_alphabet=KeywordWrapAlphabet.from_a):
+ """Encipher a message using the Playfair cipher."""
column_order = list(range(5))
row_order = list(range(5))
if letters_to_merge is None:
grid = polybius_grid(keyword, column_order, row_order,
letters_to_merge=letters_to_merge,
wrap_alphabet=wrap_alphabet)
- message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter,
- padding_replaces_repeat=padding_replaces_repeat)
+ message_bigrams = playfair_bigrams(
+ sanitise(message), padding_letter=padding_letter,
+ padding_replaces_repeat=padding_replaces_repeat)
ciphertext_bigrams = [playfair_encipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]
return cat(ciphertext_bigrams)
def playfair_decipher(message, keyword, padding_letter='x',
padding_replaces_repeat=False, letters_to_merge=None,
wrap_alphabet=KeywordWrapAlphabet.from_a):
+ """Decipher a message using the Playfair cipher."""
column_order = list(range(5))
row_order = list(range(5))
if letters_to_merge is None:
grid = polybius_grid(keyword, column_order, row_order,
letters_to_merge=letters_to_merge,
wrap_alphabet=wrap_alphabet)
- message_bigrams = playfair_bigrams(sanitise(message), padding_letter=padding_letter,
- padding_replaces_repeat=padding_replaces_repeat)
+ message_bigrams = playfair_bigrams(
+ sanitise(message), padding_letter=padding_letter,
+ padding_replaces_repeat=padding_replaces_repeat)
plaintext_bigrams = [playfair_decipher_bigram(b, grid, padding_letter=padding_letter) for b in message_bigrams]
return cat(plaintext_bigrams)
-def playfair_break_mp(message,
+def playfair_break(message,
letters_to_merge=None, padding_letter='x',
wordlist=keywords, fitness=Pletters,
number_of_solutions=1, chunksize=500):
+ """Break a message enciphered using the Playfair cipher, using a dictionary
+ of keywords and frequency analysis.
+
+ If `wordlist` is not specified, use
+ [`szyfrow.support.langauge_models.keywords`](support/language_models.html#szyfrow.support.language_models.keywords).
+ """
+
if letters_to_merge is None:
letters_to_merge = {'j': 'i'}
fit = fitness(plaintext)
else:
fit = float('-inf')
- logger.debug('Playfair break attempt using key {0} (wrap={1}, merging {2}, '
- 'pad replaces={3}), '
- 'gives fit of {4} and decrypt starting: '
- '{5}'.format(keyword, wrap, letters_to_merge, pad_replace,
- fit, sanitise(plaintext)[:50]))
return (keyword, wrap, letters_to_merge, padding_letter, pad_replace), fit
def playfair_simulated_annealing_break(message, workers=10,
plain_alphabet=None,
cipher_alphabet=None,
fitness=Pletters, chunksize=1):
+ """Break a message enciphered using the Playfair cipher, using simulated
+ annealing to determine the keyword. This function just sets up a stable
+ of workers who do the actual work, implemented as
+ `szyfrow.playfair.playfair_simulated_annealing_break_worker`.
+
+ See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
+ for detail on how this works.
+ """
worker_args = []
ciphertext = sanitise(message)
for i in range(workers):
def playfair_simulated_annealing_break_worker(message, plain_alphabet, cipher_alphabet,
t0, max_iterations, fitness):
+ """One thread of a simulated annealing run.
+ See a [post on simulated annealing](https://work.njae.me.uk/2019/07/08/simulated-annealing-and-breaking-substitution-ciphers/)
+ for detail on how this works.
+ """
def swap(letters, i, j):
if i > j:
i, j = j, i
# print('exception triggered: new_fit {}, current_fit {}, temp {}'.format(new_fitness, current_fitness, temperature))
sa_chance = 0
if (new_fitness > current_fitness or random.random() < sa_chance):
- # logger.debug('Simulated annealing: iteration {}, temperature {}, '
- # 'current alphabet {}, current_fitness {}, '
- # 'best_plaintext {}'.format(i, temperature, current_alphabet,
- # current_fitness, best_plaintext[:50]))
-
- # logger.debug('new_fit {}, current_fit {}, temp {}, sa_chance {}'.format(new_fitness, current_fitness, temperature, sa_chance))
current_fitness = new_fitness
current_alphabet = alphabet
current_wrap = wrap
best_padding_letter = current_padding_letter
best_fitness = current_fitness
best_plaintext = plaintext
- if i % 500 == 0:
- logger.debug('Simulated annealing: iteration {}, temperature {}, '
- 'current alphabet {}, current_fitness {}, '
- 'best_plaintext {}'.format(i, temperature, current_alphabet,
- current_fitness, plaintext[:50]))
temperature = max(temperature - dt, 0.001)
return { 'alphabet': best_alphabet