X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=szyfrow%2Faffine.py;h=955363782ce90bfbc2e6ca31b335d780c1013aea;hb=refs%2Fheads%2Fmain;hp=a5647be20e3b30abd5573ac087eac7da1f1f5d2f;hpb=a870050db6bc974b1bb0d132001750b6624fb43f;p=szyfrow.git diff --git a/szyfrow/affine.py b/szyfrow/affine.py index a5647be..9553637 100644 --- a/szyfrow/affine.py +++ b/szyfrow/affine.py @@ -1,7 +1,23 @@ -from support.utilities import * -from support.language_models import * -from logger import logger +"""Enciphering and deciphering using the [affine cipher](https://en.wikipedia.org/wiki/Affine_cipher). +Also attempts to break messages that use an affine cipher. +The affine cipher operates one letter at a time. It converts each letter to a +number, then enciphers that number using a multiplier and a number. The result +is taken mod 26 and converted back into a letter. + +For a multiplier _m_ and adder _a_, a letter converted to number _x_ is +enciphered as _E(x)_ = (_m_ _x_ + _a_) mod 26. Deciphering uses the modular +inverse of _m_, _m_⁻¹, so that _D(x)_ = _m_⁻¹ (_x_ - _a_) mod 26. + +If `one_based` is `True`, the conversion between letters and numbers maps +'a' → 1, 'b' → 2, … 'z'→ 26 and the `mod` function is adjusted to keep +numbers in this range during enciphering and deciphering. If `one_based` is +`False`, the conversion maps 'a' → 0, 'b' → 1, … 'z'→ 25 and `mod` behaves +normally. +""" + +from szyfrow.support.utilities import * +from szyfrow.support.language_models import * modular_division_table = { (multiplier, (multiplier * plaintext) % 26): plaintext @@ -11,7 +27,10 @@ modular_division_table = { def affine_encipher_letter(accented_letter, multiplier=1, adder=0, one_based=True): - """Encipher a letter, given a multiplier and adder + """Encipher a letter, given a multiplier and adder. + + Accented version of latin letters (such as é and ö) are converted to their + non-accented versions before encryption. >>> cat(affine_encipher_letter(l, 3, 5, True) \ for l in string.ascii_letters) @@ -46,9 +65,6 @@ def affine_decipher_letter(letter, multiplier=1, adder=0, one_based=True): if letter in string.ascii_letters: cipher_number = pos(letter) if one_based: cipher_number += 1 - # plaintext_number = ( - # modular_division_table[multiplier] - # [(cipher_number - adder) % 26]) plaintext_number = ( modular_division_table[multiplier, (cipher_number - adder) % 26] ) @@ -85,7 +101,11 @@ def affine_decipher(message, multiplier=1, adder=0, one_based=True): def affine_break(message, fitness=Pletters): - """Breaks an affine cipher using frequency analysis + """Breaks an affine cipher using frequency analysis. + + It tries all possible combinations of multiplier, adder, and one_based, + scores the fitness of the text decipherd with each combination, and returns + the key that produces the most fit deciphered text. >>> affine_break('lmyfu bkuusd dyfaxw claol psfaom jfasd snsfg jfaoe ls ' \ 'omytd jlaxe mh jm bfmibj umis hfsul axubafkjamx. ls kffkxwsd jls ' \ @@ -106,18 +126,10 @@ def affine_break(message, fitness=Pletters): plaintext = affine_decipher(sanitised_message, multiplier, adder, one_based) fit = fitness(plaintext) - logger.debug('Affine break attempt using key {0}x+{1} ({2}) ' - 'gives fit of {3} and decrypt starting: {4}'. - format(multiplier, adder, one_based, fit, - plaintext[:50])) if fit > best_fit: best_fit = fit best_multiplier = multiplier best_adder = adder best_one_based = one_based - logger.info('Affine break best fit with key {0}x+{1} ({2}) gives fit of ' - '{3} and decrypt starting: {4}'.format( - best_multiplier, best_adder, best_one_based, best_fit, - affine_decipher(sanitised_message, best_multiplier, - best_adder, best_one_based)[:50])) + return (best_multiplier, best_adder, best_one_based), best_fit