NnmnLog

Rubyでシーザー暗号を総当たりで解いてみた

作成: 2021-07-25
更新: 2021-07-25
タグ: Ruby 暗号

シーザー暗号とは

シーザー暗号とは、ジュリアス・シーザー(Julius Caesar)が考案した暗号で、アルファベットを何文字かシフトさせることで暗号化を行います。

例えば、3文字シフトさせて暗号化する場合、adになります。 xyzなど最後の文字は、先頭に循環させてabcとします。

平文:   abcdefghijklmnopqrstuvwxyz
暗号文: defghijklmnopqrstuvwxyzabc

アルファベットの数が26個あるので、暗号文に対して25通りの複合化パターンを試せば、どれかは正解の平文にたどり着けることになります。

1文字暗号化するたびに、シフトする文字数を変更することで、より複雑に暗号化することができますが、今回はそこまで考慮しません。

Rubyで解読するスクリプトを書いてみる

ある文字列を暗号化した以下の文字を解読してみたいと思います。

SkwkmkdDroboscxyxkwoiod

以下のスクリプトを書いて、暗号文に対して25通りの複合処理を試してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File: decode.rb
class Caesar
    attr_reader :replace_from, :replace_to

    def initialize(rot: 3)
        rot = rot
        alphabets = "abcdefghijklmnopqrstuvwxyz"

        @replace_from = alphabets + alphabets.upcase
        @replace_to   = (alphabets[rot..] + alphabets[..(rot-1)]) + (alphabets[rot..] + alphabets[..(rot-1)]).upcase
    end

    def encode(plain)
        plain.tr(@replace_from, @replace_to)
    end

    def decode(secret)
        secret.tr(@replace_to, @replace_from)
    end
end

class << Caesar
    def brute_force_attack_dump(secret)
        (1..25).map do |rot|
            decoder = Caesar.new(rot: rot)
            {
                rot: rot,
                plain: decoder.decode(secret)
            }
        end
    end
end

# main
if __FILE__ == $0
    while line = gets
        line.chomp!
        puts "Secret #{line}:"
        Caesar.brute_force_attack_dump(line).each do |decoded_object|
            puts "[ROT:#{decoded_object[:rot].to_s.rjust(2)}] #{decoded_object[:plain]}"
        end
    end
end

Caesarクラスは、何文字シフトさせるかをインスタンス作成にrotで指定することで、その値に従った暗号化処理と複合化処理を提供します。

そして、今回の解読は、クラスメソッドbrute_force_attack_dumpで実施しており、このメソッドに暗号文を渡すと、暗号文を総当たりで複合化した結果が返ってきます。

処理としては、以下のことをやっています。

  1. rot文字シフトさせた文字列で複合するCaesarインスタンスを作成
  2. 与えられた暗号文を複合化する
  3. 上記をrot:1~25まで繰り返す
  4. 結果をオブジェクトのリストで返す

実際に実行すると以下の一覧が得られます。

$ echo "SkwkmkdDroboscxyxkwoiod" | ruby decode.rb 
Secret SkwkmkdDroboscxyxkwoiod:
[ROT: 1] RjvjljcCqnanrbwxwjvnhnc
[ROT: 2] QiuikibBpmzmqavwviumgmb
[ROT: 3] PhthjhaAolylpzuvuhtlfla
[ROT: 4] OgsgigzZnkxkoytutgskekz
[ROT: 5] NfrfhfyYmjwjnxstsfrjdjy
[ROT: 6] MeqegexXlivimwrsreqicix
[ROT: 7] LdpdfdwWkhuhlvqrqdphbhw
[ROT: 8] KcocecvVjgtgkupqpcogagv
[ROT: 9] JbnbdbuUifsfjtopobnfzfu
[ROT:10] IamacatThereisnonameyet
[ROT:11] HzlzbzsSgdqdhrmnmzldxds
[ROT:12] GykyayrRfcpcgqlmlykcwcr
[ROT:13] FxjxzxqQebobfpklkxjbvbq
[ROT:14] EwiwywpPdanaeojkjwiauap
[ROT:15] DvhvxvoOczmzdnijivhztzo
[ROT:16] CuguwunNbylycmhihugysyn
[ROT:17] BtftvtmMaxkxblghgtfxrxm
[ROT:18] AsesuslLzwjwakfgfsewqwl
[ROT:19] ZrdrtrkKyvivzjeferdvpvk
[ROT:20] YqcqsqjJxuhuyidedqcuouj
[ROT:21] XpbprpiIwtgtxhcdcpbtnti
[ROT:22] WoaoqohHvsfswgbcboasmsh
[ROT:23] VnznpngGurervfabanzrlrg
[ROT:24] UmymomfFtqdquezazmyqkqf
[ROT:25] TlxlnleEspcptdyzylxpjpe

この中から、文章を構成していそうな単語を含む文章を抽出してみます。

今回は、isという文字列を含むものを抽出してみると以下の1行がヒットします。

$ echo "SkwkmkdDroboscxyxkwoiod" | ruby decode.rb | grep -i is
[ROT:10] IamacatThereisnonameyet

スペースやドットが削除されているため、少しわかりずらいですが、I am a cat. There is no name yet.という文章が10文字シフトして暗号化されていたことがわかりました。

まとめ

古典暗号として広く知られているシーザー暗号について学びました。

単純な仕組みですが暗号化途中でシフトする文字数を変えるなどすると、組み合わせが増えていくので、その場合の解読方法なども今後考えてみたいです。

おまけ

暗号化は以下のスクリプトで実施しています。

1
2
3
4
5
6
7
8
9
10
if __FILE__ == $0
    plain_text = 'IamacatThereisnonameyet'
    encoder = Caesar.new(rot: ARGV[0].to_i)

    puts "ROT: #{ARGV[0].to_i}"
    puts "Replace from: #{encoder.replace_from}"
    puts "Replace to  : #{encoder.replace_to}"
    puts "Plain text: #{plain_text}"
    puts "Secret message: #{encoder.encode(plain_text)}"
end

実行結果

$ ruby encode.rb 10
ROT: 10
Replace from: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ
Replace to  : klmnopqrstuvwxyzabcdefghijKLMNOPQRSTUVWXYZABCDEFGHIJ
Plain text: IamacatThereisnonameyet
Secret message: SkwkmkdDroboscxyxkwoiod