ヘロログ
暗号

ヴィジュネル暗号(Vigenère Cipher)

シーザー暗号(シフト暗号)では、すべての文字を同じ数だけずらすため、頻度分析や総当たり攻撃で容易に解読されてしまう。この弱点を克服するために考案されたのがヴィジュネル暗号(Vigenère Cipher)である。

ヴィジュネル暗号は、平文の位置ごとに異なるシフト量を適用する多表式暗号(polyalphabetic cipher)であり、シフト量は「鍵」と呼ばれるキーワードによって決定される。同じ文字でも、位置によって異なる暗号文に変換されるため、単純な頻度分析を無力化できる。

ヴィジュネル暗号の歴史

この暗号の原型は、1553年にイタリアの暗号学者ジョヴァン・バッティスタ・ベッラーゾ(Giovan Battista Bellaso)が発表したものである。その後、フランスの外交官ブレーズ・ド・ヴィジュネル(Blaise de Vigenère, 1523〜1596年)が1586年に著書『Traicté des Chiffres(暗号論)』で改良版を発表した。19世紀にベッラーゾの功績がヴィジュネルに誤って帰属され、以後「ヴィジュネル暗号」の名で知られるようになった。

この暗号は長い間解読不可能とされ、「le chiffre indéchiffrable」(解読不能の暗号)と呼ばれていた。しかし1863年、プロイセンの軍人フリードリヒ・カシスキーが鍵の長さを特定する手法を発表し、ヴィジュネル暗号の神話は崩れた。なお、イギリスの数学者チャールズ・バベッジは1854年頃に独自に同様の解読法を発見していたが、公表しなかった。

アルゴリズム

ヴィジュネル暗号は、シフト暗号を位置ごとに切り替える暗号である。鍵となるキーワードの各文字がシフト量を決定する。

鍵の繰り返し

鍵が平文より短い場合は、平文と同じ長さになるまで鍵を繰り返す。例えば、平文が「attackatdawn」(12文字)で鍵が「LEMON」(5文字)なら、拡張鍵は「LEMONLEMONLE」となる。

暗号化

アルファベットの各文字に0〜25の番号を割り当てる(A=0, B=1, ..., Z=25)。平文のi番目の文字をp_i、鍵のi番目の文字をk_iとすると、暗号文のi番目の文字c_iは次式で求まる。

ヴィジュネル暗号の暗号化
c_i = (p_i + k_i) \bmod 26

復号

ヴィジュネル暗号の復号
p_i = (c_i - k_i) \bmod 26
計算例:「attackatdawn」を鍵「LEMON」で暗号化
平文attackatdawn
LEMONLEMONLE
暗号文LXFOPVEFRNHR

各文字の計算過程:

\begin{aligned} \text{a}(0) + \text{L}(11) &= (0+11) \bmod 26 = 11 \to \text{L} \\ \text{t}(19) + \text{E}(4) &= (19+4) \bmod 26 = 23 \to \text{X} \\ \text{t}(19) + \text{M}(12) &= (19+12) \bmod 26 = 5 \to \text{F} \\ \text{a}(0) + \text{O}(14) &= (0+14) \bmod 26 = 14 \to \text{O} \end{aligned}

同じ文字「t」が2回目と8回目で異なる暗号文(XとF)に変換されている点に注目してほしい。これが多表式暗号の特徴であり、単純な頻度分析を防ぐ仕組みである。

ヴィジュネル表

ヴィジュネル暗号の暗号化・復号にはヴィジュネル表(Vigenère square / tabula recta)が用いられる。これは26行26列の表で、各行がシフト暗号の換字表に対応している。上部に平文の文字、左端に鍵の文字を配置し、交差するセルが暗号文の文字となる。

ABCDEFGHIJKLMNOPQRSTUVWXYZ
AABCDEFGHIJKLMNOPQRSTUVWXYZ
BBCDEFGHIJKLMNOPQRSTUVWXYZA
CCDEFGHIJKLMNOPQRSTUVWXYZAB
DDEFGHIJKLMNOPQRSTUVWXYZABC
EEFGHIJKLMNOPQRSTUVWXYZABCD
FFGHIJKLMNOPQRSTUVWXYZABCDE
GGHIJKLMNOPQRSTUVWXYZABCDEF
HHIJKLMNOPQRSTUVWXYZABCDEFG
IIJKLMNOPQRSTUVWXYZABCDEFGH
JJKLMNOPQRSTUVWXYZABCDEFGHI
KKLMNOPQRSTUVWXYZABCDEFGHIJ
LLMNOPQRSTUVWXYZABCDEFGHIJK
MMNOPQRSTUVWXYZABCDEFGHIJKL
NNOPQRSTUVWXYZABCDEFGHIJKLM
OOPQRSTUVWXYZABCDEFGHIJKLMN
PPQRSTUVWXYZABCDEFGHIJKLMNO
QQRSTUVWXYZABCDEFGHIJKLMNOP
RRSTUVWXYZABCDEFGHIJKLMNOPQ
SSTUVWXYZABCDEFGHIJKLMNOPQR
TTUVWXYZABCDEFGHIJKLMNOPQRS
UUVWXYZABCDEFGHIJKLMNOPQRST
VVWXYZABCDEFGHIJKLMNOPQRSTU
WWXYZABCDEFGHIJKLMNOPQRSTUV
XXYZABCDEFGHIJKLMNOPQRSTUVW
YYZABCDEFGHIJKLMNOPQRSTUVWX
ZZABCDEFGHIJKLMNOPQRSTUVWXY
ヴィジュネル表の使い方

