ヘロログ
暗号

スキュタレー暗号(Scytale Cipher)

シーザー暗号ヴィジュネル暗号が文字を別の文字に「置換」する換字式暗号であるのに対し、スキュタレー暗号(Scytale Cipher)は文字の「並び順」を入れ替える転置式暗号(transposition cipher)の最古の例である。暗号文には平文と同じ文字がすべて含まれているが、その順序が変わっているため内容を読み取れない。

古代スパルタと暗号通信

スキュタレー(古代ギリシャ語:σκυτάλη, skutálē=「棒」の意)は、紀元前5世紀頃の古代スパルタで軍事通信に用いられたとされる暗号器具である。ペロポネソス戦争(紀元前431〜404年)の時代、スパルタの監督官(エフォロイ)は将軍の派遣時に同じ太さの円筒を2本用意し、一方を手元に残し、もう一方を将軍に持たせた。

この暗号の詳細な記録は、1世紀のギリシャの著述家プルタルコスの『対比列伝(リュサンドロス伝)』に見られる。彼によれば、革ひもを円筒に巻き付けて平文を書き、ほどいた革ひもを使者に持たせて運ばせた。受信者は同じ太さの円筒に巻き付けることで初めて内容を読むことができた。

アルゴリズム

スキュタレー暗号の鍵は、円筒の太さ(直径)である。直径が決まれば円周も決まり、1周あたりに書ける文字数nが確定する。このnがアルゴリズム上の鍵となる。

暗号化

平文をn文字ずつの行に分割して、行方向(横)に書き込む。文字数がnの倍数でない場合は、末尾にダミー文字(冗字)を補う。暗号文は、この表を列方向(縦)に読み出すことで得られる。

復号

暗号文の長さをLとすると、行数は\dfrac{L}{n}となる。暗号文を列方向に\dfrac{L}{n}文字ずつ分割して表に書き込み、行方向に読み出せば平文が復元される。

計算例:「sendmoretroops」を n=4 で暗号化

平文「sendmoretroops」(14文字)をn = 4で暗号化する。14は4の倍数でないため、16文字になるまで冗字「x」を補い、4列×4行の表に横書きする。

列1列2列3列4
行1send
行2more
行3troo
行4psxx

列方向(縦)に読み出すと、暗号文は「SMTP EORS NROX DEOX」(スペースは便宜上の区切り)となる。

図解

スキュタレー暗号の暗号化プロセスを図に示す。平文を横方向に書き込み、縦方向に読み出すことで文字の順序が入れ替わる。

平文を横に書き込む sendmoretroopsxx 縦に読み出す smtpeorsnroxdeox 暗号文: SMTPEORSNROXDEOX 図:スキュタレー暗号の暗号化プロセス(n = 4)
換字式暗号との違い

シーザー暗号などの換字式暗号は文字そのものを別の文字に変える。一方、スキュタレー暗号は文字の並び順だけを変え、使われている文字自体は変わらない。暗号文中の各文字の出現頻度は平文と完全に一致するため、頻度分析は暗号の種類の特定には役立つが、鍵の特定には直接使えない。

安全性と弱点

スキュタレー暗号の鍵は「1周あたりの文字数n」であり、暗号文の長さLの約数に限定される。例えばL = 30なら、nの候補は2, 3, 5, 6, 10, 15の6通りしかないため、すべてを試す総当たり攻撃で容易に解読できる。

さらに、暗号文の1文字目は平文の1文字目と常に一致するという構造的な弱点がある。一定間隔で文字を抽出して意味のある単語が現れれば、その間隔がnである可能性が高い。

まとめ

項目内容
分類転置式暗号(transposition cipher)
円筒の太さ(1周あたりの文字数 n
暗号化平文を n 文字ずつ横書きし、縦に読み出す
復号暗号文を \dfrac{L}{n} 文字ずつ縦に書き込み、横に読み出す
鍵空間暗号文の長さ L の約数の個数(極めて少ない)
安全性極めて低い(約数の総当たりで容易に解読)

スキュタレー暗号は転置式暗号の原型であり、文字の並び替えで情報を秘匿するというアイデアは、現代のブロック暗号においても転置操作として活用されている。より複雑な転置を行う転置式暗号では、鍵の並べ替えパターンを自由に設定することで、鍵空間を大幅に拡大できる。

実践

下のウィジェットでスキュタレー暗号を体験できる。1周の文字数を変えて結果を確認してみよう。

スキュタレー暗号エンコーダー / デコーダー

Python実装

Pythonによるスキュタレー暗号の暗号化・復号の実装を示す。

scytale_cipher.py
import math

def scytale_encrypt(plaintext, n):
    rows = math.ceil(len(plaintext) / n)
    padded = plaintext.ljust(rows * n, 'x')
    cipher = ''
    for col in range(n):
        for row in range(rows):
            cipher += padded[row * n + col]
    return cipher

def scytale_decrypt(ciphertext, n):
    rows = len(ciphertext) // n
    grid = {}
    idx = 0
    for col in range(n):
        for row in range(rows):
            grid[(row, col)] = ciphertext[idx]
            idx += 1
    return ''.join(grid[(r, c)]
                   for r in range(rows)
                   for c in range(n))

実行例
# 暗号化
ct = scytale_encrypt("sendmoretroops", 4)
print(f"暗号化: sendmoretroops -> {ct}")

# 復号
pt = scytale_decrypt(ct, 4)
print(f"復号:   {ct} -> {pt}")

# 総当たり攻撃
print("\n--- 総当たり攻撃 ---")
L = len(ct)
for n in range(2, L):
    if L % n == 0:
        print(f"n={n:2d}: {scytale_decrypt(ct, n)}")
暗号化: sendmoretroops -> smtpeorsnroxdeox 復号: smtpeorsnroxdeox -> sendmoretroopsxx --- 総当たり攻撃 --- n= 2: spmotresndroexox n= 4: sendmoretroopsxx n= 8: smntroeoproxsdex