Engineering Note

プログラミングなどの技術的なメモ

【暗号技術入門】単一換字暗号と頻度分析

 

cipher

結城浩氏の「暗号技術入門 第3版 秘密の国のアリス」を読んでいく中で、折角なので自分でも勉強ついでにコーディングをしてみました。

今回は単一換字暗号と頻度分析について学んでいきます。

 

 

単一換字暗号とは

単一換字暗号とは、各アルファベットにおいて一対一の対応関係を作り暗号化します。

シーザー暗号では、ただアルファベットを一律でずらすだけのため、鍵空間としては26パターンしかないですが、単一換字暗号の場合は26!となるため鍵空間は大きくなります。

 

頻度分析とは

頻度分析とは、平文で使われる文字の頻度を分析するものです。

頻度分析ではブルートフォースアタックで解読するのは困難となりますが、文字や単語の出現頻度などで推測することにより解読することを可能とします。

そのため、平文は長い方が解読しやすくなります。

 

Pythonで単一換字暗号を実装

では、Pythonで単一換字暗号を実装していきます。

 

from collections import defaultdict
import re
import string
import random

def key_gen():
    key = list(string.ascii_uppercase)
    random.shuffle(key)

    key_dic = {}
    for i, s in enumerate(list(string.ascii_lowercase)):
        key_dic[s] = key[i]

    return key_dic

def enc_simple_substitution_cipher(lines, key_dic):
    enc_lines = ""
    for s in lines:
        enc_lines += key_dic[s]

    return enc_lines

lines = """some plain text""".lower()

lines = re.sub(r"[,.!?':;*()\-\n ]", "", lines)
key_dic = key_gen()
enc_lines = enc_simple_substitution_cipher(lines, key_dic)

print(enc_lines)

# 実行結果
# JGRFTCYFVCKJGTTGSTURBFWVZGPQVCACTCKJGTTGSTURBCLQVCACTKCLQVGCKJGTTGSTURBANLQVGPANLQVGPCKJGTTGSTURBANLQVCARTGIYVRTCKJGTTGSTURBBGKTGIKQRSKGCKJGTTGSTURBIGBKGSGRIRJCVCTZANFBTKKQGACRVARKGKRSGBTKQGACRVGBNFWUTNJSGRDTUGSFVGKRVTUNFWUQSRATCARVCTZJGRTKQFSCTZGSSNSKKUNFVIBGOGSQRKKKCVGBTVZFBVGKKGPQVCACTVZKCVGBAGICBTUGYRAGNYRLJCWFCTZSGYFKGTUGTGLQTRTCNBTNWFGKKTUGSGKUNFVIJGNBGRBIQSGYGSRJVZNBVZNBGNJOCNFKMRZTNINCTRVTUNFWUTURTMRZLRZBNTJGNJOCNFKRTYCSKTFBVGKKZNFSGIFTAUBNMCKJGTTGSTURBBGOGSRVTUNFWUBGOGSCKNYTGBJGTTGSTURBSCWUTBNMCYTUGCLQVGLGBTRTCNBCKURSITNGPQVRCBCTKRJRICIGRCYTUGCLQVGLGBTRTCNBCKGRKZTNGPQVRCBCTLRZJGRWNNICIGRBRLGKQRAGKRSGNBGUNBDCBWWSGRTCIGRVGTKINLNSGNYTUNKG

 

上記はある文章を単一換字で暗号化してみました。

これを頻度分析で解読していきたいと思います。

 

Pythonで頻度分析をしてみる

それでは上記の暗号文を頻度分析で解読してみます。

インタラクティブに文字を置換できるように以下のメソッドを追加してみます。

 

def frequency_analysis(enc_lines):
    dict = defaultdict(int)

    for word in enc_lines:
        dict[word] += 1
    
    dict = sorted(dict.items(), key=lambda x:x[1], reverse=True)
    print(dict)
    print(enc_lines)

    while (True):
        target = input("対象の文字:")
        str = input("置き換える文字:")

        enc_lines = enc_lines.replace(target, str)

        print(enc_lines)
        
# 実行結果
[('G', 88), ('T', 75), ('R', 53), ('C', 52), ('K', 45), ('N', 41), ('B', 40), ('V', 33), ('S', 32), ('U', 29), ('F', 21), ('J', 20), ('Q', 20), ('A', 17), ('I', 17), ('Z', 15), ('L', 15), ('Y', 11), ('W', 11), ('P', 6), ('O', 5), ('M', 4), ('D', 2)]
JGRFTCYFVCKJGTTGSTURBFWVZGPQVCACTCKJGTTGSTURBCLQVCACTKCLQVGCKJGTTGSTURBANLQVGPANLQVGPCKJGTTGSTURBANLQVCARTGIYVRTCKJGTTGSTURBBGKTGIKQRSKGCKJGTTGSTURBIGBKGSGRIRJCVCTZANFBTKKQGACRVARKGKRSGBTKQGACRVGBNFWUTNJSGRDTUGSFVGKRVTUNFWUQSRATCARVCTZJGRTKQFSCTZGSSNSKKUNFVIBGOGSQRKKKCVGBTVZFBVGKKGPQVCACTVZKCVGBAGICBTUGYRAGNYRLJCWFCTZSGYFKGTUGTGLQTRTCNBTNWFGKKTUGSGKUNFVIJGNBGRBIQSGYGSRJVZNBVZNBGNJOCNFKMRZTNINCTRVTUNFWUTURTMRZLRZBNTJGNJOCNFKRTYCSKTFBVGKKZNFSGIFTAUBNMCKJGTTGSTURBBGOGSRVTUNFWUBGOGSCKNYTGBJGTTGSTURBSCWUTBNMCYTUGCLQVGLGBTRTCNBCKURSITNGPQVRCBCTKRJRICIGRCYTUGCLQVGLGBTRTCNBCKGRKZTNGPQVRCBCTLRZJGRWNNICIGRBRLGKQRAGKRSGNBGUNBDCBWWSGRTCIGRVGTKINLNSGNYTUNKG

まず最初の実行結果から各文字の出現回数を降順で表示しています。

文字の頻度ではe、次にtが多いので各文字を置換してみます。

 

JeRFtCYFVCKJetteStURBFWVZePQVCACtCKJetteStURBCLQVCACtKCLQVeCKJetteStURBANLQVePANLQVePCKJetteStURBANLQVCARteIYVRtCKJetteStURBBeKteIKQRSKeCKJetteStURBIeBKeSeRIRJCVCtZANFBtKKQeACRVARKeKRSeBtKQeACRVeBNFWUtNJSeRDtUeSFVeKRVtUNFWUQSRAtCARVCtZJeRtKQFSCtZeSSNSKKUNFVIBeOeSQRKKKCVeBtVZFBVeKKePQVCACtVZKCVeBAeICBtUeYRAeNYRLJCWFCtZSeYFKetUeteLQtRtCNBtNWFeKKtUeSeKUNFVIJeNBeRBIQSeYeSRJVZNBVZNBeNJOCNFKMRZtNINCtRVtUNFWUtURtMRZLRZBNtJeNJOCNFKRtYCSKtFBVeKKZNFSeIFtAUBNMCKJetteStURBBeOeSRVtUNFWUBeOeSCKNYteBJetteStURBSCWUtBNMCYtUeCLQVeLeBtRtCNBCKURSItNePQVRCBCtKRJRICIeRCYtUeCLQVeLeBtRtCNBCKeRKZtNePQVRCBCtLRZJeRWNNICIeRBRLeKQRAeKRSeNBeUNBDCBWWSeRtCIeRVetKINLNSeNYtUNKe

 

上記の"ette"の文字は"better"ではないかと推測されるため、各文字も併せて置換してみます。

 

beRFtCYFVCKbettertURBFWVZePQVCACtCKbettertURBCLQVCACtKCLQVeCKbettertURBANLQVePANLQVePCKbettertURBANLQVCARteIYVRtCKbettertURBBeKteIKQRrKeCKbettertURBIeBKereRIRbCVCtZANFBtKKQeACRVARKeKRreBtKQeACRVeBNFWUtNbreRDtUerFVeKRVtUNFWUQrRAtCARVCtZbeRtKQFrCtZerrNrKKUNFVIBeOerQRKKKCVeBtVZFBVeKKePQVCACtVZKCVeBAeICBtUeYRAeNYRLbCWFCtZreYFKetUeteLQtRtCNBtNWFeKKtUereKUNFVIbeNBeRBIQreYerRbVZNBVZNBeNbOCNFKMRZtNINCtRVtUNFWUtURtMRZLRZBNtbeNbOCNFKRtYCrKtFBVeKKZNFreIFtAUBNMCKbettertURBBeOerRVtUNFWUBeOerCKNYteBbettertURBrCWUtBNMCYtUeCLQVeLeBtRtCNBCKURrItNePQVRCBCtKRbRICIeRCYtUeCLQVeLeBtRtCNBCKeRKZtNePQVRCBCtLRZbeRWNNICIeRBRLeKQRAeKRreNBeUNBDCBWWreRtCIeRVetKINLNreNYtUNKe

 

今度は"bettert"と続いているため、前後を考えると"isbetterthan"という文章ではないかと推測してみます…。

 

こんなことを繰り返していると、平文が以下の"The Zen of Python"であることがわかります。

 

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

 

参考

暗号技術入門 第3版 秘密の国のアリス