暗号化:上端から平文の文字の列を、左端から鍵の文字の行を探し、交差するセルが暗号文の文字。 復号:左端から鍵の文字の行を探し、その行の中で暗号文の文字を見つけ、その列の上端が平文の文字。

ヴィジュネル表の各行は、シフト暗号の換字表そのものである。鍵が「A」の行はシフト0(平文のまま)、「B」の行はシフト1、「C」の行はシフト2、...、「Z」の行はシフト25に対応する。つまり、ヴィジュネル暗号は複数のシフト暗号を鍵の文字に従って切り替える暗号といえる。

安全性と弱点

ヴィジュネル暗号は、シーザー暗号と比べて格段に安全性が高い。鍵長がn文字の場合、鍵空間は26^n通りとなり、鍵が長いほど総当たり攻撃は困難になる。

鍵長鍵空間
1文字26(シフト暗号と同等)
3文字17,576
5文字11,881,376
10文字約141兆

しかし実際の運用では比較的短い鍵が使われることが多く、鍵の繰り返しによるパターンが弱点となる。

カシスキー法(Kasiski Examination)

1863年にフリードリヒ・カシスキーが発表した手法で、鍵の長さを特定する。暗号文中に同じ文字列が繰り返し出現する場合、その間隔は鍵の長さの倍数である可能性が高い。複数の間隔の最大公約数を求めることで、鍵長を推定できる。

鍵長が判明すれば、暗号文を鍵長ごとに分割し、各グループを独立したシフト暗号として頻度分析で解読できる。

ワンタイムパッドとの関係

もし鍵を平文と同じ長さにし、完全にランダムに生成して一度しか使用しなければ、ヴィジュネル暗号はワンタイムパッド暗号となり、理論上解読不可能になる。しかし、そのような長い鍵を安全に共有・管理することは現実には極めて困難である。

まとめ

項目内容
分類多表式暗号(polyalphabetic cipher)
キーワード(繰り返して使用)
暗号化c_i = (p_i + k_i) \bmod 26
復号p_i = (c_i - k_i) \bmod 26
鍵空間26^nn = 鍵長)
解読手法カシスキー法で鍵長を特定後、頻度分析

ヴィジュネル暗号は約300年にわたり「解読不能」と信じられていたが、カシスキー法の登場により破られた。鍵の繰り返しという構造的弱点を排除するために、鍵を平文と同じ長さのランダム列とするワンタイムパッド暗号が後に考案されることとなる。また、多表式暗号の原理をローター(回転子)で機械的に実現したのがエニグマ暗号機である。

実践

下のウィジェットでヴィジュネル暗号の暗号化・復号を体験できる。

ヴィジュネル暗号エンコーダー / デコーダー

Python実装

Pythonによるヴィジュネル暗号の暗号化・復号の実装を示す。

vigenere_cipher.py
def vigenere_encrypt(plaintext, key):
    result = []
    key = key.upper()
    ki = 0
    for c in plaintext:
        if c.isalpha():
            base = ord('A') if c.isupper() else ord('a')
            shift = ord(key[ki % len(key)]) - ord('A')
            result.append(chr((ord(c) - base + shift) % 26 + base))
            ki += 1
        else:
            result.append(c)
    return ''.join(result)

実行例
def vigenere_decrypt(ciphertext, key):
    result = []
    key = key.upper()
    ki = 0
    for c in ciphertext:
        if c.isalpha():
            shift = ord(key[ki % len(key)]) - ord('A')
            result.append(chr((ord(c.upper()) - ord('A') - shift) % 26 + ord('a')))
            ki += 1
        else:
            result.append(c)
    return ''.join(result)

# 暗号化
ct = vigenere_encrypt("attackatdawn", "LEMON")
print(f"暗号化: attackatdawn -> {ct}")

# 復号
pt = vigenere_decrypt(ct, "LEMON")
print(f"復号:   {ct} -> {pt}")
暗号化: attackatdawn -> LXFOPVEFRNHR 復号: LXFOPVEFRNHR -> attackatdawn