Python パスワード クラッキング ガイド: 15~19

ライセンス: CC BY-NC-SA 4.0

翻訳者:フライング ドラゴン

この記事は[OpenDocCN 飽和翻訳プロジェクト]から提供されたもので、翻訳後編集 (MTPE)プロセスを使用して効率を可能な限り向上させます。

SBを収穫する者はSBによって神にされ、SBを覚醒させようとする者はSBから見ればSBである。——SB第三法則

15. アフィン暗号の解読

原文: https://inventwithpython.com/cracking/chapter15.html

暗号解析は、文明が数学、統計学、言語学などのいくつかの分野で十分に洗練された学術レベルに達するまで発明できませんでした。

—サイモン・シンガー、『The Book of Codes』

画像

第 14 章では、アフィン暗号は数千のキーに制限されており、簡単に総当たり攻撃が可能であることを学びました。この章では、アフィン暗号で暗号化された情報を解読するプログラムの作成方法を学習します。

この章で取り上げるトピック

  • 指数演算子 ( **)

  • continue声明

Affine パスワード クラッカーのソース コード

[ファイル] -> [新規ファイル]を選択して、新しいファイル エディタ ウィンドウを開きます。ファイルエディタに次のコードを入力し、名前を付けて保存しますaffineHacker.pymyMessage変数の文字列を手動で入力するのは少し難しい場合があるため、ファイルからコピーして貼り付けることで時間を節約できますwww.nostarch.com/crackingcodesおよび が同じディレクトリにあることaffineHacker.pyを確認してください。dictionary.txtpyperclip.pyaffinicipher.pydetectEnglish.pycryptomath.pyaffinihacker.py

affineHacker.py

# Affine Cipher Hacker
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip, affineCipher, detectEnglish, cryptomath

SILENT_MODE = False

def main():
    # You might want to copy & paste this text from the source code at
    # https://www.nostarch.com/crackingcodes/.
    myMessage = """5QG9ol3La6QI93!xQxaia6faQL9QdaQG1!!axQARLa!!A
          uaRLQADQALQG93!xQxaGaAfaQ1QX3o1RQARL9Qda!AafARuQLX1LQALQI1
          iQX3o1RN"Q-5!1RQP36ARu"""

    hackedMessage = hackAffine(myMessage)

    if hackedMessage != None:
        # The plaintext is displayed on the screen. For the convenience of
        # the user, we copy the text of the code to the clipboard:
        print('Copying hacked message to clipboard:')
        print(hackedMessage)
        pyperclip.copy(hackedMessage)
    else:
        print('Failed to hack encryption.')


