
書記が数学やるだけ#870 ヴィジュネル暗号・転置式暗号
ヴィジュネル暗号の解読法について例を示す。
問題

暗号文:yptumevptJsltatvjywupuimgvizmkdmrvqvuvtjijuigfjftwcltvqxalxffqhzscxmioigqtxamtctqhruuewpgyjiklgfpvtjxvfbwlqnkbwhrfvptyeefbdhwjwuthqfpoioigqetywfhbwlirtbwalvumehvrvmpuhvscpswkcbxvrkqewpgyvptSenuwuUekwzthruqnChxltmhNsugvipxcgbwlqrfmrlrktmhwitvbdalvqxxumfpadmqrpsxuhigyjpvvubwhxkjmnzlfwtskitniglxygkpbwvuewpgykuelpkjmbaskjmhltrtiipse
説明
ヴィジュネル暗号とは,フランスの外交官ブレーズ・ド・ヴィジュネルによる多表式の換字式暗号のことであり,鍵の周期により複数の変換則ができることから頻度分析に強くなる。

ヴィジュネル暗号の解読においては,鍵の周期の推測が重要となる。その手法として,カシスキーテストや一致指数を用いた方法などが知られている。

その他の古典暗号として,転置式暗号を紹介する。これは平文の文字を置換により並べ替えて暗号文を作成するものである。古くは古代ギリシャのスキュタレーがあり,最近ではポリュビオスの暗号表との組み合わせであるADFGVX暗号がWW1中のドイツで用いられた。

エニグマ (Enigma) とは,1918年にドイツの発明家アルトゥール・シェルビウスによって発明され,第二次世界大戦でナチス・ドイツが用いたローター式暗号機である。エニグマ暗号機は理論上3つの換字要素,ローター,反転ローターおよびプラグボードから構成され,膨大な変換則を有した。

解答
一致指数について,鍵長Kに対し以下の関係式が導出できる。

