ADFGVX暗号の暗号化/解読用モジュールを書いてみた

  • 8946|ハッキングチャレンジサイトさんの#34でADFGVX形式の暗号が出てきた
  • 自分の手で書けそうだと思ったので書いてみたが、以外と面倒だった
  • ADFGVXの文字で変換後、文字行列を縦に読むのか横に読むのか仕様が分からず、両方を実装するのに苦労した(NumPy.arrayならもう少しラクだったかも)
  • あくまでアルゴリズムの理解用です

以下のサイトを参考に書いてみました。
ありがとうございました。

以下、コードです。

class ADFGVX
  
  def initialize(char_matrix)
    if char_matrix.length == 6
      if char_matrix.first.length == 6
        @table = char_matrix
        @chars = "ADFGVX".split ""
      end
    end
  end

  # 暗号化 ########################

  def encode1(plain)
    plain.each_char.map { |c| translate c }.join
  end

  def encode2(enc, key, read_vertical=false)
    
    # キー文字列の文字の重複を取り除く
    uks = unique_chars key
    uks_len = uks.length
    
    # 文字行列を生成
    m = [uks]
    enc.each_char.each_slice(uks_len) { |row| m.push row }
    
    # 末尾行の要素数に余りがある場合はnilで埋める
    if enc.length % uks_len != 0
      (uks_len - enc.length % uks_len).times { m.last.push nil }
    end

    # 列をソート
    tmp = m.transpose.sort_by { |row| row[0] }
    m = tmp.transpose

    # キー文字列を取り除く
    m.shift

    m = m.transpose if read_vertical
    m.flatten.join
  end

  def encode(plain, key, read_vertical=false)
    enc = encode1 plain
    encode2 enc, key, read_vertical
  end

  # 復号 ##########################

  def decode(cipher, key, read_vertical=false)
    raise "cipher length is not even" if cipher.length.odd?

    # キーに出現する文字をソート
    uk = unique_chars key
    uks = uk.sort
    uks_len = uks.length

    if read_vertical
      # 縦に暗号文を敷き詰める場合
      row_size = cipher.length / uks_len
      m = read cipher, row_size, uk, uks, read_vertical: true
    else
      # 横に暗号文を敷き詰める場合
      row_size = uks_len
      m = read cipher, row_size, uk, uks
    end

    # キ―文字(重複なし&ソート済)を追加
    # このキー文字を元に列をソートし直す
    m.unshift uks

    # 転置してソートし直す
    m = m.transpose
    tmp = Array.new(uks_len) { nil }
    m.each do |row|
      c = row.first
      ki = uk.index c
      tmp[ki] = row
    end
    m = tmp.transpose
    m.shift    # キー文字は解読に不要なので取り除く
    dec = m.flatten.join

    dec.each_char.each_slice(2).map do |pair|
      ri = @chars.index pair.first
      ci = @chars.index pair.last
      @table[ri][ci]
    end.join
  end

  private

    # ADFGVXからなる文字ペアに変換
    def translate(char)
      for ri in 0..5
        for ci in 0..5
          ret = @table[ri][ci]
          return [@chars[ri], @chars[ci]].join if ret == char
        end
      end
    end

    # 文字のダブりをなくす
    def unique_chars(key)
      key.downcase.split("").uniq
    end

    # 列数ずつ暗号文を読み込む
    def read(cipher, n, uk, uks, read_vertical=false)
      m = []
      # 端数が出ない場合
      if cipher.length % uk.length == 0
        cipher.each_char.each_slice(n) do |slice|
          m.push slice
        end
        m = m.transpose if read_vertical
      else
        # 端数が出る場合
        cipher.each_char.each_slice(n) do |slice|
          if slice.length == n then m.push slice
          else

            # 直前までの表を転置→余りを追加(縦読みの場合のみ)
            if read_vertical
              m = m.transpose
              last = Array.new(uk.length) { nil }
            else
              last = Array.new(n) { nil }
            end

            slice.each.with_index do |c, i|
              kc = uk[i]
              ki = uks.index kc    # ソート後の位置
              last[ki] = c
            end
            m.push last

          end
        end
      end
      m
    end

end

結構長いコードになってますが、実際には文字行列を切り刻んでソートしている
だけの内容です。が、Rubyで書いたおかげでむしろこれだけの行数で収まったと
いうべきと思います。each_slice()様様です。まぁPythonのNumPy.arrayで
書くという手もあるにはあったわけですが。

まとめ

  • キー文字を割り当てた後に列ごとソートするので、キーが得られないと解読が難しい
  • キーが判明したとしても、変換表が得られないといずれにせよ解読できない
  • 行列(配列)を切り刻んでいるだけなのでRubyで実装するぶんには比較的楽しい(面倒!?)
  • 実際に暗号が解読できるかではなく、解読する側に時間、人的なコストを必要とさせることが暗号の本質なのかと(当暗号が使用されたのは第一次大戦中だそうです)

Whether it is decodable or not, the most important thing is it's costful.
(解読できようができまいが、手間をかけさせる(ものである)ことが重要だ)