def hackAffine(message):
    print('Hacking...')

    # Python programs can be stopped at any time by pressing Ctrl-C (on
    # Windows) or Ctrl-D (on macOS and Linux):
    print('(Press Ctrl-C or Ctrl-D to quit at any time.)')

    # Brute-force by looping through every possible key:
    for key in range(len(affineCipher.SYMBOLS) ** 2):
        keyA = affineCipher.getKeyParts(key)[0]
        if cryptomath.gcd(keyA, len(affineCipher.SYMBOLS)) != 1:
            continue

        decryptedText = affineCipher.decryptMessage(key, message)
        if not SILENT_MODE:
            print('Tried Key %s... (%s)' % (key, decryptedText[:40]))

        if detectEnglish.isEnglish(decryptedText):
            # Check with the user if the decrypted key has been found:
            print()
            print('Possible encryption hack:')
            print('Key: %s' % (key))
            print('Decrypted message: ' + decryptedText[:200])
            print()
            print('Enter D for done, or just press Enter to continue
                  hacking:')
            response = input('> ')

            if response.strip().upper().startswith('D'):
                return decryptedText
    return None


# If affineHacker.py is run (instead of imported as a module), call
# the main() function:
if __name__ == '__main__':
    main()

Affine パスワード クラッカーの実行例

ファイル エディタで , を押してプログラムF5を実行すると、出力は次のようになります。affineHacker.py

Hacking...
(Press Ctrl-C or Ctrl-D to quit at any time.)
Tried Key 95... (U&'<3dJ^Gjx'-3^MS'Sj0jxuj'G3'%j'<mMMjS'g)
Tried Key 96... (T%&;2cI]Fiw&,2]LR&Ri/iwti&F2&$i&;lLLiR&f)
Tried Key 97... (S$%:1bH\Ehv%+1\KQ%Qh.hvsh%E1%#h%:kKKhQ%e)
--snip--
Tried Key 2190... (?^=!-+.32#0=5-3*"="#1#04#=2-= #=!~**#"=')
Tried Key 2191... (' ^BNLOTSDQ^VNTKC^CDRDQUD^SN^AD^[email protected]^H)
Tried Key 2192... ("A computer would deserve to be called i)
Possible encryption hack:
Key: 2192
Decrypted message: "A computer would deserve to be called intelligent if it
could deceive a human into believing that it was human." -Alan Turing
Enter D for done, or just press Enter to continue hacking:
> d
Copying hacked message to clipboard:
"A computer would deserve to be called intelligent if it could deceive a human
into believing that it was human." –Alan Turing

Affine パスワード クラッカーがどのように機能するかを詳しく見てみましょう。

モジュール、定数、main()関数を設定する

Affine Cipher Cracker は、使用するコードのほとんどをすでに記述しているため、長さは 60 行です。4 行目は、前の章で作成したモジュールをインポートします。

# Affine Cipher Hacker
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip, affineCipher, detectEnglish, cryptomath

SILENT_MODE = False

Affine 暗号クラッカーを実行すると、考えられるすべての復号化が実行され、大量の出力が生成されることがわかります。ただし、この出力をすべて出力するとプログラムの速度が低下します。プログラムの速度を上げたい場合は、 6 行目にSILENT_MODE変数を設定してTrue、これらすべてのメッセージの出力を停止します。

main()次に、関数を設定します。

def main():
    # You might want to copy & paste this text from the source code at
    # https://www.nostarch.com/crackingcodes/.
    myMessage = """5QG9ol3La6QI93!xQxaia6faQL9QdaQG1!!axQARLa!!A
          uaRLQADQALQG93!xQxaGaAfaQ1QX3o1RQARL9Qda!AafARuQLX1LQALQI1
          iQX3o1RN"Q-5!1RQP36ARu"""

    hackedMessage = hackAffine(myMessage)

攻撃された暗号文は 11 行目に文字列として保存され、この文字列は次のセクションで説明する関数myMessageに渡されます。hackAffine()この呼び出しの戻り値は、暗号文が解読された場合は元のメッセージの文字列、解読が失敗した場合は値ですNone

15 行目から 22 行目のコードは、次hackedMessageのように設定されているかどうかを確認しますNone

    if hackedMessage != None:
        # The plaintext is displayed on the screen. For the convenience of
        # the user, we copy the text of the code to the clipboard:
        print('Copying hacked message to clipboard:')
        print(hackedMessage)
        pyperclip.copy(hackedMessage)
    else:
        print('Failed to hack encryption.')

hackedMessageそうでない場合None、メッセージは 19 行目で画面に出力され、20 行目でクリップボードにコピーされます。それ以外の場合、プログラムは単にユーザーにフィードバックを出力し、暗号文を解読できないことを伝えます。関数がどのように機能するかを詳しく見てみましょうhackAffine()

hackAffine()関数

hackAffine()この関数は 25 行目から始まり、復号化コードが含まれています。まず、ユーザーにいくつかの指示を出力します。

def hackAffine(message):
    print('Hacking...')

    # Python programs can be stopped at any time by pressing Ctrl-C (on
    # Windows) or Ctrl-D (on macOS and Linux):
    print('(Press Ctrl-C or Ctrl-D to quit at any time.)')

復号化プロセスには時間がかかる場合があるため、ユーザーがプログラムを早めに終了したい場合は、ctrl+C(Windows の場合) またはctrl+D(macOS および Linux の場合) を押します。

コードの残りの部分に進む前に、指数演算子を理解する必要があります。

指数演算子

+アフィン暗号クラッカーを理解するために (基本的な、-*/および演算子に加えて//)知っておく必要がある便利な数学演算子の 1 つは、べき乗演算子( **) です。指数演算子は、数値を別の数値で累乗します。たとえば、Python では、2 の 5 乗は です2 ** 5これは 2 回自体を 5 回回したものに相当します2 * 2 * 2 * 2 * 22 ** 5と は両方とも2 * 2 * 2 * 2 * 2整数に評価されます32

対話型シェルに次のように入力して、**演算子の動作を確認します。

>>> 5 ** 2
25
>>> 2 ** 5
32
>>> 123 ** 10
792594609605189126649

それ自体を乗算すると と等しいため、式は5 ** 2と評価されます同様に、それ自体を 5 回乗算した結果はであるため、を返します255252 ** 532232

ソース コードに戻って、**プログラム内で演算子が何を行うかを見てみましょう。

可能なキーの総数を計算します。

33 行目では**、演算子を使用して、可能なキーの総数をカウントします。

    # Brute-force by looping through every possible key:
    for key in range(len(affineCipher.SYMBOLS) ** 2):
        keyA = affineCipher.getKeyParts(key)[0]

len(affineCipher.SYMBOLS)キー A には最大でも整数があり、キー B には最大でもlen(affineCipher.SYMBOLS)整数があることがわかっています。考えられるすべてのキーを取得するには、これらの値を乗算します。同じ値を単独で乗算しているため、式で演算子をlen(affineCipher.SYMBOLS) ** 2使用できます。**

34 行目では、整数キーを 2 つの整数に分割するためにaffinicipher.py使用する関数を呼び出します。getKeyParts()この例では、関数を使用してテストしているキーの一部を取得します。この関数呼び出しの戻り値は、キー A 用とキー B 用の 2 つの整数のタプルであることを思い出してください。行 34 では、タプルの最初の整数を関数呼び出しの後に[0]配置して格納しますhackAffine()keyA

たとえば、affineCipher.getKeyParts(key)[0]タプルとインデックス(42, 22)[0]が評価され、インデックスは(42, 22)[0]タプルのインデックスの0値に評価されます42これはキー値の一部を取得して変数に格納するだけですkeyAキー A が有効かどうかを計算するのにキー B は必要ないため、キー B の部分 (返されたタプルの 2 番目の値) は無視されます。行 35 と行 36 では、keyAそれがアフィン暗号の有効なキー A であるかどうかを確認し、そうでない場合は、プログラムは次のキーに進んで試行します。実行がどのようにループの先頭に戻るかを理解するには、continueステートメントを確認する必要があります。

continue声明

continueステートメントcontinueでは、パラメータを使用せずにキーワードのみを使用します。whileorループfor内でステートメントを使用しますcontinueステートメントが実行されるとcontinue、プログラムの実行はすぐに次の反復のループの先頭にジャンプします。これは、プログラムの実行がループ ブロックの終わりに達したときにも発生します。ただし、continueステートメントにより、プログラムの実行はループの終わりに到達する前にループの先頭に戻ります。

対話型シェルに次のように入力します。

>>> for i in range(3):
...   print(i)
...   print('Hello!')
...
0
Hello!
1
Hello!
2
Hello!

forrangeオブジェクトをループすると、iの値は から0までのすべての整数になります3が、 は含まれません0各反復で、print('Hello!')関数呼び出しが画面に表示されますHello!

现在对比一下下一个例子中的for循环,除了在print('Hello!')行之前有一个continue语句之外,下一个例子与上一个例子相同。

>>> for i in range(3):
...   print(i)
...   continue
...   print('Hello!')
...
0
1
2

请注意,Hello!永远不会被打印,因为continue语句导致程序执行跳回到下一次迭代的for循环的起点,并且执行永远不会到达print('Hello!')行。

一个continue语句通常被放在一个if语句的块中,以便在某些条件下,在循环的开始处继续执行。让我们回到我们的代码,看看它是如何使用continue语句根据使用的密钥跳过执行的。

使用continue跳过代码

在源代码中,第 35 行使用cryptomath模块中的gcd()函数来确定密钥 A 对于符号集大小是否互质:

        if cryptomath.gcd(keyA, len(affineCipher.SYMBOLS)) != 1:
            continue

回想一下,如果两个数的最大公约数(GCD)是 1,那么这两个数就是互质的。如果密钥 A 和符号集大小不是互质的,则第 35 行上的条件是True并且执行第 36 行上的continue语句。这会导致程序执行跳回到下一次迭代的循环起点。结果,如果密钥无效,程序跳过对第 38 行的decryptMessage()的调用,继续尝试其他密钥,直到找到正确的密钥。

当程序找到正确的密钥时,通过用第 38 行的密钥调用decryptMessage()来解密消息:

        decryptedText = affineCipher.decryptMessage(key, message)
        if not SILENT_MODE:
            print('Tried Key %s... (%s)' % (key, decryptedText[:40]))

如果SILENT_MODE被设置为False,则Tried Key消息被打印在屏幕上,但是如果它被设置为True,则跳过第 40 行上的print()调用。

接下来,第 42 行使用来自detectEnglish模块的isEnglish()函数来检查解密的消息是否被识别为英语:

        if detectEnglish.isEnglish(decryptedText):
            # Check with the user if the decrypted key has been found:
            print()
            print('Possible encryption hack:')
            print('Key: %s' % (key))
            print('Decrypted message: ' + decryptedText[:200])
            print()

間違った復号化キーが使用された場合、復号化されたメッセージはランダムな文字のように見え、isEnglish()返されますFalseisEnglish()ただし、復号化されたメッセージが (関数の基準に従って) 読める英語であると認識された場合、プログラムはそれをユーザーに表示します。

isEnglish()正しいキーが見つからない場合でも、関数がテキストを誤って英語として認識する可能性があるため、英語として認識される復号化されたメッセージを示します。これが確かに正しい復号化であるとユーザーが判断した場合は、 を入力しDて押すことができますEnter

            print('Enter D for done, or just press Enter to continue
                  hacking:')
            response = input('> ')

            if response.strip().upper().startswith('D'):
                return decryptedText

それ以外の場合、ユーザーは単にキーを押すだけで、呼び出し回车键から空の文字列が返され、関数はさらにキーの押下を試行し続けます。input()hackAffine()

54 行目の先頭のインデントからわかるように、この行は33 行目のforループが完了した後に実行されます。

    return None

forループが終了して 54 行目まで到達した場合は、考えられるすべての復号化キーを調べましたが、正しいキーが見つかりませんでした。この時点で、hackAffine()関数はNone暗号文の解読に成功しなかったことを示す値を返します。

プログラムが正しいキーを見つけた場合、実行は 54 行目に到達せずに 53 行目の関数から戻ります。

呼び出しmain()関数

affineHacker.pyプログラムとして実行する場合、特殊変数はではなく__name__文字列に設定されますこの場合、関数を呼び出します。'__main__''affineHacker'main()

# If affineHacker.py is run (instead of imported as a module), call
# the main() function:
if __name__ == '__main__':
    main()

これでアフィン暗号解読プログラムは終了です。

要約する

新しいハッキング手法を紹介していないため、この章はかなり短いです。ご覧のとおり、使用可能なキーの数が数千しかない限り、コンピューターがすべての使用可能なキーをブルート フォースし、関数を使用して正しいキーを検索するのにそれほど時間はかかりませんisEnglish()

**ある数値を別の数値で累乗するべき乗演算子 ( ) について学習しました。continueまた、ステートメントを使用して、実行がブロックの終わりに達するまで待機するのではなく、プログラムの実行をループの先頭に戻す方法も学びました。

便利なことに、私たちはアフィン暗号をハッキングするためのコードを 、 affineCipher.pydetectEnglish.pycryptomath.pyたくさん書いてきました。関数のトリックは、プログラム内でコードを再利用するのに役立ちます。

第 16 章では、コンピュータが総当たり攻撃を行うことが不可能な単純な置換暗号について学びます。この暗号に使用できる鍵の数は数兆を超えます。ラップトップがこれらのキーの一部を通過することは私たちが生きている間に不可能であるため、パスワードはブルートフォース攻撃の影響を受けなくなります。

練習問題

練習問題の答えは、www.nostarch.com/crackingcodesこの本の Web サイトで見つけることができます。

  1. 2 ** 5理由を評価しますか?

  2. 6 ** 2理由を評価しますか?

  3. 以下のコードは何を出力しますか?

    for i in range(5):
        if i == 2:
            continue
        print(i)
    
  4. 別のプログラムが実行された場合、import affineHackerその関数は呼び出されますか?affineHacker.pymain()

16、簡単なパスワードの置き換えを実現するためのプログラミング

原文: https://inventwithpython.com/cracking/chapter16.html

「インターネットは人類が発明した中で最も自由なツールであり、監視のための最良のツールです。どちらでもありません。両方です。」

—ジョン・ペリー・バーロウ、電子フロンティア財団共同創設者

画像

第 15 章では、アフィン暗号には約 1000 個の可能なキーがあるが、コンピューターはそれらすべてを簡単に解読できることを学びました。コンピューターが総当たり攻撃できないほど多くの可能性のあるキーを備えた暗号が必要です。

単純置換暗号は、使用可能なキーの数が多いため、ブルート フォース攻撃に対して効果的な暗号の 1 つです。たとえコンピュータが 1 秒間に 1 兆個のキーを試行できたとしても、それぞれのキーを試行するには 1,200 万年かかることになります。この章では、単純な置換暗号を実装するプログラムを作成し、いくつかの便利な Python 関数と文字列メソッドを学習します。

この章で取り上げるトピック

  • sort()リストメソッド

  • 文字列内の重複文字を削除する

  • ラッパー関数

  • isupper()そしてislower()文字列メソッド

単純な置換暗号の仕組み

単純な置換暗号を実装するには、ランダムな文字を選択し、各文字を 1 回だけ使用してアルファベットの各文字を暗号化します。単純な置換暗号のキーは常に 26 文字のランダムなシーケンスです。単純な置換暗号の場合、考えられる鍵の順序は 403,291,461,126,605,635,584,000,000 通りあります。鍵がたくさん!さらに、その数が非常に多いため、総当たり攻撃は不可能です。(この数値がどのように計算されたかについては、 に進んでくださいwww.nostarch.com/crackingcodes。)

まずは紙とペンを使って簡単な置換コードを試してください。この例では、キー を使用してメッセージ「夜明けの攻撃」を暗号化しますVJZBGNFEPLITMXDWKQUCRYAHSOまず、図 16-1 に示すように、アルファベットの文字と各文字の下に対応するキーを書き出します。

画像

図 16-1: キーの例の暗号化アルファベット

メッセージを暗号化するには、平文の先頭行の文字を見つけて、それを下の行の文字に置き換えます。Aへの暗号化VTへの暗号化CCへの暗号化Zなど。したがって、このメッセージはAttack at dawn.として暗号化されますVccvzi vc bvax.

暗号化されたメッセージを復号するには、暗号文の最下行の文字を見つけて、最上行の対応する文字に置き換えます。Vに復号化されACに復号化されTZに復号C化されます。

一番下の行は移動されますが、アルファベット順は維持されるシーザー暗号とは異なり、単純な置換暗号では、一番下の行が完全にシャッフルされます。これにより、より多くの可能なキーが得られ、単純な置換暗号を使用することの大きな利点となります。欠点は、キーの長さが 26 文字であり、覚えにくいことです。キーを書き留める必要がある場合がありますが、その場合は、他の人に読まれないように注意してください。

単純な置き換え暗号プログラムのソース コード

选择文件 -> 新文件,打开新文件编辑器窗口。在文件编辑器中输入以下代码,保存为simpleSubCipher.py。确保将pyperclip.py文件放在与simpleSubCipher.py文件相同的目录中。按F5运行程序。

简单子
Cipher.py

# Simple Substitution Cipher
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip, sys, random


LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
    myMessage = 'If a man is offered a fact which goes against his
          instincts, he will scrutinize it closely, and unless the evidence
          is overwhelming, he will refuse to believe it. If, on the other
          hand, he is offered something which affords a reason for acting
          in accordance to his instincts, he will accept it even on the
          slightest evidence. The origin of myths is explained in this way.
          -Bertrand Russell'
    myKey = 'LFWOAYUISVKMNXPBDCRJTQEGHZ'
    myMode = 'encrypt' # Set to 'encrypt' or 'decrypt'.

    if keyIsValid(myKey):
        sys.exit('There is an error in the key or symbol set.')
    if myMode == 'encrypt':
        translated = encryptMessage(myKey, myMessage)
    elif myMode == 'decrypt':
        translated = decryptMessage(myKey, myMessage)
    print('Using key %s' % (myKey))
    print('The %sed message is:' % (myMode))
    print(translated)
    pyperclip.copy(translated)
    print()
    print('This message has been copied to the clipboard.')


def keyIsValid(key):
    keyList = list(key)
    lettersList = list(LETTERS)
    keyList.sort()
    lettersList.sort()

    return keyList == lettersList


def encryptMessage(key, message):
    return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
    return translateMessage(key, message, 'decrypt')


def translateMessage(key, message, mode):
    translated = ''
    charsA = LETTERS
    charsB = key
    if mode == 'decrypt':
        # For decrypting, we can use the same code as encrypting. We
        # just need to swap where the key and LETTERS strings are used.
        charsA, charsB = charsB, charsA

    # Loop through each symbol in the message:
    for symbol in message:
        if symbol.upper() in charsA:
            # Encrypt/decrypt the symbol:
            symIndex = charsA.find(symbol.upper())
            if symbol.isupper():
                translated += charsB[symIndex].upper()
            else:
                translated += charsB[symIndex].lower()
        else:
            # Symbol is not in LETTERS; just add it:
            translated += symbol

    return translated


def getRandomKey():
    key = list(LETTERS)
    random.shuffle(key)
    return ''.join(key)


if __name__ == '__main__':
    main()

简单替换密码程序的运行示例

当您运行simpleSubCipher.py程序时,加密的输出应该如下所示:

Using key LFWOAYUISVKMNXPBDCRJTQEGHZ
The encrypted message is:
Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr sxrjsxwjr, ia esmm
rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa sr pqaceiamnsxu, ia esmm caytra
jp famsaqa sj. Sy, px jia pjiac ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor
l calrpx ypc lwjsxu sx lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax
px jia rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
-Facjclxo Ctrramm

This message has been copied to the clipboard.

注意,如果明文中的字母是小写的,那么它在密文中也是小写的。同样,如果字母在明文中是大写的,那么在密文中也是大写的。简单替换密码不加密空格或标点符号,而只是按原样返回这些字符。

要解密这个密文,将它粘贴为第 10 行的myMessage变量的值,并将myMode改为字符串'decrypt'。当您再次运行该程序时,解密输出应该如下所示:

Using key LFWOAYUISVKMNXPBDCRJTQEGHZ
The decrypted message is:
If a man is offered a fact which goes against his instincts, he will
scrutinize it closely, and unless the evidence is overwhelming, he will refuse
to believe it. If, on the other hand, he is offered something which affords
a reason for acting in accordance to his instincts, he will accept it even
on the slightest evidence. The origin of myths is explained in this way.
-Bertrand Russell

This message has been copied to the clipboard.

设置模块、常量和main()函数

让我们来看看简单替换密码程序的源代码的第一行。

# Simple Substitution Cipher
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip, sys, random


LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

第 4 行导入了pyperclipsysrandom模块。LETTERS常量变量设置为全部大写字母的字符串,这是为简单替换密码程序设置的符号。

simpleSubCipher.py中的main()函数,类似于前面章节中密码程序的main()函数,在程序第一次运行时被调用。它包含存储用于程序的messagekeymode的变量。

def main():
    myMessage = 'If a man is offered a fact which goes against his
          instincts, he will scrutinize it closely, and unless the evidence
          is overwhelming, he will refuse to believe it. If, on the other
          hand, he is offered something which affords a reason for acting
          in accordance to his instincts, he will accept it even on the
          slightest evidence. The origin of myths is explained in this way.
          -Bertrand Russell'
    myKey = 'LFWOAYUISVKMNXPBDCRJTQEGHZ'
    myMode = 'encrypt' # Set to 'encrypt' or 'decrypt'.

简单替换密码的密钥很容易出错,因为它们相当长,需要包含字母表中的每个字母。例如,很容易输入缺少一个字母的密钥或两次输入相同字母的密钥。keyIsValid()函数确保密钥可被加密和解密函数使用,如果密钥无效,该函数将退出程序并显示一条错误消息:

    if keyIsValid(myKey):
        sys.exit('There is an error in the key or symbol set.')

14 行目からのkeyIsValid()戻り値に無効なキーFalseが含まれている場合myKey、15 行目でプログラムが終了します。

myMode16 行目から 19 行目では、変数が'encrypt'orに設定されているかどうかを確認し、'decrypt'それに応じてencryptMessage()orを呼び出しますdecryptMessage()

    if myMode == 'encrypt':
        translated = encryptMessage(myKey, myMessage)
    elif myMode == 'decrypt':
        translated = decryptMessage(myKey, myMessage)

encryptMessage()戻りdecryptMessage()値は、translated変数に格納された暗号化または復号化されたメッセージの文字列です。

行 20 では、画面で使用されるキーを出力します。暗号化または復号化されたメッセージは画面に出力され、クリップボードにコピーされます。

    print('Using key %s' % (myKey))
    print('The %sed message is:' % (myMode))
    print(translated)
    pyperclip.copy(translated)
    print()
    print('This message has been copied to the clipboard.')

25 行目はmain()関数のコードの最後の行であるため、プログラムの実行は 25 行目以降に戻ります。main()呼び出しがプログラムの最後の行で完了すると、プログラムは終了します。

keyIsValid()次に、関数がメソッドを使用してsort()キーが有効かどうかをテストする方法を見ていきます。

sort()リストメソッド

List には、sort()リストの項目を数値順またはアルファベット順に並べ替えるメソッドがあります。リスト内の項目を並べ替えるこの種の関数は、2 つのリストに同じ項目が含まれているものの、順序が異なるかどうかを確認する必要がある場合に便利です。

ではsimpleSubCipher.py、単純な置換キー文字列値は、シンボル セット内のすべての文字に重複文字や欠落文字がない場合にのみ機能します。文字列値をソートし、ソートされた値と等しいかどうかをチェックするLETTERSことで、それが有効なキーであるかどうかをチェックできます。list()ただし、ソートできるのはリストのみであり、文字列はソートできないため (文字列は不変、つまり値を変更できないことを思い出してください)、値のリスト バージョンに渡すことで文字列を取得します。次に、これらのリストを並べ替えた後、2 つのリストを比較して、それらが等しいかどうかを確認します。すでにアルファベット順になっていますがLETTERS、後で拡張して追加の文字を含めることになるので、並べ替えます。

def keyIsValid(key):
    keyList = list(key)
    lettersList = list(LETTERS)
    keyList.sort()
    lettersList.sort()

key中的字符串被传递到第 29 行的list()。返回的列表值存储在名为keyList的变量中。

在第 30 行,LETTERS常量变量(包含字符串'ABCDEFGHIJKLMNOPQRSTUVWXYZ')被传递给list(),后者以如下格式返回列表:['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']

在第 31 和 32 行,keyListlettersList中的列表通过调用它们的sort()列表方法按字母顺序排序。注意,类似于append()列表方法,sort()列表方法原地修改列表,并且没有返回值。

排序时,keyListlettersList的值应该是相同的,因为keyList只不过是顺序被打乱的LETTERS中的字符。第 34 行检查值keyListlettersList是否相等:

    return keyList == lettersList

如果keyListlettersList相等,你可以确定keyListkey参数没有任何重复的字符,因为LETTERS里面没有重复的字符。在这种情况下,第 34 行返回True。但是如果keyListlettersList不匹配,密钥无效,第 34 行返回False

包装函数

simpleSubCipher.py程序中的加密代码和解密代码几乎相同。当你有两段非常相似的代码时,最好将它们放入一个函数中并调用两次,而不是输入两次代码。这不仅节省了时间,更重要的是,避免了在复制粘贴代码时引入 bug。这也是有利的,因为如果代码中有 bug,您只需要在一个地方修复 bug,而不是在多个地方。

ラッパー関数は、別の関数のコードをラップし、ラッパー関数によって返された値を返すことにより、繰り返しコードを入力することを避けるのに役立ちます。通常、ラッパー関数は、ラップされた関数のパラメーターまたは戻り値に小さな変更を加えます。それ以外の場合は、関数を直接呼び出すことができるため、ラップする必要はありません。

ラッパー関数がどのように機能するかを理解するために、コード内でラッパー関数を使用する例を見てみましょう。この場合、行 37 と行 41 の合計がencryptMessage()ラッパーdecryptMessage ()関数になります。

def encryptMessage(key, message):
    return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
    return translateMessage(key, message, 'decrypt')

これらの各ラッパー関数はtranslateMessage ()、ラップされた関数である を呼び出し、translateMessage()戻り値を返します。(関数については次のセクションで説明しますtranslateMessage()。) 両方のラッパー関数は同じtranslateMessage()関数を使用するため、暗号に変更を加える必要がある場合は、encryptMessage()sumdecryptMessage ()関数ではなく、この 1 つの関数のみを変更するだけで済みます。

これらのラッパー関数を配置すると、simpleSubCipher.pyプログラムをインポートする人は、この本の他のすべての暗号化プログラムを呼び出すことができるのと同じように、 および というencryptMessage()名前の関数を呼び出すことができます。decryptMessage()ラッパー関数には、コードを見なくても、それらの関数を使用する他のユーザーにその関数が何を行うかを伝える明示的な名前があります。そのため、コードを共有したい場合、他の人がより簡単にコードを使用できるようになります。

他のプログラムは、次のように暗号プログラムをインポートし、その関数を呼び出すことで、encryptMessage()さまざまな暗号を使用してメッセージを暗号化できます。

import affineCipher, simpleSubCipher, transpositionCipher
--snip--
ciphertext1 =        affineCipher.encryptMessage(encKey1, 'Hello!')
ciphertext2 = transpositionCipher.encryptMessage(encKey2, 'Hello!')
ciphertext3 =     simpleSubCipher.encryptMessage(encKey3, 'Hello!')

名前に一貫性があると、一方の暗号化プログラムに精通した人がもう一方の暗号化プログラムを簡単に使用できるようになるため、役に立ちます。たとえば、最初の引数は常にキーであり、2 番目の引数は常にメッセージであることがわかります。これは、本書のほとんどの暗号化プログラムで使用されている規則です。個別のsum関数translateMessage()の代わりに関数を使用すると、他のプログラムと矛盾します。encryptMessage()decryptMessage

次に関数を見ていきますtranslateMessage()

translateMessage()関数

translateMessage()暗号化と復号化のための関数。

def translateMessage(key, message, mode):
    translated = ''
    charsA = LETTERS
    charsB = key
    if mode == 'decrypt':
        # For decrypting, we can use the same code as encrypting. We
        # just need to swap where the key and LETTERS strings are used.
        charsA, charsB = charsB, charsA

パラメータと がtranslateMessage()あり、さらに 3 番目のパラメータがあることに注意してください呼び出し時には関数内の呼び出しがパラメータとして渡され関数内の呼び出しが渡されますこれにより、関数は、渡されたメッセージを暗号化する必要があるか復号化する必要があるかを判断します。keymessagemodetranslateMessageencryptMessage()mode'encrypt'decryptMessage()'decrypt'translateMessage()

実際の暗号化プロセスは単純です。message引数内の文字ごとに、関数はLETTERS内のその文字のインデックスを検索し、keyその文字を引数内の同じインデックスにある文字に置き換えます。復号化ではその逆が行われます。つまり、keyのインデックスを検索し、LETTERSその文字を の同じインデックスにある文字に置き換えます。

LETTERSandを使用する代わりに、プログラムはkey変数charsAandを使用しますcharsB。これにより、charsBの文字を の同じインデックスにある文字に置き換えることができますcharsA割り当てられた値を変更できるためcharsAcharsBプログラムによる暗号化と復号化の切り替えが容易になります。47 行目ではcharsAの文字を のLETTERS文字に設定し、48 行目では のcharsB文字を のkey文字に設定しています。

下の画像は、同じコードを使用して文字を暗号化または復号化する方法を示しています。図 16-2 は暗号化プロセスを示しています。図の上段は内の文字charsA( に設定LETTERS)、中段は内の文字charsB( に設定key)、下段はその文字に対応する整数インデックスを示します。

画像

図 16-2: インデックスを使用した平文の暗号化

translateMessage()のコードは常に のcharsAメッセージ文字のインデックスを検索し、charsBそのインデックスで の対応する文字に置き換えます。したがって、暗号化のために、そのままにしてcharsAおきます。charsB変数を使用して、に設定されているため、内の文字を 内の文字charsA置き換えますcharsBLETTERSkeycharsALETTERScharsBkey

復号化には、 52 行目のトグルcharsA, charsB = charsB, charsA合計の値を使用します。図 16-3 は、復号化プロセスを示しています。charsAcharsB

画像

図 16-3: インデックスを使用した暗号文の復号化

translateMessage()のコードは常に のcharsB文字を の同じインデックスにある文字に置き換えることに注意してくださいcharsAしたがって、52 行目で値が交換されると、translateMessage()コードは暗号化プロセスではなく復号化プロセスを実行します。

次の数行のコードは、プログラムが暗号化と復号化に使用されるインデックスを見つける方法を示しています。

    # Loop through each symbol in the message:
    for symbol in message:
        if symbol.upper() in charsA:
            # Encrypt/decrypt the symbol:
            symIndex = charsA.find(symbol.upper())

55 行目のループはfor、ループの各反復でsymbol変数を文字列内の文字に設定しますmessageこの記号の大文字形式が に存在する場合charsA(keyと には大文字のみが存在することを思い出してくださいLETTERS)、行 58 でsymbolの中の大文字形式のインデックスが検索されますcharsAsymIndex変数にはこのインデックスが格納されます。

56 行目のステートメントはに存在することが保証されているため、find()メソッドが決して返らないことはすでにわかっています-1(fromfind()メソッドは-1文字列内に見つからないパラメーターを意味します) それ以外の場合、行 58 は実行されません。ifsymbol.upper()charsA

次に、暗号化または復号されたそれぞれを使用して、関数から返される文字列symbolを構築します。translateMessage()ただしkey、 と は両方とも大文字であるため、最初の in が小文字であるかどうかを確認し、そうであれば、復号化または暗号化されたものを小文字に調整するLETTERS必要があります。これを行うには、と という2 つの文字列メソッドを学習する必要がありますmessagesymbolsymbolisupper()islower()

isupper()そしてislower()文字列メソッド

isupper()islower()文字列が大文字か小文字かをチェックするメソッド

より具体的には、両方の条件が満たされる場合、isupper()文字列メソッドは次を返しますTrue

  • 文字列には少なくとも 1 つの大文字が含まれます。

  • 文字列に小文字は含まれていません。

両方の条件が満たされる場合、islower()文字列メソッドは次を返しますTrue

  • 文字列には少なくとも 1 つの小文字が含まれています。

  • この文字列には大文字はありません。

文字列内の非アルファベット文字は、これらのメソッドが または を返すかどうかには影響しませんTrueFalseただし、文字列内に非アルファベット文字のみが存在する場合、両方のメソッドは Yes と評価されますFalse対話型シェルに次のコマンドを入力して、これらのメソッドがどのように機能するかを確認します。

   >>> 'HELLO'.isupper()
   True
   >>> 'HELLO WORLD 123'.isupper() # ➊
   True
   >>> 'hello'.islower() # ➋
   True
   >>> '123'.isupper()
   False
   >>> ''.islower()
   False

➊ の例では、 が返されます。には少なくとも 1 つの大文字が含まれているTrueため、小文字は存在しません。'HELLO WORLD 123'文字列内の数値は計算には影響しません。➋ では、文字列内に少なくとも 1 つの小文字があり、大文字が存在しないため、'hello'.islower()戻ります。True'hello'

コードに戻って、メソッドisupper()islower()文字列メソッドがどのように使用されるかを見てみましょう。

isupper()ケースを確実にするために使用します

simpleSubCipher.pyプログラムは、isupper()およびislower()string メソッドを使用して、平文の大文字と小文字が暗号文に確実に反映されるようにします。

            if symbol.isupper():
                translated += charsB[symIndex].upper()
            else:
                translated += charsB[symIndex].lower()

59 行目はsymbol大文字をテストします。そうである場合、行 60 は、大文字バージョンの文字 から までを連結しcharsB[symIndex]ますtranslatedこれにより、key文字の大文字バージョンが大文字入力に対応します。symbol小文字がある場合、行 62 は の文字の小文字charsB[symIndex]バージョンを連結しますtranslated

またはなどsymbol、シンボル セット内の文字ではない場合は、行 59 が返され、行 60 の代わりに行 62 が実行されます。その理由は、これらの文字列に少なくとも 1 つの大文字が含まれていないため、条件が満たされないためです。この場合、行 62 のメソッド呼び出しは文字列にまったく影響を与えません。文字列には文字が含まれていないからです。メソッドは、やなどの非アルファベット文字を変更しません。生の非アルファベット文字を返すだけです。'5''?'Falseisupper()lower()lower()'5''?'

elseブロックの 62 行目では、文字列内のsymbol小文字アルファベット以外の文字を指定します。

63 行目のインデントは、56 行目のelseステートメントがif symbol.upper() in charsA:そのステートメントとペアであることを意味するため、symbolそれがLETTERS含まれていない場合は 63 行目が実行されます。

        else:
            # Symbol is not in LETTERS; just add it:
            translated += symbol

symbolそうでない場合はLETTERS、65 行目を実行します。symbolこれは、 内の文字を暗号化または復号化できないことを意味するため、単純に のtranslated末尾に文字を連結します。

translateMessage()関数の最後で、67 行目は、translated暗号化または復号化されたメッセージを含む変数の値を返します。

    return translated

接下来,我们将看看如何使用getRandomKey()函数为简单的替换密码生成一个有效的密钥。

生成随机密钥

键入包含字母表中每个字母的密钥的字符串可能很困难。为了帮助我们做到这一点,getRandomKey()函数返回一个有效的密钥来使用。第 71 到 73 行随机打乱了LETTERS常量中的字符。

def getRandomKey():
    key = list(LETTERS)
    random.shuffle(key)
    return ''.join(key)

阅读第 123 页上的随机打乱一个字符串,了解如何使用list()random.shuffle()join()方法打乱一个字符串。

要使用getRandomKey()函数,我们需要将第 11 行从myKey = 'LFWOAYUISVKMNXPBDCRJTQEGHZ'改为:

    myKey = getRandomKey()

因为我们的简单替换密码程序中的第 20 行打印了正在使用的密钥,所以您将能够看到函数getRandomKey()返回的密钥。

调用main()函数

如果simpleSubCipher.py作为一个程序运行,而不是作为一个模块被另一个程序导入,程序结尾的第 76 和 77 行调用main()

if __name__ == '__main__':
    main()

我们对简单替换密码程序的研究到此结束。

总结

在这一章中,你学习了如何使用sort()列表方法对列表中的条目进行排序,以及如何比较两个有序列表来检查字符串中的重复字符或缺失字符。您还了解了isupper()islower()字符串方法,它们检查字符串值是由大写字母还是小写字母组成的。您了解了包装函数,包装函数是调用其他函数的函数,通常只添加微小的变化或不同的参数。

简单的替换密码有太多可能的密钥,无法强行破解。这使得它不受你用来破解以前的密码程序的技术的影响。你必须编写更聪明的程序来破解这个密码。

在第 17 章中,你将学习如何破解简单的替换密码。您将使用更加智能和复杂的算法,而不是暴力破解所有的密钥。

练习题

练习题的答案可以在本书的网站www.nostarch.com/crackingcodes找到。

  1. 为什么不能用暴力攻击来对付简单的替换密码,即使是用强大的超级计算机?

  2. 运行这段代码后spam变量包含什么?

      spam = [4, 6, 2, 8]
      spam.sort()
    
  3. 什么是包装函数?

  4. 'hello'.islower()求值为什么?

  5. 'HELLO 123'.isupper()求值为什么?

  6. '123'.islower()求值为什么?

十七、破解简单替换密码

原文:https://inventwithpython.com/cracking/chapter17.html

“加密本质上是一种私人行为。事实上,加密的行为将信息从公共领域移除。即使是针对加密技术的法律,也只能延伸到一个国家的边境和暴力地区。”

——埃里克·休斯,《一个赛博朋克的宣言》(1993)

画像

在第 16 章中,你了解到简单的替换密码不可能用暴力破解,因为它有太多可能的密钥。要破解简单的替换密码,我们需要创建一个更复杂的程序,使用字典值来映射密文的潜在解密字母。在这一章中,我们将编写这样一个程序来将潜在的解密输出列表缩小到正确的一个。

本章涵盖的主题

  • 单词模式、候选单词、潜在的解密字母和密码字母映射

  • 正则表达式

  • sub()正则表达式方法

使用单词模式解密

在暴力攻击中,我们尝试每一个可能的密钥来检查它是否能解密密文。如果密钥是正确的,解密结果是可读的英语。但是,通过首先分析密文,我们可以减少可能尝试的密钥数量,甚至可以找到完整或部分密钥。

让我们假设原始明文主要由英语字典文件中的单词组成,就像我们在第 11 章中使用的那样。虽然密文不会由真正的英语单词组成,但它仍然包含由空格分隔的字母组,就像普通句子中的单词一样。在本书中,我们将这些称为密码。在替换密码中,字母表中的每个字母都有一个唯一的对应加密字母。我们将密文中的字母称为密文。因为每个明文字母只能加密成一个密码字母,并且我们在这个版本的密码中没有加密空格,所以明文和密文将共享相同的单词模式

たとえば、平文がある場合MISSISSIPPI SPILL、対応する暗号文は次のようになりますRJBBJBBJXXJ BXJHH平文の最初の単語は、最初のパスワードと同じ数の文字を持ちます。2 番目の平文と 2 番目の暗号語についても同様です。平文と暗号文は同じ文字とスペースのパターンを共有します。平文で繰り返される文字は、暗号文でも同じ数と位置で繰り返されることにも注意してください。

したがって、パスワードは英語辞書ファイル内の単語に対応し、単語のパターンが一致すると仮定できます。次に、その暗号がどの単語に復号されるかを辞書で見つけることができれば、その単語に含まれる各暗号文字の復号を把握できます。この技術で十分な暗号を解読できれば、メッセージ全体を復号化できます。

単語パターンを見つける

HGHHUパスワードの単語パターンを調べてみましょう。この暗号には、元の平文が持つ必要がある特定の特性があることがわかります。これら 2 つの単語には次の共通点がなければなりません。

  1. 長さは 5 文字にする必要があります。

  2. 1 文字目、3 文字目、4 文字目は同じである必要があります。

  3. 正確に 3 つの異なる文字を持つ必要があり、最初、2 番目、および 5 番目の文字はすべて異なる必要があります。

このパターンに当てはまる英語の単語を考えてみましょう。は、同じパターンで配置された 3 つの異なる文字 ( 、 、 ) を使用した 5 文字の長さ ( 、 、 、 、 ) からなる単語です ( 1文字3文字Puppy4 文字目は2 文字目で5 番目の文字を表します)。このパターンに当てはまります。これらの単語と、その基準に一致する英語辞書ファイル内の他の単語は、復号化の可能性があります。PUPPYPUYPUYMommyBobbylullsnannyHGHHU

プログラムが理解できる方法で単語のパターンを表現するには、各パターンをピリオドで区切られた文字のパターンを表す一連の数字に分割します。

単語パターンの作成は簡単です。最初の文字には番号 0 が割り当てられ、その後最初に出現した異なる文字には次の番号が割り当てられます。たとえばCat、 の単語パターンは であり0.1.2Categoryの単語パターンは です0.1.2.3.4.5.4.0.2.6.4.7.8

単純な置換暗号では、暗号化にどのキーが使用されたかに関係なく、平文とその暗号語は常に同じ単語パターンを持ちます。暗号文HGHHUの単語パターンは です0.1.0.0.2。これは、HGHHU平文の対応する単語パターンも であることを意味します0.1.0.0.2

解読された可能性のある文字の発見

を復号化するにはHGHHU、英語の辞書ファイル内のすべての単語と、このファイルの単語パターンを見つける必要があります0.1.0.0.2本書では、パスワードと同じ単語パターンを持つ平文の単語をパスワードの候補と呼びます。HGHHU候補リストは次のとおりです。

  • PUPPY

  • MOMMY

  • BOBBY

  • LULLS

  • NANNY

単語パターンを使用すると、暗号文がどの平文文字に復号されるかを推測できます。これを、暗号文の潜在的な復号文字と呼びます。単純な置換暗号で暗号化されたメッセージを解読するには、メッセージ内の各単語の潜在的な復号文字をすべて見つけ、消去法によって実際の復号文字を決定する必要があります。表 17-1 に、HGHHU復号化できる可能性のある文字を示します。

表 17-1 :HGHHUの暗号文の潜在的な復号文字

暗号文 H G H H U
潜在的な復号化文字 P U P P Y
M O M M Y
B O B B Y
L U L L S
N A N N Y

以下は、表 17-1 を使用して作成された暗号文字マップです。

  1. HP復号化された可能性のある文字、MBLおよび がありますN

  2. G復号化された可能性のある文字UOが存在しますA

  3. UY解読可能な文字と が存在する可能性がありますS

  4. Hこの例では、 、 、Gおよびを除く他のすべての暗号文字にはU、復号化できる可能性のある文字がありません。

暗号アルファベット マップには、すべてのアルファベット文字と、それらの復号化可能な文字が表示されます。暗号化されたメッセージの収集を開始すると、アルファベットのすべての文字について潜在的な復号文字が見つかりますが、暗号文字 と だけが暗号文例の一部であるため、他の暗号文字については潜在的な復号文字はありませHGU

また、候補文字間に重複があり、その多くが文字で終わるため、U復号化できる文字は 2 つだけ (Yと) であることにも注意してください。重複が多いほど、復号化できる可能性のある文字が少なくなり、その暗号文字が何を復号化するかを把握しやすくなりますSY

表 17-1 を Python コードで表すには、次のように、辞書値を使用して暗号文字マップを表します ( 、'H''G'およびのキーと値のペア'U'は太字です)。

{
    
    'A': [], 'B': [], 'C': [], 'D': [], 'E': [], 'F': [], 'G': ['U', 'O', 'A'],
'H': ['P', 'M', 'B', 'L', 'N'], 'I': [], 'J': [], 'K': [], 'L': [], 'M': [],
'N': [], 'O': [], 'P': [], 'Q': [], 'R': [], 'S': [], 'T': [], 'U': ['Y',
'S'], 'V': [], 'W': [], 'X': [], 'Y': [], 'Z': []}

この辞書には、アルファベットの各文字に 1 つのキーと、各文字の復号化される可能性のある文字のリストを含む 26 のキーと値のペアが含まれています。キー'H'復号化された可能性のある文字が表示されます。値の場合、他のキーにはこれまでのところ復号化できる可能性のある文字がないため、空のリストがあります。'G''[]

他の暗号化された単語の暗号文字マッピングを相互参照することによって、暗号文字の潜在的な復号文字の数を 1 文字に減らすことができれば、その暗号文字が何に復号されるかを知ることができます。26 個すべての暗号を解くことはできなくても、ほとんどの暗号マップを解読して、ほとんどの暗号文を解読することはできます。

この章で使用する基本的な概念と用語をいくつか説明したので、クラッキング プロセスの手順を見てみましょう。

クラッキングプロセスの概要

単語パターンを使用すると、単純な置換暗号を解読するのは非常に簡単です。クラッキングプロセスの主なステップは次のように要約できます。

  1. 暗号文内の各パスワードの単語パターンを見つけます。

  2. 找出每个密码可以解密成的候选英文单词。

  3. 创建一个字典,显示每个密码的潜在解密字母,作为每个密码的密码映射。

  4. 将密码字母映射组合成一个映射,我们称之为相交映射

  5. 从组合映射中移除任何已求解的密码字母。

  6. 用解出的密文解密密文。

密文中的密码越多,映射相互重叠的可能性就越大,每个密码的潜在解密字母就越少。这意味着在简单的替换密码中,密文信息越长,就越容易破解

在深入研究源代码之前,让我们看看如何使破解过程的前两步变得更容易。我们将使用我们在第 11 章中使用的字典文件和一个名为wordPatterns.py的模块来获取字典文件中每个单词的单词模式,并在列表中对它们进行排序。

makewodpatterns模块

要计算dictionary.txt字典文件中每个单词的单词模式,从www.nostarch.com/crackingcodes下载makewodpatterns.py。确保这个程序和dictionary.txt都在保存本章的simpleSubHacker.py程序的文件夹中。

makewodpatterns.py程序有一个getWordPattern()函数,它接受一个字符串(比如'puppy')并返回它的单词模式(比如'0.1.0.0.2')。当您运行makeWordPatterns.py时,它应该会创建 Python 模块wordPatterns.py。该模块包含一个变量赋值语句,如下所示,长度超过 43,000 行:

allPatterns = {
    
    '0.0.1': ['EEL'],
 '0.0.1.2': ['EELS', 'OOZE'],
 '0.0.1.2.0': ['EERIE'],
 '0.0.1.2.3': ['AARON', 'LLOYD', 'OOZED'],
--snip--

allPatterns变量包含一个字典值,将单词模式字符串作为关键字,将与该模式匹配的英语单词列表作为其值。例如,要查找模式为0.1.2.1.3.4.5.4.6.7.8的所有英语单词,请在交互式 shell 中输入以下内容:

>>> import wordPatterns
>>> wordPatterns.allPatterns['0.1.2.1.3.4.5.4.6.7.8']
['BENEFICIARY', 'HOMOGENEITY', 'MOTORCYCLES']

allPatterns字典中,键'0.1.2.1.3.4.5.4.6.7.8'具有列表值['BENEFICIARY', 'HOMOGENEITY', 'MOTORCYCLES'],它包含三个具有这种特殊单词模式的英语单词。

现在让我们导入wordPatterns.py模块,开始构建简单的替换破解程序!

如果在交互 shell 中导入wordPatterns时得到一条ModuleNotFoundError错误消息,请先在交互 shell 中输入以下内容:

>>> import sys
>>> sys.path.append('name_of_folder')

将文件夹名称替换为wordPatterns.py保存的位置。这告诉交互式 shell 在您指定的文件夹中查找模块。

简单替换破解程序的源代码

选择文件 -> 新建文件,打开文件编辑器窗口。在文件编辑器中输入以下代码,保存为simpleSubHacker.py。确保将pyperclip.pysimpleSubCipher.pywordPatterns.py文件放在与simpleSubHacker.py相同的目录下。按F5运行程序。

单纯子
黑客. py

# Simple Substitution Cipher Hacker
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import os, re, copy, pyperclip, simpleSubCipher, wordPatterns,
       makeWordPatterns





LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
nonLettersOrSpacePattern = re.compile('[^A-Z\s]')

def main():
    message = 'Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr
           sxrjsxwjr, ia esmm rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa
           sr pqaceiamnsxu, ia esmm caytra jp famsaqa sj. Sy, px jia pjiac
           ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor l calrpx ypc lwjsxu sx
           lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax px jia
           rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
           -Facjclxo Ctrramm'

    # Determine the possible valid ciphertext translations:
    print('Hacking...')
    letterMapping = hackSimpleSub(message)

    # Display the results to the user:
    print('Mapping:')
    print(letterMapping)
    print()
    print('Original ciphertext:')
    print(message)
    print()
    print('Copying hacked message to clipboard:')
    hackedMessage = decryptWithCipherletterMapping(message, letterMapping)
    pyperclip.copy(hackedMessage)
    print(hackedMessage)


def getBlankCipherletterMapping():
    # Returns a dictionary value that is a blank cipherletter mapping:
    return {
    
    'A': [], 'B': [], 'C': [], 'D': [], 'E': [], 'F': [], 'G': [],
           'H': [], 'I': [], 'J': [], 'K': [], 'L': [], 'M': [], 'N': [],
           'O': [], 'P': [], 'Q': [], 'R': [], 'S': [], 'T': [], 'U': [],
           'V': [], 'W': [], 'X': [], 'Y': [], 'Z': []}


def addLettersToMapping(letterMapping, cipherword, candidate):
    # The letterMapping parameter takes a dictionary value that
    # stores a cipherletter mapping, which is copied by the function.
    # The cipherword parameter is a string value of the ciphertext word.
    # The candidate parameter is a possible English word that the
    # cipherword could decrypt to.

    # This function adds the letters in the candidate as potential
    # decryption letters for the cipherletters in the cipherletter
    # mapping.


    for i in range(len(cipherword)):
        if candidate[i] not in letterMapping[cipherword[i]]:
            letterMapping[cipherword[i]].append(candidate[i])



def intersectMappings(mapA, mapB):
    # To intersect two maps, create a blank map and then add only the
    # potential decryption letters if they exist in BOTH maps:
    intersectedMapping = getBlankCipherletterMapping()
    for letter in LETTERS:

        # An empty list means "any letter is possible". In this case just
        # copy the other map entirely:
        if mapA[letter] == []:
            intersectedMapping[letter] = copy.deepcopy(mapB[letter])
        elif mapB[letter] == []:
            intersectedMapping[letter] = copy.deepcopy(mapA[letter])
        else:
            # If a letter in mapA[letter] exists in mapB[letter],
            # add that letter to intersectedMapping[letter]:
            for mappedLetter in mapA[letter]:
                if mappedLetter in mapB[letter]:
                    intersectedMapping[letter].append(mappedLetter)

    return intersectedMapping


def removeSolvedLettersFromMapping(letterMapping):
    # Cipherletters in the mapping that map to only one letter are
    # "solved" and can be removed from the other letters.
    # For example, if 'A' maps to potential letters ['M', 'N'], and 'B'
    # maps to ['N'], then we know that 'B' must map to 'N', so we can
    # remove 'N' from the list of what 'A' could map to. So 'A' then maps
    # to ['M']. Note that now that 'A' maps to only one letter, we can
    # remove 'M' from the list of letters for every other letter.
    # (This is why there is a loop that keeps reducing the map.)

    loopAgain = True
    while loopAgain:
        # First assume that we will not loop again:
        loopAgain = False

        # solvedLetters will be a list of uppercase letters that have one
        # and only one possible mapping in letterMapping:
        solvedLetters = []
        for cipherletter in LETTERS:
            if len(letterMapping[cipherletter]) == 1:
                solvedLetters.append(letterMapping[cipherletter][0])

        # If a letter is solved, then it cannot possibly be a potential
        # decryption letter for a different ciphertext letter, so we
        # should remove it from those other lists:
        for cipherletter in LETTERS:
            for s in solvedLetters:
                if len(letterMapping[cipherletter]) != 1 and s in
                       letterMapping[cipherletter]:
                    letterMapping[cipherletter].remove(s)
                    if len(letterMapping[cipherletter]) == 1:
                        # A new letter is now solved, so loop again:
                        loopAgain = True
    return letterMapping


def hackSimpleSub(message):
    intersectedMap = getBlankCipherletterMapping()
    cipherwordList = nonLettersOrSpacePattern.sub('',
           message.upper()).split()
    for cipherword in cipherwordList:
        # Get a new cipherletter mapping for each ciphertext word:
        candidateMap = getBlankCipherletterMapping()

        wordPattern = makeWordPatterns.getWordPattern(cipherword)
        if wordPattern not in wordPatterns.allPatterns:
            continue # This word was not in our dictionary, so continue.

        # Add the letters of each candidate to the mapping:
        for candidate in wordPatterns.allPatterns[wordPattern]:
            addLettersToMapping(candidateMap, cipherword, candidate)

        # Intersect the new mapping with the existing intersected mapping:
        intersectedMap = intersectMappings(intersectedMap, candidateMap)

    # Remove any solved letters from the other lists:
    return removeSolvedLettersFromMapping(intersectedMap)


def decryptWithCipherletterMapping(ciphertext, letterMapping):
    # Return a string of the ciphertext decrypted with the letter mapping,
    # with any ambiguous decrypted letters replaced with an underscore.

    # First create a simple sub key from the letterMapping mapping:
    key = ['x'] * len(LETTERS)
    for cipherletter in LETTERS:
        if len(letterMapping[cipherletter]) == 1:
            # If there's only one letter, add it to the key:
            keyIndex = LETTERS.find(letterMapping[cipherletter][0])
            key[keyIndex] = cipherletter
        else:
            ciphertext = ciphertext.replace(cipherletter.lower(), '_')
            ciphertext = ciphertext.replace(cipherletter.upper(), '_')
    key = ''.join(key)

    # With the key we've created, decrypt the ciphertext:
    return simpleSubCipher.decryptMessage(key, ciphertext)


if __name__ == '__main__':
    main()

简单替换破解程序的运行示例

当你运行这个程序时,它试图破解message变量中的密文。它的输出应该如下所示:

Hacking...
Mapping:
{
    
    'A': ['E'], 'B': ['Y', 'P', 'B'], 'C': ['R'], 'D': [], 'E': ['W'], 'F':
['B', 'P'], 'G': ['B', 'Q', 'X', 'P', 'Y'], 'H': ['P', 'Y', 'K', 'X', 'B'],
'I': ['H'], 'J': ['T'], 'K': [], 'L': ['A'], 'M': ['L'], 'N': ['M'], 'O':
['D'], 'P': ['O'], 'Q': ['V'], 'R': ['S'], 'S': ['I'], 'T': ['U'], 'U': ['G'],
'V': [], 'W': ['C'], 'X': ['N'], 'Y': ['F'], 'Z': ['Z']}

Original ciphertext:
Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr sxrjsxwjr, ia esmm
rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa sr pqaceiamnsxu, ia esmm caytra
jp famsaqa sj. Sy, px jia pjiac ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor
l calrpx ypc lwjsxu sx lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax
px jia rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
-Facjclxo Ctrramm

Copying hacked message to clipboard:
If a man is offered a fact which goes against his instincts, he will
scrutinize it closel_, and unless the evidence is overwhelming, he will refuse
to _elieve it. If, on the other hand, he is offered something which affords
a reason for acting in accordance to his instincts, he will acce_t it even
on the slightest evidence. The origin of m_ths is e__lained in this wa_.
-_ertrand Russell

现在我们来详细探究一下源代码。

设置模块和常量

让我们看看简单替换破解程序的前几行。第 4 行导入了 7 个不同的模块,比迄今为止任何其他程序都多。第 10 行的全局变量LETTERS存储符号集,它由字母表中的大写字母组成。

# Simple Substitution Cipher Hacker
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import os, re, copy, pyperclip, simpleSubCipher, wordPatterns,
       makeWordPatterns
     --snip--
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

re模块是正则表达式模块,它允许使用正则表达式进行复杂的字符串操作。让我们看看正则表达式是如何工作的。

用正则表达式查找字符

正则表达式是定义匹配特定字符串的特定模式的字符串。例如,第 11 行的字符串'[^A-Z\s]'是一个正则表达式,它告诉 Python 查找不是从AZ的大写字母或空白字符的任何字符(比如空格、制表符或换行符)。

nonLettersOrSpacePattern = re.compile('[^A-Z\s]')

re.compile()函数创建一个re模块可以使用的正则表达式模式对象(缩写为 正则表达式对象模式对象)。我们将使用这个对象从第 241 页的的hackSimpleSub()函数中删除任何非字母字符。

您可以使用正则表达式执行许多复杂的字符串操作。要了解更多关于正则表达式的信息,请转到www.nostarch.com/crackingcodes

设置main()函数

与本书中之前的破解程序一样,main()函数将密文存储在message变量中,第 18 行将该变量传递给hackSimpleSub()函数:

def main():
    message = 'Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr
           sxrjsxwjr, ia esmm rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa
           sr pqaceiamnsxu, ia esmm caytra jp famsaqa sj. Sy, px jia pjiac
           ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor l calrpx ypc lwjsxu sx
           lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax px jia
           rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
           -Facjclxo Ctrramm'

    # Determine the possible valid ciphertext translations:
    print('Hacking...')
    letterMapping = hackSimpleSub(message)

hackSimpleSub()不是返回解密的消息或None(如果无法解密的话),而是返回一个删除了解密字母的相交密码字母映射。(我们将在第 234 页的上的相交两个映射中查看如何创建相交映射。)这个相交的密文映射然后被传递给decryptWithCipherletterMapping()以将存储在message中的密文解密成可读格式,你将在第 243 页的中的“解密消息中看到更多细节。

存储在letterMapping中的密码字母映射是一个字典值,它有 26 个大写的单字母字符串作为代表密码字母的关键字。它还列出了每个密码字母的潜在解密字母的大写字母,作为字典的值。当每个密码字母只有一个与之相关的潜在解密字母时,我们就有了一个完全解决的映射,并且可以使用相同的密码和密钥解密任何密文。

生成的每个密文映射取决于所使用的密文。在某些情况下,我们将只有部分解决的映射,其中一些密码没有潜在的解密,而其他密码有多个潜在的解密。不包含字母表中每个字母的较短密文更有可能导致不完整的映射。

向用户显示破解结果

然后程序调用print()函数在屏幕上显示letterMapping、原始消息和解密后的消息:

    # Display the results to the user:
    print('Mapping:')
    print(letterMapping)
    print()
    print('Original ciphertext:')
    print(message)
    print()
    print('Copying hacked message to clipboard:')
    hackedMessage = decryptWithCipherletterMapping(message, letterMapping)
    pyperclip.copy(hackedMessage)
    print(hackedMessage)

行 28 では、復号化されたメッセージを変数に保存しますhackedMessage。変数はクリップボードにコピーされて画面に出力されるため、ユーザーは元のメッセージと比較できます。復号化されたメッセージを見つけるために使用しますdecryptWithCipherletterMapping()。これはプログラムの後半で定義します。

次に、暗号文を作成するためのすべての関数を見てみましょう。

パスワード文字マップを作成する

プログラムは、暗号文内のパスワードごとに暗号マップを作成する必要があります。完全なマップを作成するには、いくつかのヘルパー関数が必要です。ヘルパー関数の 1 つは新しいパスワード マップを構築し、パスワードごとにそれを呼び出すことができます。

別の関数は、暗号語、その現在のレターマップ、および候補の復号語を取得して、すべての候補の復号語を検索します。この関数を各パスワードと各候補単語に対して呼び出します。次に、この関数は、候補単語内の復号化される可能性のあるすべての文字をパスワード ワードの文字マップに追加し、文字マップを返します。

暗号文から複数の単語のレターマップが得られたら、関数を使用してそれらを結合します。次に、最終ヘルパー関数を使用して、1 つの復号文字を各暗号文字に照合することで、できるだけ多くの暗号文字の復号を解決します。前述したように、すべての暗号を解読できるとは限りませんが、この問題の解決方法については、243 ページの「メッセージの復号化」で説明します。

白地図を作成する

まず、空のパスワード文字マップを作成する必要があります。

def getBlankCipherletterMapping():
    # Returns a dictionary value that is a blank cipherletter mapping:
    return {
    
    'A': [], 'B': [], 'C': [], 'D': [], 'E': [], 'F': [], 'G': [],
           'H': [], 'I': [], 'J': [], 'K': [], 'L': [], 'M': [], 'N': [],
           'O': [], 'P': [], 'Q': [], 'R': [], 'S': [], 'T': [], 'U': [],
           'V': [], 'W': [], 'X': [], 'Y': [], 'Z': []}

呼び出されると、getBlankCipherletterMapping()この関数はキーがアルファベット 26 文字の文字列に設定されている辞書を返します。

地図に文字を追加する

マップに文字を追加するには、addLettersToMapping()38 行目で関数を定義します。

def addLettersToMapping(letterMapping, cipherword, candidate):

この関数は、暗号マップ ( letterMapping)、マップへの暗号 ( )、およびcipherword暗号を復号化できる候補復号語 ( candidate)の 3 つの引数を取ります。この関数は、candidate各文字を の対応するインデックス位置にある暗号文字にマッピングするcipherwordか、文字が存在しない場合は に文字を追加しますletterMapping

たとえば、'PUPPY'yesの場合cipherword 'HGHHU'関数はのキーに値を追加します次に、関数は次の文字に進み、キーとペアになっている値をリストに追加します。以下同様に続きます。candidateaddLettersToMapping()'P'letterMapping'H''U''G'

レターがすでに復号化される可能性のあるレターのリストに含まれている場合、そのaddLettersToMapping ()レターはリストに再度追加されません。たとえば、 では'PUPPY'、次の 2 つのインスタンスでは、キーがすでに存在しているため、キーへの追加'P'はスキップされます。最後に、関数はキーの値を変更して、復号化される可能性のある文字のリストにキーが含まれるようにします'P''H''U''Y'

addLettersToMapping()のコードは、一致する単語パターンを持つ合計ペアのみを渡す必要があるという点でのコードと同じlen(cipherword)ことを前提としていますlen(candidate)cipherwordcandidate

次に、プログラムはcipherword文字列内の各インデックスを反復処理して、復号化された可能性のある文字のリストに文字が追加されているかどうかを確認します。

    for i in range(len(cipherword)):
        if candidate[i] not in letterMapping[cipherword[i]]:
            letterMapping[cipherword[i]].append(candidate[i])

変数を使用して、iインデックスごとにcipherword各文字と、それcandidateに対応する潜在的な復号化文字を反復処理します。これができるのは、追加する可能性のある復号化文字が暗号文字のcipherword[i]文字であるためですcandidate[i]たとえば、代わりにcipherwordの場合はインデックスから開始して、と を使用して各文字列の最初の文字にアクセスします。次に、実行は 51 行目のステートメントに移ります'HGHHU'candidate'PUPPY'i0cipherword[0]candidate[0]if

ifこのステートメントは、潜在的な復号化文字が暗号文字の潜在的な復号化文字のリストに既に存在するかどうかを確認しcandidate[i]、既にリストに存在する場合は追加しません。これは、アクセス マップ内のキーと同様に、アクセス マップ内のパスワード文字を使用してletterMapping[cipherword[i]]行われます。このチェックにより、復号化された可能性のある文字のリストに重複する文字が含まれることがなくなります。cipherword[i]letterMapping

たとえば、'PUPPY'最初の from は'P'ループの最初の反復で追加される可能性がありますletterMappingが、i3 回目の反復で等しい場合2、from は最初の反復ですでに追加されてcandidate[2]いる'P'ためマップに追加されません。

潜在的な復号化文字がまだマップ内にない場合、行 52 は、暗号文字マップ内の潜在的な復号化文字のリストcandidate[i]新しい文字を追加します。letterMapping[cipherword[i]]

Python は辞書自体のコピーではなく、引数として渡された辞書への参照のコピーを渡すため、この関数内の辞書への変更は関数の外部でも行われることを思い出してletterMappingくださいaddLettersToMapping()これは、参照の 2 つのコピーが、 126 行目のaddLettersToMapping()呼び出しで引数としてletterMapping渡された同じ辞書を依然として参照しているためです。

内のすべてのインデックスを反復した後、関数は変数のマップcipherwordに文字を追加します。letterMapping次に、プログラムがこのマッピングを他の暗号のマッピングと比較して重複をチェックする方法を見てみましょう。

2 つの地図の交差点

hackSimpleSub()この関数は、 function を使用して2 つの暗号文字マップを sum引数intersectMappings()として渡しsumの結合マップを返しますこの関数は、重複を防ぐために、 sum を結合し、空のマップを作成し、マップ内に復号化された可能性のある文字を空のマップに追加するようにプログラムに指示します (両方の文字がマップ内に存在する場合)。mapAmapBmapAmapBintersectMappings()mapAmapB

def intersectMappings(mapA, mapB):
    # To intersect two maps, create a blank map and then add only the
    # potential decryption letters if they exist in BOTH maps:
    intersectedMapping = getBlankCipherletterMapping()

まず、59 行目で暗号マップを作成し、getBlankCipherletterMapping()呼び出して戻り値を変数に格納することで、intersectedMappingマージされたマップを格納します。

60 行目のforループは、LETTERS定数変数の大文字を反復処理し、letterその変数を辞書のキーmapAとして使用します。mapB

    for letter in LETTERS:

        # An empty list means "any letter is possible". In this case just
        # copy the other map entirely:
        if mapA[letter] == []:
            intersectedMapping[letter] = copy.deepcopy(mapB[letter])
        elif mapB[letter] == []:
            intersectedMapping[letter] = copy.deepcopy(mapA[letter])

行 64 は、mapA復号化される可能性のある文字のリストが空かどうかを確認します。空のリストは、暗号を任意の文字に復号できることを意味します。この場合、交差する暗号文字マップは、other潜在的な復号文字のマップのリストを単純に複製します。たとえば、mapAの復号化される可能性のある文字のリストが空の場合、65 行目で交差マップのリストを のmapBリストのコピーに設定し、その逆も同様です。両方のマップのリストが空の場合、行 64 の条件は true でありTrue、行 65 でmapB空のリストが交差するマップに単純にコピーされることに注意してください。

68 行目のelseブロックは、mapA両方の合計mapBが空でない場合を処理します。

        else:
            # If a letter in mapA[letter] exists in mapB[letter],
            # add that letter to intersectedMapping[letter]:
            for mappedLetter in mapA[letter]:
                if mappedLetter in mapB[letter]:
                    intersectedMapping[letter].append(mappedLetter)

    return intersectedMapping

マップが空でない場合、行 71 はmapA[letter]リスト内の大文字の文字列を反復処理します。mapA[letter]行 72では、 の大文字が の中のmapB[letter]大文字文字列のリストにも存在するかどうかを確認します。そうであれば、行 73 でintersectedMapping[letter]この共通文字を潜在的な復号化文字のリストに追加します。

行 60 で始まるforループが終了すると、 の暗号文字マップには、との潜在的な復号文字のリストにintersectedMapping存在する潜在的な復号文字のみが含まれます。行 75 は、この完全に交差する暗号文字マップを返します。次に、交差点マップの出力例を見てみましょう。mapAmapB

アルファベットマッピングヘルパー関数の仕組み

レターマップ ヘルパー関数を定義したので、それらがどのように連携するかをよりよく理解するために対話型シェルで使用してみましょう。暗号文の'OLQIHXIRCKGNZ PLQRZKBZB MPBKSSIPLC'交差する暗号マップを作成しましょう。これには 3 つの暗号のみが含まれます。各単語のマッピングを作成し、これらのマッピングを結合します。

simpleSubHacker.pyインタラクティブ シェルにインポートします

>>> import simpleSubHacker

次に、getBlankCipherletterMapping()空白文字のマップを作成する呼び出しを行い、このマップを次のletterMapping1名前の変数に保存します。

>>> letterMapping1 = simpleSubHacker.getBlankCipherletterMapping()
>>> letterMapping1
{
    
    'A': [], 'C': [], 'B': [], 'E': [], 'D': [], 'G': [], 'F': [], 'I': [],
'H': [], 'K': [], 'J': [], 'M': [], 'L': [], 'O': [], 'N': [], 'Q': [],
'P': [], 'S': [], 'R': [], 'U': [], 'T': [], 'W': [], 'V': [], 'Y': [],
'X': [], 'Z': []}

最初のパスワードの解読を始めましょう'OLQIHXIRCKGNZ'まず、次のようにmakeWordPatternモジュールの関数を呼び出して、このパスワードの単語パターンを取得する必要があります。getWordPattern()

>>> import makeWordPatterns
>>> makeWordPatterns.getWordPattern('OLQIHXIRCKGNZ')
0.1.2.3.4.5.3.6.7.8.9.10.11

辞書内のどの英単語に単語パターンがあるかを調べる0.1.2.3.4.5.3.6.7.8.9.10.11(つまり、'OLQIHXIRCKGNZ')パスワードの候補を見つける) ために、wordPatternsモジュールをインポートしてこのパターンを探します。

>>> import wordPatterns
>>> candidates = wordPatterns.allPatterns['0.1.2.3.4.5.3.6.7.8.9.10.11']
>>> candidates
['UNCOMFORTABLE', 'UNCOMFORTABLY']

2 つの英語の単語に一致する単語パターン。したがって、最初の暗号で解読できるのはと の'OLQIHXIRCKGNZ'2 つの単語だけですこれらの単語は候補なので、変数のリストとして保存します (関数のパラメーターと混同しないでください)。'UNCOMFORTABLE''UNCOMFORTABLY'candidatesaddLettersToMapping()candidate

addLettersToMapping()次に、文字を使用してその文字を にマッピングする必要がありますcipherwordまず、次のようにcandidatesリストの最初のメンバーにアクセスしてマップします。'UNCOMFORTABLE'

>>> letterMapping1 = simpleSubHacker.addLettersToMapping(letterMapping1,
'OLQIHXIRCKGNZ', candidates[0])
>>> letterMapping1
{
    
    'A': [], 'C': ['T'], 'B': [], 'E': [], 'D': [], 'G': ['B'], 'F': [], 'I':
['O'], 'H': ['M'], 'K': ['A'], 'J': [], 'M': [], 'L': ['N'], 'O': ['U'], 'N':
['L'], 'Q': ['C'], 'P': [], 'S': [], 'R': ['R'], 'U': [], 'T': [], 'W': [],
'V': [], 'Y': [], 'X': ['F'], 'Z': ['E']}

letterMapping1からわかるように、'OLQIHXIRCKGNZ'の文字は、'UNCOMFORTABLE'次の文字にマップされます。map 'O'to ['U']'L'map to ['N']'Q'map to['C']などです。

ただし、'OLQIHXIRCKGNZ'の文字は に復号化することもできるため' UNCOMFORTABLY'、それを暗号文字マップにも追加する必要があります。対話型シェルに次のように入力します。

>>> letterMapping1 = simpleSubHacker.addLettersToMapping(letterMapping1,
'OLQIHXIRCKGNZ', candidates[1])
>>> letterMapping1
{
    
    'A': [], 'C': ['T'], 'B': [], 'E': [], 'D': [], 'G': ['B'], 'F': [],
'I': ['O'], 'H': ['M'], 'K': ['A'], 'J': [], 'M': [], 'L': ['N'], 'O': ['U'],
'N': ['L'], 'Q': ['C'], 'P': [], 'S': [], 'R': ['R'], 'U': [], 'T': [],
'W': [], 'V': [], 'Y': [], 'X': ['F'], 'Z': ['E', 'Y']}

では、 のパスワード文字マッピングに に加えてマッピングが追加されたことletterMapping1を除いて、 ではほとんど変更されていないことに注意してくださいこれは、文字がまだリストにない場合にのみリストに追加されるためです。letterMapping1'E''Z''Y'addLettersToMapping()

これで、3 つの暗号語のうちの最初の暗号文字マップが得られました。2 番目のパスワードの'PLQRZKBZB新しいマッピングを取得し、プロセスを繰り返す必要があります。

>>> letterMapping2 = simpleSubHacker.getBlankCipherletterMapping()
>>> wordPat = makeWordPatterns.getWordPattern('PLQRZKBZB')
>>> candidates = wordPatterns.allPatterns[wordPat]
>>> candidates
['CONVERSES', 'INCREASES', 'PORTENDED', 'UNIVERSES']
>>> for candidate in candidates:
...   letterMapping2 = simpleSubHacker.addLettersToMapping(letterMapping2,
'PLQRZKBZB', candidate)
...
>>> letterMapping2
{
    
    'A': [], 'C': [], 'B': ['S', 'D'], 'E': [], 'D': [], 'G': [], 'F': [], 'I':
[], 'H': [], 'K': ['R', 'A', 'N'], 'J': [], 'M': [], 'L': ['O', 'N'], 'O': [],
'N': [], 'Q': ['N', 'C', 'R', 'I'], 'P': ['C', 'I', 'P', 'U'], 'S': [], 'R':
['V', 'R', 'T'], 'U': [], 'T': [], 'W': [], 'V': [], 'Y': [], 'X': [], 'Z':
['E']}

4 つの候補単語のそれぞれに対して 4 組の呼び出しを入力する代わりに、forのリストを反復処理して各単語を呼び出すループを作成できますこれで、2 番目の暗号の暗号文字マッピングが完了しました。candidatesaddLettersToMapping()addLettersToMapping()

次に、暗号アルファベットマップを と に渡して、それらの交差部分を取得する必要がありintersectMappings()ます対話型シェルに次のように入力します。letterMapping1letterMapping2

>>> intersectedMapping = simpleSubHacker.intersectMappings(letterMapping1,
letterMapping2)
>>> intersectedMapping
{
    
    'A': [], 'C': ['T'], 'B': ['S', 'D'], 'E': [], 'D': [], 'G': ['B'], 'F': [],
'I': ['O'], 'H': ['M'], 'K': ['A'], 'J': [], 'M': [], 'L': ['N'], 'O': ['U'],
'N': ['L'], 'Q': ['C'], 'P': ['C', 'I', 'P', 'U'], 'S': [], 'R': ['R'],
'U': [], 'T': [], 'W': [], 'V': [], 'Y': [], 'X': ['F'], 'Z': ['E']}

ここで、交差マップ内の暗号の潜在的な復号文字のリストは、単純にletterMapping1letterMapping2潜在的な復号文字になるはずです。

たとえば、'Z'にキーintersectedMappingがあるリストは['E']、と だけletterMapping1があるため['E', 'Y']letterMapping2のみです['E']

次に、次のように3 番目のパスワードに対して'MPBKSSIPLC'上記のすべての手順を繰り返します。

>>> letterMapping3 = simpleSubHacker.getBlankCipherletterMapping()
>>> wordPat = makeWordPatterns.getWordPattern('MPBKSSIPLC')
>>> candidates = wordPatterns.allPatterns[wordPat]
>>> for i in range(len(candidates)):
...   letterMapping3 = simpleSubHacker.addLettersToMapping(letterMapping3,
'MPBKSSIPLC', candidates[i])
...
>>> letterMapping3
{
    
    'A': [], 'C': ['Y', 'T'], 'B': ['M', 'S'], 'E': [], 'D': [], 'G': [],
'F': [], 'I': ['E', 'O'], 'H': [], 'K': ['I', 'A'], 'J': [], 'M': ['A', 'D'],
'L': ['L', 'N'], 'O': [], 'N': [], 'Q': [], 'P': ['D', 'I'], 'S': ['T', 'P'],
'R': [], 'U': [], 'T': [], 'W': [], 'V': [], 'Y': [], 'X': [], 'Z': []}

対話型シェルに次のコマンドを入力して、letterMapping3と を交差させますintersectedMapping。これはletterMapping1letterMapping2の交差マップです。

>>> intersectedMapping = simpleSubHacker.intersectMappings(intersectedMapping,
letterMapping3)
>>> intersectedMapping
{
    
    'A': [], 'C': ['T'], 'B': ['S'], 'E': [], 'D': [], 'G': ['B'], 'F': [],
'I': ['O'], 'H': ['M'], 'K': ['A'], 'J': [], 'M': ['A', 'D'], 'L': ['N'],
'O': ['U'], 'N': ['L'], 'Q': ['C'], 'P': ['I'], 'S': ['T', 'P'], 'R': ['R'],
'U': [], 'T': [], 'W': [], 'V': [], 'Y': [], 'X': ['F'], 'Z': ['E']}

この例では、リスト内に値が 1 つだけ含まれるキーの解決策を見つけることができました。たとえば、'K'に復号化します'A'ただし、キーはまたは'M'に復号化できることに注意してくださいキーが復号化されていることがわかっているため、キーは復号化する必要があるのではなく、復号化する必要があると推測できます結局のところ、単純な置換暗号は 1 つの平文文字を 1 つの暗号文字に暗号化するため、クラックされた文字が 1 つの暗号文字で使用された場合、別の暗号文字で使用することはできません。'A''D''K''A''M''D''A'

removeSolvedLettersFromMapping()関数がこれらの解決された文字をどのように見つけて、復号化される可能性のある文字のリストから削除するかを見てみましょう。作成したばかりのウィンドウが必要になるためintersectedMapping、アイドル状態のウィンドウはまだ閉じないでください。

マップ内の解決された文字を識別します

removeSolvedLettersFromMapping()この関数は、letterMapping引数内で復号化できる可能性のある文字が 1 つだけある暗号文字を検索します。これらの暗号は壊れていると見なされます。これは、解読可能な文字のリストに壊れた文字が含まれる他の暗号は、その文字に解読することが不可能であることを意味します。これは連鎖反応を引き起こす可能性があります。これは、復号化された可能性のある文字が、2 文字のみで構成される他の復号化された可能性のある文字のリストから削除されると、その結果、新しい復号化された暗号文字が得られる可能性があるためです。プログラムは、暗号アルファベット マップ全体をループして新しく解決された文字を削除することで、この状況に対処します。

def removeSolvedLettersFromMapping(letterMapping):
         --snip--
    loopAgain = True
    while loopAgain:
        # First assume that we will not loop again:
        loopAgain = False

letterMappingパラメータにはディクショナリへの参照が渡されるため、removeSolvedLettersFromMapping()関数が戻った後でも、ディクショナリには関数内で行われた変更が含まれます。88 行目では、loopAgain解決された別の文字が見つかったときにコードを再度ループする必要があるかどうかを決定するブール値を保持する変数を作成します。

88 行目の変数がloopAgainに設定されている場合True、プログラムの実行は 89 行目のループに入りますwhileループの開始時に、行 91 がloopAgainに設定されますFalseコードでは、これが89 行目のwhileループの最後の反復であると想定しています。この変数は、プログラムがこの反復中に新しく解決された暗号文字を見つけた場合にloopAgainのみ に設定されますTrue

コードの次の部分では、復号化可能な文字を 1 つだけ含む暗号リストを作成します。これらは、マップから削除される解決された文字です。

        # solvedLetters will be a list of uppercase letters that have one
        # and only one possible mapping in letterMapping:
        solvedLetters = []
        for cipherletter in LETTERS:
            if len(letterMapping[cipherletter]) == 1:
                solvedLetters.append(letterMapping[cipherletter][0])

行 96 のループは、26 個の可能な暗号文字すべてを反復処理し、その暗号文字の暗号文字マッピングについて、for潜在的な復号文字のリスト (つまり、 のリスト) を調べます。letterMapping[cipherletter]

97 行目では、このリストの長さが であるかどうかを確認します1もしそうであれば、解読できるのは 1 文字だけであり、暗号は破られていることがわかります。行 98 は、解決された復号化文字をsolvedLettersリストに追加します。解読される文字は、復号化される可能性のある文字のリストとletterMapping[cipherletter][0]同様に、常に にあり、リストのインデックスには文字列値が 1 つだけあります。letterMapping[cipherletter]0

for96 行目からの前のループが終了すると、solvedLetters変数には暗号文のすべての復号化のリストが含まれる必要があります。98 行目では、これらの復号化された文字列をリストとして に保存しますsolvedLetters

これまでのところ、プログラムはすべての解かれた文字の認識を完了しています。次に、それらが他の暗号の復号化文字の候補としてリストされているかどうかを確認し、削除します。

これを行うために、103 行目のループは、for26 個の可能な暗号文字すべてを反復処理し、暗号文字にマッピングされている潜在的な復号文字のリストを調べます。

        for cipherletter in LETTERS:
            for s in solvedLetters:
                if len(letterMapping[cipherletter]) != 1 and s in
                       letterMapping[cipherletter]:
                    letterMapping[cipherletter].remove(s)
                    if len(letterMapping[cipherletter]) == 1:
                        # A new letter is now solved, so loop again:
                        loopAgain = True
    return letterMapping

チェックされた暗号文字ごとに、行 104 でsolvedLetters文字をループしてletterMapping[cipherletter]、復号化可能な文字のリストにそれらの文字が存在するかどうかをチェックします。

行 105 は、復号化された可能性のある文字のリストがチェックによって未解決であるかどうかlen(letterMapping[cipherletter]) != 1、および解読された文字が復号化された可能性のある文字のリストに存在するかどうかを確認します。両方の基準が満たされる場合、条件が戻りTrue、行 106 で、s解読された文字を潜在的な復号化文字のリストから削除します。

この削除により、復号化される可能性のある文字のリストに 1 文字だけが残った場合、109 行目でloopAgain変数が に設定されるTrueため、コードはループの次の反復で暗号文字のマップからこの新しく解決された文字を削除できます。

89 行目のループがに設定されずにwhile完全な反復を完了した後、プログラムはループを終了し、110 行目で に保存されている暗号文字マップを返します。loopAgainTrueletterMapping

変数letterMappingには、部分的または完全に解決された可能性のある暗号アルファベット マップが含まれるようになりました。

テストremoveSolvedLetterFromMapping()機能

対話型シェルでテストして、実際のremoveSolvedLetterFromMapping()動作を見てみましょう。intersectedMapping作成時に開いていた対話型シェル ウィンドウに戻ります。(ウィンドウを閉じても心配する必要はありません。235 ページの「アルファベット マッピング ヘルプ機能の動作方法」の手順を再入力し、この例に従うことができます。)

解決された文字をそこから削除するにはintersectedMapping、対話型シェルに次のように入力します。

>>> letterMapping = simpleSubHacker.removeSolvedLettersFromMapping(
intersectedMapping)
>>> intersectedMapping
{
    
    'A': [], 'C': ['T'], 'B': ['S'], 'E': [], 'D': [], 'G': ['B'], 'F': [],
'I': ['O'], 'H': ['M'], 'K': ['A'], 'J': [], 'M': ['D'], 'L': ['N'], 'O':
['U'], 'N': ['L'], 'Q': ['C'], 'P': ['I'], 'S': ['P'], 'R': ['R'], 'U': [],
'T': [], 'W': [], 'V': [], 'Y': [], 'X': ['F'], 'Z': ['E']}

intersectedMappingそこから解決された文字を削除すると、'M'復号化された可能性のある文字が 1 つだけになったことに注目してください'D'。これはまさに私たちが予測したものです。これで、暗号文字ごとに潜在的な復号文字が 1 つだけになるため、暗号文字マップを使用して復号を開始できます。この対話型シェルの例にもう一度戻る必要があるため、ウィンドウを開いたままにしておきます。

hackSimpleSub()関数

関数getBlankCipherletterMapping()addLettersToMapping()intersectMappings()およびremoveSolvedLettersFromMapping()それらに渡す暗号文字マップの操作方法を確認したので、simpleSubHacker.pyプログラムでこれらの関数を使用してメッセージを復号化してみましょう。

113 行目はhackSimpleSub()、暗号文メッセージを受け取り、レターマップ ヘルパー関数を使用して、部分的または完全に解決された暗号レターマップを返す関数を定義しています。

def hackSimpleSub(message):
    intersectedMap = getBlankCipherletterMapping()
    cipherwordList = nonLettersOrSpacePattern.sub('', 
           message.upper()).split()

114 行目で、新しい暗号文字マップを作成し、変数に格納しますintersectedMapこの変数は最終的に各パスワードの交差マップを保持します。

message115 行目で、内のアルファベット以外の文字を削除します。nonLettersOrSpacePatternの通常のオブジェクトは、文字または空白文字ではない任意の文字列と一致します。正規表現でメソッドを呼び出しますsub()。メソッドは 2 つのパラメータを取ります。この関数は 2 番目の引数で一致するものを検索し、最初の引数の文字列で置き換えます。次に、これらすべての置換を含む文字列を返します。この例では、メソッドは、大文字の文字列を反復処理し、すべての非アルファベット文字を空白文字列 ( ) に置き換えるようsub()プログラムに指示します。これにより、すべての句読点と数字が削除された文字列が返され、変数に格納されます。message''sub()cipherwordList

115 行目での実行後、cipherwordList変数には、messageの前にある個々のパスワードの大文字文字列のリストが含まれている必要があります。

116 行目のforループは、messageリスト内の各文字列をcipherword変数に割り当てます。このループでは、コードは空のマップを作成し、パスワード候補を取得し、パスワード文字のマップに候補の文字を追加して、このマップと交差しますintersectedMap

    for cipherword in cipherwordList:
        # Get a new cipherletter mapping for each ciphertext word:
        candidateMap = getBlankCipherletterMapping()
        wordPattern = makeWordPatterns.getWordPattern(cipherword)
        if wordPattern not in wordPatterns.allPatterns:
            continue # This word was not in our dictionary, so continue.
        # Add the letters of each candidate to the mapping:
        for candidate in wordPatterns.allPatterns[wordPattern]:
            addLettersToMapping(candidateMap, cipherword, candidate)
        # Intersect the new mapping with the existing intersected mapping:
        intersectedMap = intersectMappings(intersectedMap, candidateMap)

行 118 は、getBlankCipherletterMapping ()関数から新しい空の暗号レターマップを取得し、それをcandidateMap変数に格納します。

現在のパスワード候補を見つけるために、120 行目で を呼び出しmakeWordPatternsますgetWordPattern ()場合によっては、パスワードが名前または辞書に存在しない非常に珍しい単語である場合があり、その場合、その単語パターンはどちらの にも存在しない可能性がありますwordPatternswordPatterns.allPatternsパスワードの単語パターンが辞書のキーに存在しない場合、元の平文の単語は辞書ファイルに存在しません。この場合、パスワードはマップされず、continue122 行目のステートメントは 116 行目のリスト内の次のパスワードに戻ります。

125 行目に到達すると、単語パターンがwordPatterns.allPatternsに存在することがわかります。allPatterns辞書内の値は、wordPatternパターン内の英単語を含む文字列のリストです。値はリストの形式であるため、forループを使用して値を反復処理します。ループの各反復で、candidateこれらの英単語の各文字列に変数が設定されます。

行 125 のループはfor行 126 を呼び出して、addLettersToMapping()各候補からの文字でcandidateMap暗号文字マップを更新します。addLettersToMapping()この関数はリストを直接変更するため、candidateMap関数呼び出しが返されたときにリストが変更されます。

候補内のすべての文字が のcandidateMap暗号文字マップに追加された後、行 129 はcandidateMapと交差しintersectedMap、新しい値 を返しますintersectedMap

この時点で、プログラムの実行はループの先頭の行 116 に戻りforcipherwordListリスト内の次の暗号に対する新しいマップが作成され、次の暗号に対するマップも とintersectedMap交差します。ループは、cipherWordList最後の単語に到達するまでパスワードのマッピングを続けます。

暗号文内のすべての暗号語のマップを含む、交差する最終的な暗号文字マップが得られたら、それを 132 行目に渡して、removeSolvedLettersFromMapping ()解読された文字を削除します。

    # Remove any solved letters from the other lists:
    return removeSolvedLettersFromMapping(intersectedMap)

removeSolvedLettersFromMapping ()返回的密码字母映射然后被返回给hackSimpleSub()函数。现在我们有了部分的密码解决方案,所以我们可以开始解密信息。

replace()字符串方法

string 方法返回一个替换了字符的新字符串。第一个参数是要查找的子字符串,第二个参数是替换这些子字符串的字符串。在交互式 shell 中输入以下内容以查看示例:

>>> 'mississippi'.replace('s', 'X')
'miXXiXXippi'
>>> 'dog'.replace('d', 'bl')
'blog'
>>> 'jogger'.replace('ger', 's')
'jogs'

我们将在simpleSubHacker.py程序的decryptMessage()中使用replace()字符串方法。

解密消息

为了解密我们的消息,我们将使用已经在simplesubstitutioncipher.py中编程的函数simpleSubstitutionCipher .decryptMessage()。但是simpleSubstitutionCipher.decryptMessage()只使用密钥解密,不使用字母映射,所以我们不能直接使用函数。为了解决这个问题,我们将创建一个decryptWithCipherletterMapping()函数,它接受一个字母映射,将映射转换成一个密钥,然后将密钥和消息传递给simpleSubstitutionCipher.decryptMessage()。函数decryptWithCipherletterMapping()将返回一个解密的字符串。回想一下,简单替换密钥是 26 个字符的字符串,密钥字符串中索引0处的字符是 A 的加密字符,索引1处的字符是 B 的加密字符,依此类推。

为了将一个映射转换成我们容易阅读的解密输出,我们需要首先创建一个占位符密钥,它看起来像这样:['x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x', 'x']。小写的'x'可以用在占位符密钥中,因为实际的密钥只使用大写字母。(您可以使用任何不是大写字母的字符作为占位符。)因为不是所有的字母都有解密,我们需要能够区分密钥列表中已经填充了解密字母的部分和解密还没有解决的部分。' x'表示尚未解决的字母。

让我们看看这些是如何在源代码中组合在一起的:

def decryptWithCipherletterMapping(ciphertext, letterMapping):
    # Return a string of the ciphertext decrypted with the letter mapping,
    # with any ambiguous decrypted letters replaced with an underscore.

    # First create a simple sub key from the letterMapping mapping:
    key = ['x'] * len(LETTERS)
    for cipherletter in LETTERS:
        if len(letterMapping[cipherletter]) == 1:
            # If there's only one letter, add it to the key:
            keyIndex = LETTERS.find(letterMapping[cipherletter][0])
            key[keyIndex] = cipherletter

第 140 行通过将单项式列表['x']复制 26 次来创建占位符列表。因为LETTERS是字母表中的一串字母,len(LETTERS)的计算结果是26。当用于列表和整数时,乘法运算符(*)执行列表复制。

第 141 行的for循环检查LETTERS中的每个字母是否是cipherletter变量,如果密码字母被求解(即letterMapping[cipherletter]中只有一个字母),它就用那个字母替换'x'占位符。

第 144 行的letterMapping[cipherletter][0]是解密函,keyIndex是从find()调用返回的LETTERS中解密函的索引。第 145 行将密钥列表中的这个索引设置为解密字母。

但是,如果密码字母没有解,该函数会为该密码字母插入一个下划线,以指示哪些字符仍然没有解。第 147 行用下划线替换了cipherletter中的小写字母,第 148 行用下划线替换了大写字母:

        else:
            ciphertext = ciphertext.replace(cipherletter.lower(), '_')
            ciphertext = ciphertext.replace(cipherletter.upper(), '_')

在用已求解的字母替换了key中列表的所有部分后,该函数使用join()方法将字符串列表合并成一个单独的字符串,以创建一个简单的替换密钥。这个字符串被传递给simpleSubCipher.py程序中的decryptMessage()函数。

    key = ''.join(key)

    # With the key we've created, decrypt the ciphertext:
    return simpleSubCipher.decryptMessage(key, ciphertext)

最后,第 152 行从decryptMessage ()函数返回解密的消息字符串。现在,我们已经拥有了查找相交字母映射、破解密钥和解密消息所需的所有函数。让我们看一个简单的例子,看看这些函数在交互式 shell 中是如何工作的。

在交互 Shell 中解密

让我们回到我们在第 235 页的的“字母映射帮助函数如何工作”中使用的例子。我们将使用我们在前面的 shell 示例中创建的intersectedMapping变量来解密密文消息'OLQIHXIRCKGNZ PLQRZKBZB MPBKSSIPLC'

対話型シェルに次のように入力します。

>>> simpleSubHacker.decryptWithCipherletterMapping('OLQIHXIRCKGNZ PLQRZKBZB
MPBKSSIPLC', intersectedMapping)
UNCOMFORTABLE INCREASES DISAPPOINT

暗号文を解読すると、「不快なことで失望感が増す」というメッセージが返されました。ご覧のとおり、decryptWithCipherletterMapping()この関数は正常に動作し、完全に復号化された文字列を返します。ただし、この例では、暗号文に含まれるすべての文字を解決しなかった場合に何が起こるかは示していません。暗号文字の復号化に失敗した場合に何が起こるかを確認するには、次のコマンドを使用して、解から暗号文字の合計を削除してintersectedMappingましょう。'M''S'

>>> intersectedMapping['M'] = []
>>> intersectedMapping['S'] = []

次に、次intersectedMappingのコマンドを使用して暗号文を再度復号化してみます。

>>> simpleSubHacker.decryptWithCipherletterMapping('OLQIHXIRCKGNZ PLQRZKBZB
MPBKSSIPLC', intersectedMapping)
UNCOMFORTABLE INCREASES _ISA__OINT

今回は暗号文の一部が解読されませんでした。復号化文字のないパスワード文字はアンダースコアに置き換えられます。

これは非常に短い暗号文であり、解読するのが非常に困難です。通常、暗号化されたメッセージは長くなります。(この例は、解読可能であるように特別に選択されています。この例のように短いメッセージは、通常、ワードパターン法を使用しても解読できません。) より長い暗号化を解読するには、長いメッセージ内の各暗号の暗号マップを作成し、それらを横断する必要があります。一緒に。hackSimpleSub()関数はプログラム内の他の関数を呼び出して、このタスクを実行します。

呼び出しmain()関数

行 155 と 156 は、関数が直接実行され、別の Python プログラムによってモジュールとしてインポートされなかった場合にmain()実行する関数を呼び出しますsimpleSubHacker.py

if __name__ == '__main__':
    main()

これで、simpleSubHacker.pyプログラムで使用されるすべての関数についての説明が完了しました。

注記

私たちのハッキング方法は、空間が暗号化されていない場合にのみ機能します。シンボル セットを拡張すると、暗号プログラムでスペース、数字、句読点、文字を暗号化できるようになり、暗号化されたメッセージの解読が困難になります (ただし不可能ではありません)。このような情報を解読するには、文字の頻度だけでなく、記号セット内のすべての記号の頻度も更新する必要があります。これによりクラッキングがより複雑になるため、本書では文字のみが暗号化されます。

要約する

叫ぶ!このsimpleSubHacker.py手順は非常に複雑です。暗号文字マップを使用して、各暗号文文字の可能な復号文字をモデル化する方法を学びました。また、可能性のある文字をマップに追加し、それらを交差させ、他の解読可能文字のリストから解決された文字を削除することで、可能性のあるキーの数を絞り込む方法についても説明しました。403,291,461,126,605,635,584,000,000 個の可能なキーを強引に見つける代わりに、複雑な Python コードを使用して、元の単純な置換キーのすべてではないにしても、ほとんどを見つけることができます。

単純な置換暗号の主な利点は、使用可能なキーが多数あることです。欠点は、パスワードと辞書ファイル内の単語を比較して、どのパスワードがどの文字に復号化されるかを判断するのが比較的簡単であることです。第 18 章では、何世紀にもわたって解読不可能と考えられていたバージニア暗号と呼ばれる、より強力な複数文字置換暗号を検討します。

練習問題

練習問題の答えは、www.nostarch.com/crackingcodesこの本の Web サイトで見つけることができます。

  1. helloこの単語の単語パターンは何ですか?

  2. mammothgogglesの単語パターンと同じですか?

  3. どの単語がPYYACAOパスワードの平文単語である可能性がありますか? Alleged、、、efficientlyまたはpoodle

18. バージニア暗号を実現するプログラミング

原文: https://inventwithpython.com/cracking/chapter18.html

「私は当時も今も、暗号化の普及によって私たちの安全と自由が得られる利益は、それを使用する犯罪者やテロリストによる避けられない損害をはるかに上回っていると信じています。」

—マット・ブレイザー、AT&T Labs、2001 年 9 月

画像

イタリアの暗号学者ジョヴァン・バッティスタ・ベラッソは、1553 年にバージニア暗号を初めて記述しましたが、最終的にこの暗号は、後年この暗号を再発明した多くの人物の 1 人であるフランスの外交官ブレーズ・ド・バージニア・エルにちなんで命名されました。「解読不可能な暗号」を意味する「le chiffre indéchiffrable」として知られるこの暗号は、19 世紀の英国学者チャールズ・バベッジが解読するまで解読されませんでした。

バージニア暗号には、総当たり攻撃が行われる可能性のあるキーが非常に多く含まれているため、英語検出モジュールを使用した場合でも、本書でこれまで説明した中で最も強力な暗号の 1 つです。第 17 章で学んだ単語パターン攻撃に対しても無敵です。

この章で取り上げるトピック

  • 子供

  • list-append-concatenate プロシージャを使用して文字列を構築する

バージニア暗号での複数のアルファベットキーの使用

シーザー暗号とは異なり、バージニア暗号には複数のキーがあります。バージニア暗号は複数の置換セットを使用するため、複数文字置換暗号となります。単純な換字式暗号とは異なり、バージニア暗号は頻度分析だけでは破ることができません。シーザー暗号のように 0 ~ 25 の数値キーを使用する代わりに、バージニアを表すためにアルファベット キーを使用します。

バージニア キーは、英単語などの文字のシーケンスであり、文字を平文で暗号化する 1 文字のサブキーに分割されます。たとえば、PIZZAバージニア キーを使用する場合、最初のサブキーはP、2 番目のサブキーはI、3 番目と 4 番目のサブキーは両方、Z5 番目のサブキーAは 、最初のサブキーは です。このキーは平文の最初の文字を暗号化し、2 番目のサブキーは暗号化します。サブキーは 2 番目の文字を暗号化します。以下同様です。平文の 6 文字目に達すると、最初のサブキーに戻ります。

図 18-1 に示すように、バージニア暗号を使用することは、複数のシーザー暗号を使用することと同じです。平文全体を 1 つのシーザー暗号で暗号化する代わりに、平文の各文字に異なるシーザー暗号を適用します。

画像

図 18-1: 複数のシーザー暗号を組み合わせたバージニア暗号

各サブキーは整数に変換され、Caesar 暗号化キーとして使用されます。たとえば、この文字はACaesar 暗号キー 0 に対応します。図18-2に示すように、文字はBキー 1 に対応し、以下同様にキー 25 まで対応しますZ

画像

図 18-2: シーザー キーとそれに対応する文字

例を見てみましょう。以下は、ウィガニエのピザの横に表示されている「常識は一般的ではない」というメッセージです。平文は、その下の各文字を暗号化する対応するサブキーがあることを示しています。

COMMONSENSEISNOTSOCOMMON
PIZZAPIZZAPIZZAPIZZAPIZZ

Pサブキー を使用して平文の最初を暗号化するにはC、サブキーに対応する数値キー 15 を使用してシーザー暗号で暗号化し、暗号文字 を生成し、Rサブキーを循環して平文の文字ごとにプロセスを繰り返します。表 18-1 にこのプロセスを示します。平文文字の整数とサブキー (括弧内に指定) を加算して、暗号文文字の整数を取得します。

表 18-1 : Virginia サブキーを使用したアルファベットの暗号化

平文の文字 サブキー 暗号文の文字
C (2) P (15) R (17)
O (14) I (8) W (22)
M (12) Z (25) L (11)
M (12) Z (25) L (11)
O (14) A (0) O (14)
N (13) P (15) C (2)
S (18) I (8) A (0)
E (4) Z (25) D (3)
N (13) Z (25) M (12)
S (18) A (0) S (18)
E (4) P (15) T (19)
I (8) I (8) Q (16)
S (18) Z (25) R (17)
N (13) Z (25) M (12)
O (14) A (0) O (14)
T (19) P (15) I (8)
S (18) I (8) A (0)
O (14) Z (25) N (13)
C (2) Z (25) B (1)
O (14) A (0) O (14)
M (12) P (15) B (1)
M (12) I (8) U (20)
O (14) Z (25) N (13)
N (13) Z (25) M (12)

PIZZAバージニア暗号とキー (サブキー 15、8、25、25、0 で構成される) を使用して、通常の意味での平文を ciphertext に暗号化しますRWLLOC ADMST QR MOI AN BOBUNM

キーが長いほど安全性が高くなります

バージニア キーの文字数が多いほど、暗号化されたメッセージのブルート フォース攻撃に対する耐性が高くなります。PIZZA は 5 文字しかないため、バージニアのキーワードとしては適切な選択ではありません。5 文字のキーには 11881376 通りの組み合わせがあります (26 文字の 5 乗は であるため26 ** 5 = 26×26×26×26×26 = 11881376)。1,100 万個のキーは、一人の人間が力ずくで解読するには多すぎますが、コンピュータなら数時間ですべてを試すことができます。まず、AAAAAキーを使用してメッセージを復号化し、復号化された結果が英語であるかどうかを確認します。AAAAB次に、ピザに到達するまで、 、AAAAC、 などを試行できます。

幸いなことに、キーに文字が追加されるたびに、使用可能なキーの数が 26 倍になります。考えられる鍵が 1000 兆個あると、コンピュータが暗号を解読するには何年もかかるでしょう。表 18-2 に、キーの長さごとに使用できるキーの数を示します。

表 18-2 : バージニア鍵の長さに基づいた可能な鍵の数

|キーの長さ|方程式|使用可能な
キー|||||||||||||||||||||||||||||||||||||| _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ | | | | | | | | | | _ _ _ _---------
126= 26
226 × 26= 676
3676 × 26= 17,576
417,576 × 26= 456,976
5456,976 × 26= 11,881,376
611,881,376 × 26= 308,915,776
7308,915,776 × 26= 8,031,810,176
88,031,810,176 × 26= 208,827,064,576
9208,827,064,576 × 26= 5,429,503,678,976
105,429,503,678,976 × 26= 141,167,095,653,376
11141,167,095,653,376 × 26= 3,670,344,486,987,776|
| 12| 3,670,344,486,987,776 × 26| = 95,428,956,661,682,176|
| 13| 95,428,956,661,682,176 × 26| = 2,481,152,873,203,736,576|
| 14| 2,481,152,873,203,736,576 × 26| = 64,509,974,703,297,150,976|

12 文字以上の長さのキーの場合、ラップトップが妥当な時間内にキーを解読することは不可能です。

辞書攻撃を防ぐキーを選択してください

バージニア キーは、PIZZAこのように実際の単語である必要はありません。12 文字のキーなど、任意の長さの文字の任意の組み合わせを使用できますDURIWKNMFICK実際、辞書に載っている単語は使用しないほうがよいでしょう。暗号解析者は、暗号作成者がキーとして英語の単語を使用していると予想するかもしれませんが、単語はRADIOLOGISTS12 文字のキーでもあり、 .DURIWKNMFICK

辞書にあるすべての英単語を使用しようとするブルート フォース攻撃は、辞書攻撃として知られています。使用可能な 12 文字のキーは 95、428、956、661、682、176 個ありますが、辞書ファイルには 12 文字の単語が約 1800 個しかありません。辞書の 12 文字の単語をキーとして使用すると、ランダムな 3 文字のキー (17,576 個のキーが考えられる) よりも総当たり攻撃が簡単になります。

もちろん、暗号作成者には、バージニア鍵の長さが分からないという利点があります。しかし、暗号解読者は、1 文字のキーをすべて試し、次に 2 文字のキーをすべて試し、というようにすることができ、それでも辞書の単語キーを非常に迅速に見つけることができます。

バージニア暗号プログラムのソースコード

[ファイル] -> [新規ファイル]を選択して、新しいファイル エディタ ウィンドウを開きます。ファイル エディターに次のコードを入力し、名前を付けて保存しvigenereCipher.pypyperclip.py同じディレクトリにあることを確認します。「プログラムの実行」を押しますF5

vigenereCipher.py

# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
    # This text can be downloaded from https://www.nostarch.com/
          crackingcodes/:
    myMessage = """Alan Mathison Turing was a British mathematician,
          logician, cryptanalyst, and computer scientist."""
    myKey = 'ASIMOV'
    myMode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.

    if myMode == 'encrypt':
        translated = encryptMessage(myKey, myMessage)
    elif myMode == 'decrypt':
        translated = decryptMessage(myKey, myMessage)

    print('%sed message:' % (myMode.title()))
    print(translated)
    pyperclip.copy(translated)
    print()
    print('The message has been copied to the clipboard.')


def encryptMessage(key, message):
    return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
    return translateMessage(key, message, 'decrypt')


def translateMessage(key, message, mode):
    translated = [] # Stores the encrypted/decrypted message string.

    keyIndex = 0
    key = key.upper()

    for symbol in message: # Loop through each symbol in message.
        num = LETTERS.find(symbol.upper())
        if num != -1: # -1 means symbol.upper() was not found in LETTERS.
            if mode == 'encrypt':
                num += LETTERS.find(key[keyIndex]) # Add if encrypting.
            elif mode == 'decrypt':
                num -= LETTERS.find(key[keyIndex]) # Subtract if
                      decrypting.

            num %= len(LETTERS) # Handle any wraparound.

            # Add the encrypted/decrypted symbol to the end of translated:
            if symbol.isupper():
                translated.append(LETTERS[num])
            elif symbol.islower():
                translated.append(LETTERS[num].lower())

            keyIndex += 1 # Move to the next letter in the key.
            if keyIndex == len(key):
                keyIndex = 0
        else:
            # Append the symbol without encrypting/decrypting:
            translated.append(symbol)

    return ''.join(translated)


# If vigenereCipher.py is run (instead of imported as a module), call
# the main() function:
if __name__ == '__main__':
    main()

バージニア暗号プログラムの実例

プログラムを実行すると、出力は次のようになります。

Encrypted message:
Adiz Avtzqeci Tmzubb wsa m Pmilqev halpqavtakuoi, lgouqdaf, kdmktsvmztsl, izr
xoexghzr kkusitaaf.
The message has been copied to the clipboard.

该程序打印加密的邮件,并将加密的文本复制到剪贴板。

设置模块、常量和main()函数

程序的开头有描述程序的普通注释、pyperclip模块的一个import语句,以及一个名为LETTERS的变量,该变量包含每个大写字母的字符串。维吉尼亚密码的main()函数类似于本书中的其他main()函数:它从定义变量messagekeymode开始。

# Vigenere Cipher (Polyalphabetic Substitution Cipher)
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

import pyperclip

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def main():
   # This text can be downloaded from https://www.nostarch.com/
         crackingcodes/:
   myMessage = """Alan Mathison Turing was a British mathematician,
         logician, cryptanalyst, and computer scientist."""
   myKey = 'ASIMOV'
   myMode = 'encrypt' # Set to either 'encrypt' or 'decrypt'.

   if myMode == 'encrypt':
       translated = encryptMessage(myKey, myMessage)
   elif myMode == 'decrypt':
       translated = decryptMessage(myKey, myMessage)

   print('%sed message:' % (myMode.title()))
   print(translated)
   pyperclip.copy(translated)
   print()
   print('The message has been copied to the clipboard.')

在运行程序之前,用户在第 10、11 和 12 行设置这些变量。加密或解密的消息(取决于myMode的设置)存储在一个名为translated的变量中,因此它可以打印到屏幕上(第 20 行)并复制到剪贴板上(第 21 行)。

用列表-追加-连接过程构建字符串

这本书里几乎所有的程序都用某种形式的代码构建了一个字符串。也就是说,程序创建一个变量,该变量以空白字符串开始,然后使用字符串连接添加字符。这就是以前的密码程序对translated变量所做的。打开交互式 shell 并输入以下代码:

>>> building = ''
>>> for c in 'Hello world!':
>>>     building += c
>>> print(building)

这段代码遍历字符串'Hello world!'中的每个字符,并将其连接到存储在building中的字符串的末尾。在循环的末尾,building保存着完整的字符串。

尽管字符串连接看起来是一种简单的技术,但在 Python 中却非常低效。从空白列表开始,然后使用append()列表方法会快得多。当您构建完字符串列表后,您可以使用join()方法将该列表转换为单个字符串值。下面的代码与前面的例子做同样的事情,但是速度更快。在交互式 shell 中输入代码:

>>> building = []
>>> for c in 'Hello world!':
>>>     building.append(c)
>>> building = ''.join(building)
>>> print(building)

使用这种方法来构建字符串而不是修改字符串会使程序运行得更快。您可以通过使用time.time()对这两种方法进行计时来看出不同之处。打开一个新的文件编辑器窗口,输入以下代码:

stringTest.py

import time

startTime = time.time()
for trial in range(10000):
  building = ''
  for i in range(10000):
      building += 'x'
print('String concatenation: ', (time.time() - startTime))

startTime = time.time()
for trial in range(10000):
  building = []
  for i in range(10000):
      building.append('x')
  building = ''.join(building)
print('List appending:       ', (time.time() - startTime))

将该程序另存为stringTest.py并运行。输出将如下所示:

String concatenation:  40.317070960998535
List appending:        10.488219022750854

程序stringTest.py将变量startTime设置为当前时间,运行代码使用连接将 10,000 个字符追加到字符串中,然后打印完成连接所用的时间。然后程序将startTime重置为当前时间,运行代码来使用列表追加方法构建一个相同长度的字符串,然后打印完成所用的总时间。在我的电脑上,使用字符串连接来构建 10,000 个字符串,每个字符串包含 10,000 个字符,大约需要 40 秒,但使用列表-追加-连接过程来完成同样的任务只需要 10 秒。如果你的程序构建了很多字符串,使用列表可以让你的程序运行得更快。

我们将使用列表-追加-连接过程为本书中剩余的程序构建字符串。

加密和解密消息

因为加密和解密代码基本相同,我们将为函数translateMessage()创建两个名为encryptMessage()decryptMessage()的包装函数,它们将保存要加密和解密的实际代码。

def encryptMessage(key, message):
    return translateMessage(key, message, 'encrypt')


def decryptMessage(key, message):
    return translateMessage(key, message, 'decrypt')

translateMessage()函数一次一个字符地构建加密(或解密)的字符串。translated中的列表存储了这些字符,以便在字符串构建完成时可以将它们连接起来。

def translateMessage(key, message, mode):
    translated = [] # Stores the encrypted/decrypted message string.

    keyIndex = 0
    key = key.upper()

请记住,维吉尼亚密码只是凯撒密码,只是根据字母在消息中的位置使用不同的密钥。跟踪使用哪个子密钥的keyIndex变量从0开始,因为用于加密或解密消息第一个字符的字母是key[0]

该程序假定密钥全部是大写字母。为了确保密钥有效,第 38 行在key上调用upper()

translateMessage()中的其余代码类似于凯撒密码:

    for symbol in message: # Loop through each symbol in message.
        num = LETTERS.find(symbol.upper())
        if num != -1: # -1 means symbol.upper() was not found in LETTERS.
            if mode == 'encrypt':
                num += LETTERS.find(key[keyIndex]) # Add if encrypting.
            elif mode == 'decrypt':
                num -= LETTERS.find(key[keyIndex]) # Subtract if
                      decrypting.

40 行目のループはfor、ループの各反復でmessage文字 in を変数に設定しますsymbol行 41 では、LETTERS大文字symbolバージョンのインデックスを見つけます。これは、文字を数字に変換する方法です。

行 41numが に設定されていない場合-1大文字バージョン (文字を意味する)LETTERSが で見つかります変数はどのサブキーが使用されているかを追跡し、サブキーは常に評価値になります。symbolsymbolkeyIndexkey[keyIndex]

もちろん、これは単なる 1 文字の文字列です。LETTERSサブキーを整数に変換して、この文字のインデックスを見つける必要があります。次に、この整数は 44 行目のシンボル数に加算され (暗号化されている場合)、または 46 行目のシンボル数から減算されます (復号化されている場合)。

シーザー暗号では、 のnum新しい値がより小さいかどうか0(その場合はそれに加算しますlen(LETTERS))、またはnumの新しい値がより大きいかどうかlen(LETTERS)(その場合はそれから減算しますlen(LETTERS)) をチェックします。これらのチェックはラップアラウンド ケースを処理します。

ただし、どちらの場合も簡単に処理できる方法があります。len(LETTERS)に格納されている整数を法とすれば、num1 行のコードで同じ計算を行うことができます。

            num %= len(LETTERS) # Handle any wraparound.

たとえば、num「はい」の場合、それに追加(すなわち) get-8します。これは として表現でき、その値は ですまたは、 「はい」の場合は、減算してを取得し次のように計算します48 行目のモジュロ演算は、両方のラップアラウンド ケースを処理します。26len(LETTERS)18-8 % 2618num3126531 % 265

暗号化された (または復号化された) 文字が に存在しますLETTERS[num]ただし、暗号化された (または復号化された) 文字の大文字と小文字がsymbol元の大文字と小文字が一致する必要があります。

            # Add the encrypted/decrypted symbol to the end of translated:
            if symbol.isupper():
                translated.append(LETTERS[num])
            elif symbol.islower():
                translated.append(LETTERS[num].lower())

したがって、symbolそれが大文字の場合、行 51 の条件は、True行 52 の文字がすべての場所にLETTERS[num]追加されることです。これは、内のすべての文字がすでに大文字であるためです。translatedLETTERS

ただし、symbol小文字の場合は 53 行目の条件が変更され、54 行目で にTrue小文字が追加されますこれは、暗号化された (または復号化された) メッセージを元のメッセージのケースと一致させる方法です。LETTERS[num]translated

シンボルを変換したので、次の反復で次のサブキーが使用されることを確認したいと思います。56 行目はkeyIndex1 ずつ増加するため、次の反復では次のサブキーのインデックスが使用されます。

            keyIndex += 1 # Move to the next letter in the key.
            if keyIndex == len(key):
                keyIndex = 0

ただし、キーの最後のサブキーにいる場合、その長さはkeyIndexと等しくなります。key行 57 でこれをチェックし、そうであれば、行 58 で最初のサブキーを指すようにkeyIndexリセットします。0key[keyIndex]

インデントは、59 行目のステートメントがelse42 行目のステートメントと対にifなっていることを意味します。

        else:
            # Append the symbol without encrypting/decrypting:
            translated.append(symbol)

文字列内にシンボルが見つからない場合はLETTERS、61 行目のコードが実行されます。これは、symbol数値または句読点 ( や など) の'5'場合に発生します。'?'この場合、61 行目で未変更のシンボルが に追加されますtranslated

でのtranslated文字列の構築が完了したので、空の文字列でメソッドを呼び出しますjoin()

    return ''.join(translated)

この行により、関数は呼び出されたときに暗号化または復号化されたメッセージ全体を返します。

呼び出しmain()関数

68 行目と 69 行目でプログラムのコードが終了します。

if __name__ == '__main__':
    main()

プログラムが単独で実行され、そのプログラムencryptMessage ()とその関数を使用するdecryptMessage()別のプログラムによってインポートされない場合、これらの行はその関数を呼び出しますmain()

要約する

この本の終わりに近づいていますが、バージニア暗号は、最初に学習する暗号プログラムの 1 つであるシーザー暗号よりもそれほど複雑ではないことに注意してください。シーザー暗号をわずかに変更するだけで、総当たりで解読できるよりも多くの可能なキーを持つ暗号を作成しました。

バージニア暗号は、単純な置換クラッキング プログラムで使用される辞書の単語パターンに対して脆弱ではありません。何百年もの間、「解読不能」なバージニア暗号が情報の秘密を守ってきましたが、この暗号も最終的には脆弱になってしまいました。第 19 章と第 20 章では、バージニア暗号を解読できる周波数分析テクニックを学びます。

練習問題

練習問題の答えは、www.nostarch.com/crackingcodesこの本の Web サイトで見つけることができます。

  1. バージニア暗号は、1 つの鍵ではなく複数の鍵を使用する点を除いて、どの暗号に似ていますか?

  2. キー長 10 のバージニア キーに使用可能なキーはいくつありますか?

    1. 何百もの

    2. 何千もの

    3. 何百万もの

    4. 兆以上

  3. バージニア暗号とは何ですか?

19. 周波数分析

原文: https://inventwithpython.com/cracking/chapter19.html

“在混乱中寻找模式的不可言喻的天赋无法完成它的任务,除非他首先把自己沉浸在混乱中。如果它们确实包含模式,他现在没有以任何理性的方式看到它们。但是他头脑中的某些次理性部分可能会起作用。”

——尼尔·斯蒂芬森, Cryptonomicon

画像

在本章中,你将学习如何确定每个英文字母在特定文本中的出现频率。然后,您将这些频率与您的密文的字母频率进行比较,以获得有关原始明文的信息,这将有助于您破解加密。这个确定一个字母在明文和密文中出现频率的过程被称为频率分析。理解频率分析是破解维吉尼亚密码的重要一步。我们将使用字母频率分析来破解第 20 章中的维吉尼亚密码。

本章涵盖的主题

  • 字母频率和符号

  • sort()方法的keyreverse关键字参数

  • 将函数作为值传递,而不是调用函数

  • 使用keys()values()items()方法将字典转换成列表

分析文本中字母的频率

当你掷硬币时,大约一半的时间是正面,一半的时间是反面。也就是头尾的频率应该差不多。我们可以用百分比来表示频率,方法是将一个事件发生的总次数(例如,我们抛了多少次头)除以一个事件的总尝试次数(即我们抛硬币的总次数),然后将商乘以 100。我们可以通过硬币正面或反面的频率来了解它:硬币的重量是公平的还是不公平的,甚至是双面硬币。

我们还可以从密文的字母频率中了解更多信息。英语字母表中有些字母比其他字母用得更频繁。例如,字母ETAO在英语单词中出现频率最高,而字母JXQZ在英语中出现频率较低。我们将利用英语中字母频率的差异来破解维根加密的信息。

図 19-1 は、標準英語における文字の頻度を示しています。このグラフィックは、書籍、新聞、その他の情報源からのテキストを使用して編集されました。

図 19-2 に示すように、これらの文字の頻度を最も頻度の高い文字からE最も頻度の低い文字に並べ替えると、 が最も頻度の高い文字であり、次にT、次に という順になります。A

英語で最も一般的な 6 つの文字は ですETAOIN周波数別の完全なアルファベット順リストは ですETAOINSHRDLCUMWFGYPBVKJXQZ

転置暗号は、元の英語の平文の文字を異なる順序で配置することによってメッセージを暗号化することを思い出してください。これは、暗号文内の文字の頻度が元の平文内の文字の頻度と区別できないことを意味します。たとえば、転置暗号文では、ETとよりも頻繁に出現するA必要があります。QZ

E同様に、シーザー暗号文および単純置換暗号文で最も頻繁に出現する文字は、 、 、などの最も一般的な英語の文字から暗号化されている可能性が高くなりますT同様に、暗号文に出現する頻度が最も低い文字は、平文から派生し平文で暗号化されるA可能性が高くなります。XQZ

画像

図 19-1: 典型的な英語テキストの各文字の頻度分析

画像

図 19-2: 典型的な英語テキストで最も頻繁に出現する文字と最も出現頻度の低い文字

頻度分析は、各サブキーを一度に 1 つずつ総当たり攻撃できるため、バージニア暗号を解読するときに非常に役立ちます。たとえば、メッセージがPIZZAキーで暗号化されている場合、26 ** 5キー全体を一度に見つけるには 11,881,376 個のキーが必要です。ただし、5 つのサブキーのうち 1 つだけをブルート フォースで試すには、26 の可能性を試すだけで済みます。5 つのサブキーのそれぞれに対してこれを行うと、ブルート フォースまたは 130 個のサブキーのみを実行する必要があることになります26 × 5

使用密钥PIZZA,消息中从第一个字母开始的每第五个字母用P加密,从第二个字母开始的每第五个字母用I加密,依此类推。我们可以通过用所有 26 个可能的子密钥解密密文中的每五个字母来暴力破解第一个子密钥。对于第一个子密钥,我们会发现P产生的解密字母比其他 25 个可能的子密钥更匹配英语的字母频率。这将是P是第一个子密钥的一个强有力的指示。然后,我们可以对其他子项重复此操作,直到获得整个项。

匹配字母频率

为了找到消息中的字母频率,我们将使用一种算法,简单地将字符串中的字母从最高频率到最低频率排序。然后算法使用这个有序的字符串来计算这本书所说的频率匹配分数,我们将使用它来确定一个字符串的字母频率与标准英语的字母频率有多相似。

为了计算密文的频率匹配分数,我们从 0 开始,然后每次在密文的六个最频繁的字母中出现一个最频繁的英文字母(E,T,A,O,I,N)时加一个点。在密文的六个最不常用的字母中,每次出现一个最不常用的字母(V、K、J、X、Q 或 Z ),我们都会给分数加一分。

字符串的频率匹配分数可以从 0(字符串的字母频率完全不同于英语字母频率)到 12(字符串的字母频率与常规英语的字母频率相同)。知道密文的频率匹配分数可以揭示关于原始明文的重要信息。

计算简单替换密码的频率匹配分数

我们将使用以下密文来计算使用简单替换密码加密的消息的频率匹配分数:

Sy l nlx sr pyyacao l ylwj eiswi upar lulsxrj isr sxrjsxwjr, ia esmm
rwctjsxsza sj wmpramh, lxo txmarr jia aqsoaxwa sr pqaceiamnsxu, ia esmm caytra
jp famsaqa sj. Sy, px jia pjiac ilxo, ia sr pyyacao rpnajisxu eiswi lyypcor
l calrpx ypc lwjsxu sx lwwpcolxwa jp isr sxrjsxwjr, ia esmm lwwabj sj aqax
px jia rmsuijarj aqsoaxwa. Jia pcsusx py nhjir sr agbmlsxao sx jisr elh.
-Facjclxo Ctrramm

当我们统计这段密文中每个字母出现的频率,从最高频率到最低频率排序,结果是ASRXJILPWMCYOUEQNTHBFZGKVDA是出现频率最高的字母,S是第二高的字母,以此类推,字母D出现频率最低。

在本例中出现频率最高的六个字母(ASRXJI)中,有两个字母(AI)也是英语中出现频率最高的六个字母之一,它们是ETAOIN。因此,我们在频率匹配分数上加 2 分。

密文中最不频繁出现的六个字母是FZGKVD。其中三个字母(ZKV)出现在最不频繁出现的字母集中,它们是VKJXQZ。因此我们在分数上再加三分。基于从该密文导出的频率排序 ASRXJILPWMCYOUEQNTHBFZGKVD,频率匹配分数为 5,如图 19-3 所示。

画像

图 19-3:计算简单替换密码的频率匹配分数

使用简单替换密码加密的密文不会有很高的频率匹配分数。简单替换密文的字母频率与常规英语的字母频率不匹配,因为明文字母被密码字母一一替换。例如,如果字母T被加密成字母J,那么J更有可能在密文中频繁出现,尽管它是英语中出现频率最低的字母之一。

计算换位密码的频率匹配分数

这次,让我们计算使用换位密码加密的密文的频率匹配分数:

"I rc ascwuiluhnviwuetnh,osgaa ice tipeeeee slnatsfietgi tittynecenisl. e
fo f fnc isltn sn o a yrs sd onisli ,l erglei trhfmwfrogotn,l  stcofiit.
aea  wesn,lnc ee w,l eIh eeehoer ros  iol er snh nl oahsts  ilasvih  tvfeh
rtira id thatnie.im ei-dlmf i  thszonsisehroe, aiehcdsanahiec  gv gyedsB
affcahiecesd d lee   onsdihsoc nin cethiTitx  eRneahgin r e teom fbiotd  n
ntacscwevhtdhnhpiwru"

この暗号文で最も頻度の高い文字から最も頻度の低い文字までは ですEISNTHAOCLRFDGWVMUYBPZXQJKEは最も一般的な文字、Iは 2 番目に一般的な文字、というようになります。

この暗号文で最も頻繁に使用される 4 つの文字 ( EINおよびT) は、ETAOIN標準英語でも最も頻繁に使用される文字 ( ) でもあります。同様に、図 19-4 に示すように、暗号文内で最も出現頻度の低い 5 つの文字 ( ZXQJ、 ) も VKJXQZ に出現し、合計頻度一致スコアは 9 です。K

画像

図 19-4: 転置暗号の周波数一致スコアの計算

転置暗号で暗号化された暗号文は、単純な置換暗号文よりもはるかに高い頻度一致スコアを持つ必要があります。その理由は、単純な置換暗号とは異なり、転置暗号では元の平文にある同じ文字が異なる順序で使用されるためです。したがって、各文字の頻度は同じままです。

バージニア暗号の頻度分析

バージニア暗号を解読するには、サブキーを個別に復号する必要があります。これは、1 つのサブキーだけでは十分な情報を復号化できないため、英語の単語を使用した検出に依存できないことを意味します。

代わりに、サブキーの 1 つで暗号化された文字を復号化し、頻度分析を実行して、どの復号化された暗号文が通常の英語に最も近い文字頻度を生成するかを判断します。言い換えれば、どの復号化の頻度一致スコアが最も高いかを見つける必要があります。これは、正しいサブキーが見つかったことを示す良い指標となります。

2 番目、3 番目、4 番目、5 番目のサブキーに対してもこのプロセスを繰り返します。今のところ、キーの長さは 5 文字であると推測しています。(第 20 章では、Kassirsky チェックを使用してキーの長さを決定する方法を学びます。) バージニア暗号では、サブキーごとに 26 回の復号化 (アルファベットの文字の合計数) があるため、コンピューターは、26 + 26 + 26 + 26または5 文字のキーで156 回の復号化。これは、サブキーのすべての可能な組み合わせに対して復号化を実行し、合計 11,881,376 回の復号化を必要とする ( 26 × 26 × 26 × 26 × 26) よりもはるかに簡単です。

バージニア暗号を解読するにはさらに手順があります。これについては、第 20 章で解読プログラムを作成するときに学習します。次に、次の便利な関数を使用して周波数分析を実行するモジュールを作成しましょう。

getLetterCount()文字列引数を受け取り、各文字が文字列内に出現する頻度を含む辞書を返します。

getFrequencyOrder()文字列引数を受け取り、その文字列引数内で最も頻度の高いものから最も頻度の低いものへと並べ替えられた 26 文字の文字列を返します。

englishFreqMatchScore()文字列引数を受け取り、文字の頻度一致スコアを表す 0 ~ 12 の整数を返します。

文字の頻度を一致させるためのソース コード

[ファイル] -> [新規ファイル]を選択して、新しいファイル エディタ ウィンドウを開きます。ファイル エディターに次のコードを入力し、名前を付けて保存しfreqAnalysis.pypyperclip.py同じディレクトリにあることを確認します。「プログラムの実行」を押しますF5

freqAnalysis.py

# Frequency Finder
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'
LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

def getLetterCount(message):
    # Returns a dictionary with keys of single letters and values of the
    # count of how many times they appear in the message parameter:
    letterCount = {
    
    'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0,
           'G': 0, 'H': 0, 'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0,
           'O': 0, 'P': 0, 'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0,
           'W': 0, 'X': 0, 'Y': 0, 'Z': 0}

    for letter in message.upper():
        if letter in LETTERS:
            letterCount[letter] += 1

    return letterCount


def getItemAtIndexZero(items):
    return items[0]


def getFrequencyOrder(message):
    # Returns a string of the alphabet letters arranged in order of most
    # frequently occurring in the message parameter.

    # First, get a dictionary of each letter and its frequency count:
    letterToFreq = getLetterCount(message)

    # Second, make a dictionary of each frequency count to the letter(s)
    # with that frequency:
    freqToLetter = {
    
    }
    for letter in LETTERS:
        if letterToFreq[letter] not in freqToLetter:
            freqToLetter[letterToFreq[letter]] = [letter]
        else:
            freqToLetter[letterToFreq[letter]].append(letter)

    # Third, put each list of letters in reverse "ETAOIN" order, and then
    # convert it to a string:
    for freq in freqToLetter:
        freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)
        freqToLetter[freq] = ''.join(freqToLetter[freq])

    # Fourth, convert the freqToLetter dictionary to a list of
    # tuple pairs (key, value), and then sort them:
    freqPairs = list(freqToLetter.items())
    freqPairs.sort(key=getItemAtIndexZero, reverse=True)

    # Fifth, now that the letters are ordered by frequency, extract all
    # the letters for the final string:
    freqOrder = []
    for freqPair in freqPairs:
        freqOrder.append(freqPair[1])

    return ''.join(freqOrder)


def englishFreqMatchScore(message):
    # Return the number of matches that the string in the message
    # parameter has when its letter frequency is compared to English
    # letter frequency. A "match" is how many of its six most frequent
    # and six least frequent letters are among the six most frequent and
    # six least frequent letters for English.
    freqOrder = getFrequencyOrder(message)

    matchScore = 0
    # Find how many matches for the six most common letters there are:
    for commonLetter in ETAOIN[:6]:
        if commonLetter in freqOrder[:6]:
            matchScore += 1
    # Find how many matches for the six least common letters there are:
    for uncommonLetter in ETAOIN[-6:]:
        if uncommonLetter in freqOrder[-6:]:
            matchScore += 1

    return matchScore

文字をアルファベット順に保存する

行 4 ではETAOIN、 という変数を作成します。この変数には、アルファベットの 26 文字が頻度の高いものから低いものの順に格納されます。

# Frequency Finder
# https://www.nostarch.com/crackingcodes/ (BSD Licensed)

ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'

もちろん、すべての英語のテキストがこの頻度の正確な順序を反映しているわけではありません。Z が q よりも頻繁に使用される一連の文字の頻度が記載されている本を簡単に見つけることができます。たとえば、アーネスト ヴィンセント ライトの小説ではGadsby文字 E がまったく使用されないため、文字の頻度が奇妙なものになります。ただし、ほとんどの場合、ETAOIN 注文はモジュールに含められるほど正確である必要があります。

いくつかの異なる関数では、モジュールはすべて大文字のアルファベット文字列も期待するため、LETTERS5 行目に定数変数を設定します。

LETTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'

LETTERS前のプログラムの変数と同じ目的を果たしますSYMBOLS。文字列インデックスと整数インデックスの間のマッピングを提供します。

getLettersCount()次に、関数がmessage文字列に格納されている各文字の頻度をどのようにカウントするかを見ていきます。

メールの文字数を数える

getLetterCount()この関数はmessage文字列を受け取り、キーが 1 つの大文字文字列で、値が引数message内でその文字が出現する回数を格納する整数である辞書値を返します。

第 10 行通过给变量分配一个字典来创建变量letterCount,该字典将所有键设置为初始值0:

def getLetterCount(message):
    # Returns a dictionary with keys of single letters and values of the
    # count of how many times they appear in the message parameter:
    letterCount = {
    
    'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0,
           'G': 0, 'H': 0, 'I': 0, 'J': 0, 'K': 0, 'L': 0, 'M': 0, 'N': 0,
           'O': 0, 'P': 0, 'Q': 0, 'R': 0, 'S': 0, 'T': 0, 'U': 0, 'V': 0,
           'W': 0, 'X': 0, 'Y': 0, 'Z': 0}

我们通过在第 12 行使用一个for循环检查message中的每个字符,增加与键相关的值,直到它们代表每个字母的计数。

    for letter in message.upper():
        if letter in LETTERS:
            letterCount[letter] += 1

for循环遍历大写版本的message中的每个字符,并将该字符赋给letter变量。在第 13 行,我们检查字符是否存在于LETTERS字符串中,因为我们不想计算message中的非字母字符。当letterLETTERS串的一部分时,第 14 行增加letterCount[letter]处的值。

在第 12 行的for循环结束后,第 16 行的letterCount字典应该有一个计数,显示每个字母在message中出现的频率。本字典从getLetterCount()返回:

    return letterCount

例如,在本章中我们将使用下面的字符串(来自en.wikipedia.org/wiki/Alan_Turing):

"""Alan Mathison Turing was a British mathematician, logician, cryptanalyst, and computer
scientist. He was highly influential in the development of computer science, providing a
formalisation of the concepts of "algorithm" and "computation" with the Turing machine. Turing
is widely considered to be the father of computer science and artificial intelligence. During
World War II, Turing worked for the Government Code and Cypher School (GCCS) at Bletchley Park,
Britain's codebreaking centre. For a time he was head of Hut 8, the section responsible for
German naval cryptanalysis. He devised a number of techniques for breaking German ciphers,
including the method of the bombe, an electromechanical machine that could find settings
for the Enigma machine. After the war he worked at the National Physical Laboratory, where
he created one of the first designs for a stored-program computer, the ACE. In 1948 Turing
joined Max Newman's Computing Laboratory at Manchester University, where he assisted in the
development of the Manchester computers and became interested in mathematical biology. He wrote
a paper on the chemical basis of morphogenesis, and predicted oscillating chemical reactions
such as the Belousov-Zhabotinsky reaction, which were first observed in the 1960s. Turing's
homosexuality resulted in a criminal prosecution in 1952, when homosexual acts were still
illegal in the United Kingdom. He accepted treatment with female hormones (chemical castration)
as an alternative to prison. Turing died in 1954, just over two weeks before his 42nd birthday,
from cyanide poisoning. An inquest determined that his death was suicide; his mother and some
others believed his death was accidental. On 10 September 2009, following an Internet campaign,
British Prime Minister Gordon Brown made an official public apology on behalf of the British
government for "the appalling way he was treated." As of May 2012 a private member's bill was
before the House of Lords which would grant Turing a statutory pardon if enacted."""

对于这个字符串值,它有 135 个 A 实例,30 个 B 实例,依此类推,getLetterCount()将返回如下所示的字典:

{
    
    'A': 135, 'B': 30, 'C': 74, 'D': 58, 'E': 196, 'F': 37, 'G': 39, 'H': 87,
'I': 139, 'J': 2, 'K': 8, 'L': 62, 'M': 58, 'N': 122, 'O': 113, 'P': 36,
'Q': 2, 'R': 106, 'S': 89, 'T': 140, 'U': 37, 'V': 14, 'W': 30, 'X': 3,
'Y': 21, 'Z': 1}

获取元组的第一个成员

第 19 行的getItemAtIndexZero()函数在向其传递一个元组时返回索引0处的项目:

def getItemAtIndexZero(items):
    return items[0]

在程序的后面,我们将把这个函数传递给sort()方法,将字母的频率按数字顺序排序。我们将在第 275 页的上的“将字典条目转换为可排序列表”中详细了解这一点。

按频率排序邮件中的字母

getFrequencyOrder()函数将一个message字符串作为参数,并返回一个包含字母表中 26 个大写字母的字符串,按照它们在message参数中出现的频率排列。如果message是可读的英语而不是随机的胡言乱语,那么这个字符串很可能与ETAOIN常量中的字符串相似,如果不是完全相同的话。getFrequencyOrder()函数中的代码完成了计算字符串频率匹配分数的大部分工作,我们将在第 20 章的维吉尼亚 hacking 程序中使用它。

例如,如果我们将"""Alan Mathison Turing..."""字符串传递给getFrequencyOrder (),该函数将返回字符串' ETIANORSHCLMDGFUPBWYVKXQJZ',因为 E 是该字符串中最常见的字母,接下来是 T、I、A 等等。

getFrequencyOrder()函数由五个步骤组成:

  1. 计数字符串中的字母

  2. 创建频率计数和字母列表的字典

  3. 按相反的顺序排列字母列表

  4. 将该数据转换成元组列表

  5. 将列表转换成函数getFrequencyOrder()返回的最终字符串

让我们依次看看每一步。

getLetterCount()计数字母

getFrequencyOrder()的第一步用message参数调用第 28 行的getLetterCount()来获得一个名为letterToFreq的字典,包含message中每个字母的计数:

def getFrequencyOrder(message):
    # Returns a string of the alphabet letters arranged in order of most
    # frequently occurring in the message parameter.

    # First, get a dictionary of each letter and its frequency count:
    letterToFreq = getLetterCount(message)

如果我们将"""Alan Mathison Turing..."""字符串作为message参数传递,第 28 行给letterToFreq分配如下字典值:

{
    
    'A': 135, 'C': 74, 'B': 30, 'E': 196, 'D': 58, 'G': 39, 'F': 37, 'I': 139,
'H': 87, 'K': 8, 'J': 2, 'M': 58, 'L': 62, 'O': 113, 'N': 122, 'Q': 2,
'P': 36, 'S': 89, 'R': 106, 'U': 37, 'T': 140, 'W': 30, 'V': 14, 'Y': 21,
'X': 3, 'Z': 1}

创建频率计数和字母列表的字典

getFrequencyOrder()freqToLetter2 番目のステップは、キーが頻度カウント、値がそれらの頻度カウントを含むアルファベットのリストである辞書を作成することです。letterToFreq辞書がアルファベットのキーを頻度値にマップし、辞書が頻度キーをアルファベット値のリストにマップすることを考えると、辞書内のキーと値をfreqToLetter反転する必要があります。letterToFreq複数の文字が同じ頻度カウントを持つ可能性があるため、キーと値を反転します。'B'そして、'W'この例では両方の文字が頻度カウントを持つため、辞書キーは一意である必要があるため、これらを のような辞書30に入れる必要があります。{30: ['B', 'W']}それ以外の場合は、同様の{30: 'B', 30: 'W'}辞書値によってキーと値のペアの 1 つが別のペアで単純に上書きされます。

freqToLetter辞書を作成するには、32 行目でまず空の辞書を作成します。

    # Second, make a dictionary of each frequency count to the letter(s)
    # with that frequency:
    freqToLetter = {
    
    }
    for letter in LETTERS:
        if letterToFreq[letter] not in freqToLetter:
            freqToLetter[letterToFreq[letter]] = [letter]
        else:
            freqToLetter[letterToFreq[letter]].append(letter)

行 33 は、 のすべての文字をループしLETTERS、行 34 のステートメントは、ifその文字の頻度、またはletterToFreq[letter]その文字が のキーとしてすでに存在するかどうかをチェックしますfreqToLetterそうでない場合は、行 35 でアルファベット順のリストを値としてキーを追加します。文字の頻度がすでに にキーとして存在する場合freqToLetter、行 37 はその文字をすでに にletterToFreq[letter]あるリストの末尾に追加します。

"""Alan Mathison Turing..."""strings を使用して作成された値の例を使用するとletterToFreqfreqToLetter次のような値が返されるはずです。

{
    
    1: ['Z'], 2: ['J', 'Q'], 3: ['X'], 135: ['A'], 8: ['K'], 139: ['I'],
140: ['T'], 14: ['V'], 21: ['Y'], 30: ['B', 'W'], 36: ['P'], 37: ['F', 'U'],
39: ['G'], 58: ['D', 'M'], 62: ['L'], 196: ['E'], 74: ['C'], 87: ['H'],
89: ['S'], 106: ['R'], 113: ['O'], 122: ['N']}

辞書のキーには頻度カウントが含まれ、その値にはそれらの頻度を持つ文字のリストが含まれることに注意してください。

getFrequencyOrder()3 番目のステップでは、freqToLetter各リスト内の文字列を並べ替えます。freqToLetter[freq]文字のリスト*が計算され、その頻度カウントが であることを思い出してくださいfreq2 つ以上の文字が同じ頻度カウントを持つ可能性があるため、リストを使用します。その場合、リストには 2 つ以上の文字の文字列が含まれます。

複数の文字の出現頻度が同じである場合、ETAOIN文字列内での出現の逆順に文字を並べ替える必要があります。これにより、順序が一貫し、周波数一致スコアが誤って増加する可能性が最小限に抑えられます。

たとえば、スコア付けしようとしている文字列の文字 V、I、N、および K の頻度カウントがすべて同じであるとします。また、文字列内の 4 文字の頻度数は V、I、N、K よりも高く、18 文字の頻度数は低いと仮定します。この例では、これらの文字のプレースホルダーとして を使用しますx図19-5は、4つの文字を順番に並べるとどのようになるかを示しています。

画像

図 19-5: 4 つの文字がETAOIN順番に並んでいる場合、周波数一致スコアには 2 ポイントが与えられます。

この場合、I と N は、この文字列例では V や K ほど頻繁に出現しないにもかかわらず、最も頻繁に出現する上位 6 文字であるため、頻度一致スコアに 2 ポイントが追加されます。周波数一致スコアの範囲は 0 ~ 12 のみであるため、これら 2 つのポイントが大きな違いを生む可能性があります。しかし、同じ頻度の文字を逆の順序で配置することで、文字のスコアが高すぎる可能性を最小限に抑えることができます。図19-6は、4つの文字を逆の順序で示しています。

画像

図 19-6: 4 つの文字の順序が逆の場合、頻度一致スコアは増加しません。

文字を逆の順序で並べることにより、I、N、V、k のランダムな順序によって周波数一致スコアが人為的に増加することを回避します。図19-7に示すように、18文字の出現頻度が高く、4文字の出現頻度が低い場合も同様です。

画像

図 19-7: 頻度の低い文字の順序を逆にしても、ETAOIN一致スコアの増加を回避できます。

逆の並べ替え順序により、K と V が英語の頻度の低い 6 文字のいずれにも一致しないことが保証され、ここでも頻度一致スコアに 2 ポイントが追加されることが回避されます。

辞書内の各リスト値の順序を逆にするには、メソッドをfreqToLetterPython 関数に渡す必要があります。sort()関数またはメソッドを別の関数に渡す方法を見てみましょう。

関数を値で渡す

find()42 行目では、メソッドを呼び出す代わりに、メソッド呼び出しfindに値として渡します。sort()

        freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)

Python では関数を値として考えることができるため、これが可能になります。実際、 という名前の関数を定義することは、spamという名前の変数に関数定義を格納することspamと同じです。例を確認するには、対話型シェルに次のコードを入力します。

>>> def spam():
...   print('Hello!')
...
>>> spam()
Hello!
>>> eggs = spam
>>> eggs()
Hello!

このコード例では、spam()string を出力するために呼び出される関数を定義します'Hello!'これは、変数がspam関数定義を保持することも意味します。次に、spam関数を変数からeggs変数にコピーします。これを実行すると、呼び出しのようにspam()呼び出すことができますeggs()代入ステートメントspamの後に括弧が含まれていないことに注意してください。存在する場合、関数を呼び出し、関数から取得した戻り値を変数に設定します。spam()eggsspam()

関数は値であるため、関数呼び出しのパラメータとして値を渡すことができます。例を確認するには、対話型シェルに次のように入力します。

   >>> def doMath(func):
   ...   return func(10, 5)
   ...
   >>> def adding(a, b):
   ...   return a + b
   ...
   >>> def subtracting(a, b):
   ...   return a - b
   ...
   >>> doMath(adding) # ➊
   15
   >>> doMath(subtracting)
   5

这里我们定义了三个函数:doMath()adding()subtracting()。当我们将adding中的函数传递给doMath()调用 ➊ 时,我们正在将adding赋给变量func,而func(10, 5)正在调用adding()并将105传递给它。因此调用func(10, 5)实际上与调用adding(10, 5)相同。这就是doMath(adding)返回15的原因。同样,当我们将subtracting传递给doMath()调用时,doMath(subtracting)返回5,因为func(10, 5)subtracting(10, 5)相同。

sort()方法传递函数

将函数或方法传递给sort()方法让我们实现不同的排序行为。通常,sort()按字母顺序对列表中的值进行排序:

>>> spam = ['C', 'B', 'A'] 
>>> spam.sort()
>>> spam
['A', 'B', 'C']

但是,如果我们为关键字参数key传递一个函数(或方法),当列表中的每个值被传递给那个函数时,列表中的值就按照函数的返回值排序。例如,我们也可以将ETAOIN.find()字符串方法作为key传递给sort()调用,如下所示:

>>> ETAOIN = 'ETAOINSHRDLCUMWFGYPBVKJXQZ'
>>> spam.sort(key=ETAOIN.find)
>>> spam
['A', 'C', 'B']

当我们将ETAOIN.find传递给sort()方法时,sort()方法首先对每个字符串调用find()方法,以便ETAOIN.find('A')ETAOIN.find('B')ETAOIN.find('C')分别返回索引21911——每个字符串在ETAOIN字符串中的位置。然后sort()使用这些返回的索引,而不是原来的'A''B''C'字符串,对spam列表中的项目进行排序。这就是为什么'A''B''C'字符串被排序为'A''C''B',反映它们在ETAOIN中出现的顺序。

sort()方法反转字母列表

为了以相反的顺序对字母进行排序,我们首先需要通过将ETAOIN.find分配给key来基于ETAOIN字符串对它们进行排序。在对所有字母调用该方法使它们都成为索引后,sort()方法根据字母的数字索引对它们进行排序。

通常,sort()函数按字母或数字顺序对它所调用的任何列表进行排序,这被称为升序。为了以降序、反向字母顺序或反向数字顺序对项目进行排序,我们将True传递给sort()方法的reverse关键字参数。

我们在第 42 行做了所有这些:

    # Third, put each list of letters in reverse "ETAOIN" order, and then
    # convert it to a string:
    for freq in freqToLetter:
        freqToLetter[freq].sort(key=ETAOIN.find, reverse=True)
        freqToLetter[freq] = ''.join(freqToLetter[freq])

この時点で、freqToLetter整数の頻度カウントをキーとして保存し、アルファベット文字列のリストを値として保存する辞書であることを思い出してください。辞書自体ではなく、キーの文字列freqがソートされます。freqToLetter辞書には順序がないため、並べ替えることができません。リスト項目のような「最初」または「最後」のキーと値のペアがありません。

再度使用するfreqToLetter値の例"""Alan Mathison Turing..."""。ループが終了すると、これは次の場所freqToLetterに格納される値になります。

{
    
    1: 'Z', 2: 'QJ', 3: 'X', 135: 'A', 8: 'K', 139: 'I', 140: 'T', 14: 'V',
21: 'Y', 30: 'BW', 36: 'P', 37: 'FU', 39: 'G', 58: 'MD', 62: 'L', 196: 'E',
74: 'C', 87: 'H', 89: 'S', 106: 'R', 113: 'O', 122: 'N'}

3037および58key 文字列はすべて逆順に並べ替えられることに注意してください。ループが実行される前、キーと値のペアは次のようになります{30: ['B', 'W'], 37: ['F', 'U'], 58: ['D', 'M'], ...}ループの後は次のようになります{30: 'BW', 37: 'FU', 58: 'MD', ...}

43 行目のメソッド呼び出しは、join()文字列のリストを 1 つの文字列に変換します。たとえば、freqToLetter[30]の値は['B', 'W']として結合されます'BW'

辞書のリストを頻度順に並べ替えます

getFrequencyOrder()4 番目のステップでは、freqToLetter辞書内の文字列を頻度数で並べ替え、文字列をリストに変換します。ディクショナリ内のキーと値のペアには順序がないため、ディクショナリ内のすべてのキーまたは値のリスト値はランダムな順序の項目のリストになることに注意してください。つまり、このリストも並べ替える必要があります。

key()values()およびitems()辞書メソッドを使用する

keys()、 、values()およびitems()Dictionary メソッドはすべて、辞書の一部を非辞書データ型に変換します。辞書を別のデータ型に変換した後、list()関数を使用してそれをリストに変換できます。

例を確認するには、対話型シェルに次のように入力します。

>>> spam = {
    
    'cats': 10, 'dogs': 3, 'mice': 3}
>>> spam.keys()
dict_keys(['mice', 'cats', 'dogs'])
>>> list(spam.keys())
['mice', 'cats', 'dogs']
>>> list(spam.values())
[3, 10, 3]

辞書内のすべてのキーのリスト値を取得するには、オブジェクトkeys()を返すメソッドを使用し、それを関数dict_keysに渡すことができます。list()という名前の同様のvalues()辞書メソッドはdict_valuesオブジェクトを返します。これらの例は、それぞれ辞書のキーのリストと値のリストを示します。

items()キーと値の両方を取得するには、キーと値のペアのタプルを作成するオブジェクトを返す Dictionary メソッドを使用できますdict_items次に、タプルを に渡すことができますlist()インタラクティブ シェルに次のように入力して、動作を確認します。

>>> spam = {
    
    'cats': 10, 'dogs': 3, 'mice': 3}
>>> list(spam.items())
[('mice', 3), ('cats', 10), ('dogs', 3)]

items()を呼び出すことにより、辞書のキーと値のペアをタプルのリストにlist()変換します。これはまさに辞書を使って文字列を頻度順に数値的に並べ替えるspam必要があることです。freqToLetter

辞書エントリをソート可能なリストに変換する

freqToLetter整数の頻度カウントをキーとして、単一文字の文字列のリストを値として持つ辞書。items()文字列を頻度順に並べ替えるには、メソッドと関数を呼び出して、list()辞書のキーと値のペアのタプルのリストを作成します。次に、このタプルのリストをfreqPairs47 行目という変数に保存します。

    # Fourth, convert the freqToLetter dictionary to a list of
    # tuple pairs (key, value), and then sort them:
    freqPairs = list(freqToLetter.items())

48 行目では、プログラムの前半で定義したgetItemAtIndexZero関数値をsort()メソッド呼び出しに渡します。

    freqPairs.sort(key=getItemAtIndexZero, reverse=True)

getItemAtIndexZero()この関数はタプル内の最初の項目 (この場合は頻度カウントの整数) を取得します。これは、freqPairsの項目が頻度カウントの整数の順に数値的に並べ替えられることを意味します。行 48 はreverseキーワード引数にも渡されるTrueため、タプルは最大頻度カウントから最小頻度カウントまで逆の順序でソートされます。

" ""Alan Mathison Turing..."""を続けると、48 行目を実行すると、次freqPairsの値になります。

[(196, 'E'), (140, 'T'), (139, 'I'), (135, 'A'), (122, 'N'), (113, 'O'),
(106, 'R'), (89, 'S'), (87, 'H'), (74, 'C'), (62, 'L'), (58, 'MD'), (39, 'G'),
(37, 'FU'), (36, 'P'), (30, 'BW'), (21, 'Y'), (14, 'V'), (8, 'K'), (3, 'X'),
(2, 'QJ'), (1, 'Z')]

freqPairs変数は、頻度の高いものから頻度の低いものまでアルファベット順にソートされたタプルのリストになります。各タプルの最初の値は頻度カウントを表す整数で、2 番目の値は頻度カウントに関連付けられた文字を含む文字列です。

ソートされたアルファベット順リストを作成する

getFrequencyOrder()5 番目のステップは、freqPairsその中の。文字が頻度によってソートされた文字列値が必要なので、 のfreqPairs整数値は必要ありません。この変数は52 行目の空のリストで始まり、 freqOrder53 行目のforループによってfreqPairs各タプル1のインデックスにある文字列がfreqOrderの末尾に追加されます。

    # Fifth, now that the letters are ordered by frequency, extract all
    # the letters for the final string:
    freqOrder = []
    for freqPair in freqPairs:
        freqOrder.append(freqPair[1])

例を続けると、ループ終了後の行 53に値として がfreqOrder含まれている必要があります。['E',``'T',``'``I',``'A',``'N',``'O',``'R',``'S',``'H',``'C',``'L',``'MD',``'``G',``'FU',``'P',``'BW',``'Y',``'V',``'K',``'X',``'QJ',``'Z']

行 56 では、join()次のメソッドを使用して文字列を連結することにより、文字列のリストfreqOrderから文字列を作成します。

    return ''.join(freqOrder)

たとえば"""Alan Mathison Turing..."""getFrequencyOrder()文字列を返します'ETIANORSHCLMDGFUPBWYVKXQJZ'この順序によると、文字列例で最も頻繁に使用される文字は E、2 番目に頻繁に使用される文字は T、3 番目に使用頻度が高い文字は I というようになります。

メッセージの文字頻度を文字列値として取得したので、'ETAOINSHRDLCUMWFGYPBVKJXQZ'それを英語の文字頻度 ( ) 文字列値と比較して、それらがどの程度一致しているかを確認できます。

メッセージの頻度一致スコアを計算する

englishFreqMatchScore()この関数はmessage文字列を取得し、文字列の頻度一致スコアを示す と の間の整数を返し0ます12スコアが高いほど、message文字の頻度が通常の英語テキストの頻度に近くなります。

def englishFreqMatchScore(message):
    # Return the number of matches that the string in the message
    # parameter has when its letter frequency is compared to English
    # letter frequency. A "match" is how many of its six most frequent
    # and six least frequent letters are among the six most frequent and
    # six least frequent letters for English.
    freqOrder = getFrequencyOrder(message)

getFrequencyOrder()頻度一致スコアを計算する最初のステップは、関数を呼び出して文字の頻度を並べ替えることですmessage。これは 65 行目で行います。順序付けされた文字列を変数に保存しますfreqOrder

matchScore变量从第 67 行的0开始,并由从第 69 行开始的for循环递增,该循环比较ETAOIN字符串的前六个字母和freqOrder的前六个字母,为它们共有的每个字母给出一个点:

    matchScore = 0
    # Find how many matches for the six most common letters there are:
    for commonLetter in ETAOIN[:6]:
        if commonLetter in freqOrder[:6]:
            matchScore += 1

回想一下,[:6]片段与[0:6]相同,所以第 69 行和第 70 行分别对ETAOINfreqOrder字符串的前六个字母进行了切片。如果字母 E、T、A、O、I 或 N 中的任何一个也在freqOrder字符串的前六个字母中,则第 70 行的条件为True,第 71 行递增matchScore

第 73 到 75 行类似于第 69 到 71 行,除了在这种情况下,它们检查ETAOIN字符串中的最后六个字母(VKJXQZ)是否在freqOrder字符串中的最后六个字母中。如果是,则matchScore递增。

    # Find how many matches for the six least common letters there are:
    for uncommonLetter in ETAOIN[-6:]:
        if uncommonLetter in freqOrder[-6:]:
            matchScore += 1

第 77 行返回matchScore中的整数:

    return matchScore

在计算频率匹配分数时,我们忽略频率顺序中间的 14 个字母。这些中间字母的频率彼此过于相似,无法给出有意义的信息。

总结

在本章中,您学习了如何使用sort()函数按字母或数字顺序对列表值进行排序,以及如何使用reversekey关键字参数以不同方式对列表值进行排序。您学习了如何使用keys()values()items()字典方法将字典转换成列表。您还了解了可以在函数调用中将函数作为值传递。

第 20 章では、この章で作成した周波数分析モジュールを使用して、バージニア暗号を解読します。

練習問題

練習問題の答えは、www.nostarch.com/crackingcodesこの本の Web サイトで見つけることができます。

  1. 周波数分析とは何ですか?

  2. 英語で最もよく使われる 6 つの文字は何ですか?

  3. 次のコードを実行した後の変数には何が含まれますかspam?

    spam = [4, 6, 2, 8]
    spam.sort(reverse=True)
    
  4. 変数に辞書が含まれている場合spam、辞書内のキーのリスト値を取得するにはどうすればよいですか?

おすすめ

転載: blog.csdn.net/wizardforcel/article/details/132480504