from collections import Counter
def calculate_ic(text): """ 与えられたテキストの一致指数(IC)を計算する関数。
:param text: 暗号文(大文字の英字のみ)
:return: ICの値(0から1の間) """ # 文字の出現回数をカウント freq_counts = Counter(text)
# 総文字数 N = len(text)
# IC 計算式: IC = Σ f_i (f_i - 1) / (N (N - 1)) ic = sum(f * (f - 1) for f in freq_counts.values()) / (N * (N - 1)) if N > 1 else 0 return ic
例: 与えられた暗号文
ciphertext = "yptumevptJsltatvjywupuimgvizmkdmrvqvuvtjijuigfjftwcltvqxalxffqhzscxmioigqtxamtctqhruuewpgyjiklgfpvtjxvfbwlqnkbwhrfvptyeefbdhwjwuthqfpoioigqetywfhbwlirtbwalvumehvrvmpuhvscpswkcbxvrkqewpgyvptSenuwuUekwzthruqnChxltmhNsugvipxcgbwlqrfmrlrktmhwitvbdalvqxxumfpadmqrpsxuhigyjpvvubwhxkjmnzlfwtskitniglxygkpbwvuewpgykuelpkjmbaskjmhltrtiipse"
ic_value = calculate_ic(ciphertext)
print(f"一致指数(IC): {ic_value:.4f}")一致指数(IC): 0.0417
より,方程式を解くとK=7.567…と出てくるので,鍵長は7付近であると考えられる。あくまで確率計算なので,周辺を候補として残しておく。
次にカシスキーテストを実行する。これは暗号文内の繰り返しパターンを調べ,出現位置の差(距離)を求め,最大公約数(GCD)から鍵長を推定する方法である。
from collections import defaultdict
import math
def find_repeated_sequences(text, length=3):
"""
指定した長さの繰り返し文字列を見つけ、それぞれの出現位置を記録
:param text: 暗号文(英字のみ)
:param length: 繰り返し部分の最小長
:return: 繰り返し文字列の辞書 {文字列: [出現位置リスト]}
"""
seq_positions = defaultdict(list)
for i in range(len(text) - length + 1):
seq = text[i:i+length] # 3文字の部分文字列
seq_positions[seq].append(i)
# 2回以上出現するもののみ返す
return {seq: pos for seq, pos in seq_positions.items() if len(pos) > 1}
def calculate_gcd(numbers):
"""
数列の最大公約数(GCD)を求める
:param numbers: 数値のリスト
:return: 最大公約数
"""
return math.gcd(*numbers) if numbers else None
def kasiski_test(ciphertext, min_length=3, max_length=5):
"""
カシスキーテストを実行し、鍵長の候補を推定
:param ciphertext: 暗号文
:param min_length: 検出する繰り返し文字列の最小長
:param max_length: 検出する繰り返し文字列の最大長
:return: 鍵長の候補(最大公約数リスト)
"""
distances = []
# 3文字~5文字の繰り返しを探す
for length in range(min_length, max_length + 1):
repeated_sequences = find_repeated_sequences(ciphertext, length)
for seq, positions in repeated_sequences.items():
print(f"繰り返し: {seq}, 出現位置: {positions}")
# 出現位置の間隔(距離)を計算
gaps = [positions[i+1] - positions[i] for i in range(len(positions) - 1)]
distances.extend(gaps)
print("\n出現間隔(距離)リスト:", distances)
# 間隔の最大公約数(GCD)を計算
gcd = calculate_gcd(distances)
print("\n最大公約数(鍵長の候補):", gcd)
# 暗号文
ciphertext = "YptumevptJsltatvjywupuimgvizmkdmrvqvuvtjijuigfjftwcltvqxalxffqhzscxmioigqtxamtctqhruuewpgyjiklgfpvtjxvfbwlqnkbwhrfvptyeefbdhwjwuthqfpoioigqetywfhbwlirtbwalvumehvrvmpuhvscpswkcbxvrkqewpgyvptSenuwuUekwzthruqnChxltmhNsugvipxcgbwlqrfmrlrktmhwitvbdalvqxxumfpadmqrpsxuhigyjpvvubwhxkjmnzlfwtskitniglxygkpbwvuewpgykuelpkjmbaskjmhltrtiipse"
# カシスキーテストを実行
kasiski_test(ciphertext)繰り返し: ume, 出現位置: [3, 156]
繰り返し: vpt, 出現位置: [6, 114, 186]
繰り返し: gvi, 出現位置: [24, 216]
繰り返し: vtj, 出現位置: [37, 97]
繰り返し: vqx, 出現位置: [53, 245]
繰り返し: ioi, 出現位置: [68, 134]
繰り返し: oig, 出現位置: [69, 135]
繰り返し: igq, 出現位置: [70, 136]
繰り返し: hru, 出現位置: [81, 201]
繰り返し: uew, 出現位置: [84, 300]
繰り返し: ewp, 出現位置: [85, 181, 301]
繰り返し: wpg, 出現位置: [86, 182, 302]
繰り返し: pgy, 出現位置: [87, 183, 303]
繰り返し: gyj, 出現位置: [88, 264]
繰り返し: bwl, 出現位置: [103, 145, 223]
繰り返し: wlq, 出現位置: [104, 224]
繰り返し: bwh, 出現位置: [109, 271]
繰り返し: alv, 出現位置: [153, 243]
繰り返し: tmh, 出現位置: [210, 234]
繰り返し: kjm, 出現位置: [275, 311, 317]
繰り返し: ioig, 出現位置: [68, 134]
繰り返し: oigq, 出現位置: [69, 135]
繰り返し: uewp, 出現位置: [84, 300]
繰り返し: ewpg, 出現位置: [85, 181, 301]
繰り返し: wpgy, 出現位置: [86, 182, 302]
繰り返し: bwlq, 出現位置: [103, 223]
繰り返し: ioigq, 出現位置: [68, 134]
繰り返し: uewpg, 出現位置: [84, 300]
繰り返し: ewpgy, 出現位置: [85, 181, 301]
出現間隔(距離)リスト: [153, 108, 72, 192, 60, 192, 66, 66, 66, 120, 216, 96, 120, 96, 120, 96, 120, 176, 42, 78, 120, 162, 90, 24, 36, 6, 66, 66, 216, 96, 120, 96, 120, 120, 66, 216, 96, 120]
実際にGCDを計算すると1になってしまうので,ここでは5~10のうち公約数である数が最も多いものを鍵長として推測する。
import math
from collections import Counter
# 出現間隔リスト
intervals = [153, 108, 72, 192, 60, 192, 66, 66, 66, 120, 216, 96, 120, 96, 120, 96, 120, 176, 42, 78, 120, 162, 90, 24, 36, 6, 66, 66, 216, 96, 120, 96, 120, 120, 66, 216, 96, 120]
# 5~10の公約数をリストアップ
divisors = [5, 6, 7, 8, 9, 10]
# 各出現間隔に対して、公約数をリスト化
def common_divisors(num, divisors):
return [d for d in divisors if num % d == 0]
# 各出現間隔における公約数の数をカウント
divisor_count = {d: 0 for d in divisors}
for interval in intervals:
divisors_in_interval = common_divisors(interval, divisors)
for divisor in divisors_in_interval:
divisor_count[divisor] += 1
# 結果を表示
print(divisor_count){5: 11, 6: 36, 7: 1, 8: 23, 9: 9, 10: 11}
これより,カシスキーテストからは鍵長は6であると推測される。
鍵長が推測できたら,頻度分析などを用いて鍵を特定していく。今回は最初の手順のみをコードで示す。
from collections import Counter
def split_ciphertext(ciphertext, key_length):
"""
鍵長に基づいて暗号文を分割
:param ciphertext: 暗号文(英字のみ)
:param key_length: 鍵長
:return: 分割された文字列リスト
"""
split_texts = ['' for _ in range(key_length)]
for i, char in enumerate(ciphertext):
split_texts[i % key_length] += char
return split_texts
# 暗号文
ciphertext = "YptumevptJsltatvjywupuimgvizmkdmrvqvuvtjijuigfjftwcltvqxalxffqhzscxmioigqtxamtctqhruuewpgyjiklgfpvtjxvfbwlqnkbwhrfvptyeefbdhwjwuthqfpoioigqetywfhbwlirtbwalvumehvrvmpuhvscpswkcbxvrkqewpgyvptSenuwuUekwzthruqnChxltmhNsugvipxcgbwlqrfmrlrktmhwitvbdalvqxxumfpadmqrpsxuhigyjpvvubwhxkjmnzlfwtskitniglxygkpbwvuewpgykuelpkjmbaskjmhltrtiipse"
# 鍵長の仮定
key_length = 6
#暗号文を鍵長ごとに分割
split_texts = split_ciphertext(ciphertext, key_length)
split_texts['Yvtwgduutqfxqcujpfkvfwpqhtuvscqvuwqtggftvqppgujwngukjjt',
'ppauvmviwxqmtteivbbpbuoebbmmcbepwznmvbmmbxasybmtikeummi',
'tttpirtgcahixqwktwwtdtitwweppxwtutChiwrhdxdxjwnsgpwebhi',
'uJvuzvjfllzoahpljlhyhhoylahusvpSUhhNpllwaumuphzklbplalp',
'msjimqijtxsimrggxqrewqiwilvhwrgeerxsxqrilmqhvxlixwgpsts',
'elymkvjfvfcgtuyfvnfejfgfrvrvkkynkulucrktvfrivkftyvykkre']
6つの分割された文章に対し,それぞれ頻度分析を行い,対応する鍵を推測していく。
import matplotlib.pyplot as plt
from collections import Counter
import string
# 英語テキスト
english_text = """
Yvtwgduutqfxqcujpfkvfwpqhtuvscqvuwqtggftvqppgujwngukjjt
"""
# 小文字に変換し、アルファベット以外の文字を除去
english_text = english_text.lower()
english_text = "".join(filter(lambda x: x in string.ascii_lowercase, english_text))
# 文字頻度をカウント
letter_counts = Counter(english_text)
# 頻度順(降順)にソート
sorted_letters = sorted(letter_counts.items(), key=lambda x: x[1], reverse=True)
# ソートしたデータを取得
letters, frequencies = zip(*sorted_letters)
# ヒストグラムを作成
plt.figure(figsize=(10, 5))
plt.bar(letters, frequencies, color='lightcoral', edgecolor='black')
# グラフを表示
plt.show()
参考:
鍵:cipher
平文:When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them with another, and to assume among the powers of the earth, the separate and equal station to which the Laws of Nature and of Nature's God entitle them, a decent respect to the opinions of mankind requires that they should declare the causes which impel them to the separation.
暗号文:yptumevptJsltatvjywupuimgvizmkdmrvqvuvtjijuigfjftwcltvqxalxffqhzscxmioigqtxamtctqhruuewpgyjiklgfpvtjxvfbwlqnkbwhrfvptyeefbdhwjwuthqfpoioigqetywfhbwlirtbwalvumehvrvmpuhvscpswkcbxvrkqewpgyvptSenuwuUekwzthruqnChxltmhNsugvipxcgbwlqrfmrlrktmhwitvbdalvqxxumfpadmqrpsxuhigyjpvvubwhxkjmnzlfwtskitniglxygkpbwvuewpgykuelpkjmbaskjmhltrtiipse
復号鍵:YSLTWJ
本記事のもくじはこちら: