ヴィジュネル暗号 in Elixir

Haskell でヴィジュネル暗号を解いているのを見つけた。

 cf. SECCON 2016 ヴィジュネル暗号をHaskellで解いてみる – 0x90

ヴィジュネル暗号の解説は Wikipedia に詳しい。見てみると簡単そうだったからやってみよう。もちろん Elixir で。

defmodule Vigenere do

  def encrypt(plain, key) do
    String.to_charlist(plain)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
    |> to_string
  end

  def decrypt(cipher, key) do
    String.to_charlist(cipher)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {c, k} -> rem((c - k + 26), 26) + 65 end)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")
^o^ > elixir vigenere.exs
CFPE
CODE

英大文字のみに対応。65 を足したり引いたりしてるのは AZ の文字と 0 ~ 25 を対応付けるため。
さて、上記のようにうまくいったわけだけど、一つだけハマりどころがあった。それは Elixir には剰余を求める mod が見当たらないこと。代わりに余りを求める rem を使ったわけだけど、最初 mod と同じつもりで下のように書いたら、復号化に失敗した。

defmodule Vigenere do

  def encrypt(plane, key) do
    String.to_charlist(plane)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
    |> to_string
  end

  def decrypt(cipher, key) do
    String.to_charlist(cipher)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(fn {c, k} -> rem((c - k), 26) + 65 end)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")

結果はこう:

^o^ > elixir vigenere.exs
CFPE
C5DE

O になるべきところが 5 になってしまっている。原因は modrem の違い。Haskell には両方あるので試してみると:

Prelude> mod (-10) 3
2
Prelude> rem (-10) 3
-1

仕方がないので、26 を足してから rem を適用した。でもホントにこれでいいのか?

[追記]

上のスクリプトを見ると、encryptdecrypt もほとんど同じで、違うのは map の中の匿名関数だけだ。なので、共通部分をくくりだしてみた。

defmodule Vigenere do

  def encrypt(plain, key) do
    crypt(plain, key, fn {p, k} -> rem((p + k - 65 * 2), 26) + 65 end)
  end

  def decrypt(cipher, key) do
    crypt(cipher, key, fn {c, k} -> rem((c - k + 26), 26) + 65 end)
  end

  defp crypt(string, key, f) do
    String.to_charlist(string)
    |> Enum.zip(Stream.cycle(String.to_charlist(key)))
    |> Enum.map(f)
    |> to_string
  end

end

IO.puts Vigenere.encrypt("CODE", "ARM")
IO.puts Vigenere.decrypt("CFPE", "ARM")
^o^ > elixir vigenere2.exs
CFPE
CODE

OK。うまくいってる。