11
0

ElixirDesktop iOSでWebSocketのCSRFチェックでエラーが出続ける

Posted at

はじめに

この記事は Elixirアドベントカレンダーのシリーズ4の15日目の記事です

前回のクラサバ構成をiOSで動かそうとしたら、起動後にWebSocketのCSRFチェックで延々とエラーが出続けるという不具合踏んだので、その解消方法を解説します

環境

OS macOS Sonoma 14.2
CPU M1
Xcode 15.1

以下の組み合わせでためしたがErlangとElixirのバージョンは関係なかった
Erlang 25.0.4, Elixir 1.13.4
Erlang 26.2.0, Elixir 1.15.7

opensslを以下のバージョンで試したが関係はなかった
1.1.1k
3.2.0

Webサーバーを以下で試したが関係はなかった
cowboy
bandit

エラー内容

起動すると以下の画面でひたすらロードが行われる

スクリーンショット 2023-12-18 0.04.17.png

Xcodeのログを見ると以下のエラーが出ている

 ** (ErlangError) Erlang error: {:notsup, %{c_file_line_num: 49, c_file_name: 'hash_equals.c', c_function_arg_num: -1}, 'Unsupported CRYPTO_memcmp'}
        (crypto 5.1.1) :crypto.hash_equals_nif("3Ah3klaDRN7MXPETRYeMsUry", "3Ah3klaDRN7MXPETRYeMsUry")
        (phoenix 1.7.10) lib/phoenix/socket/transport.ex:498: Phoenix.Socket.Transport.connect_session/3
        (phoenix 1.7.10) lib/phoenix/socket/transport.ex:481: anonymous fn/4 in Phoenix.Socket.Transport.connect_info/3
        (elixir 1.13.4) lib/enum.ex:2396: Enum."-reduce/3-lists^foldl/2-0-"/3
        (phoenix 1.7.10) lib/phoenix/socket/transport.ex:463: Phoenix.Socket.Transport.connect_info/3
        (phoenix 1.7.10) lib/phoenix/transports/websocket.ex:48: 

どうやら:crypto.hash_equals_nifの関数を実行した時に何かしらのエラーが発生したようだ
一番上のエラーメッセージで調べても情報は皆無だった

該当箇所の追跡

一つ前のこれを追跡する

 (phoenix 1.7.10) lib/phoenix/socket/transport.ex:498: Phoenix.Socket.Transport.connect_session/3

 defp connect_session(conn, endpoint, {key, store, {csrf_token_key, init}}) do
    conn = Plug.Conn.fetch_cookies(conn)

    with csrf_token when is_binary(csrf_token) <- conn.params["_csrf_token"],
         cookie when is_binary(cookie) <- conn.cookies[key],
         conn = put_in(conn.secret_key_base, endpoint.config(:secret_key_base)),
         {_, session} <- store.get(conn, cookie, init),
         csrf_state when is_binary(csrf_state) <-
           Plug.CSRFProtection.dump_state_from_session(session[csrf_token_key]),
# ここ        true <- Plug.CSRFProtection.valid_state_and_csrf_token?(csrf_state, csrf_token) do
      session
    else
      _ -> nil
    end
  end

Plug.CSRFProtection.valid_state_and_csrf_token?
という次の手掛かりが得られた、どうやら CSRFトークンのチェックらしい

  def valid_state_and_csrf_token?(state, csrf_token) do
    with <<state_token::@encoded_token_size-binary>> <-
           state,
         <<user_token::@double_encoded_token_size-binary, mask::@encoded_token_size-binary>> <-
           csrf_token do
      valid_masked_token?(state_token, user_token, mask)
    else
      _ -> false
    end
  end

次はvalid_masked_token?

  defp valid_masked_token?(csrf_token, user_token, mask) do
   case Base.url_decode64(user_token) do
     {:ok, user_token} -> Plug.Crypto.masked_compare(csrf_token, user_token, mask)
     :error -> false
   end
 end

次はこちら、別のライブラリへ飛ぶCryptoがついたのでそろそろか?
Plug.Crypto.masked_compare

  def masked_compare(left, right, mask)
     when is_binary(left) and is_binary(right) and is_binary(mask) do
   byte_size(left) == byte_size(right) and byte_size(right) == byte_size(mask) and
     crypto_exor_hash_equals(left, right, mask)
 end

 defp crypto_exor_hash_equals(x, y, z) do
   crypto_hash_equals(mask(x, y), z)
 end

エラーに出てきた関数とほぼ同名の関数がでてきましたね

   # TODO: remove when we require OTP 25.0
  if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :hash_equals, 2) do
    defp crypto_hash_equals(x, y) do
      :crypto.hash_equals(x, y)
    end
  else
    defp crypto_hash_equals(x, y) do
      legacy_secure_compare(x, y, 0)
    end

    defp legacy_secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
      import Bitwise
      xorred = bxor(x, y)
      legacy_secure_compare(left, right, acc ||| xorred)
    end

    defp legacy_secure_compare(<<>>, <<>>, acc) do
      acc === 0
    end
  end

見つけた! ここで :cryptoを使ってるのでここが原因の箇所っぽい

解決策

上のcrypto_hash_equalsはerlangのを使ってて、下のはElixirだけて実装してるっぽいので
下の関数を呼び出せるようにしたい

ライブラリに手を加えるのはわりと簡単で
depsにライブラリのファイルがあるので手を加えて

elixir-app/deps/plug_crypto/lib/plug/crypto.ex:L133
  # TODO: remove when we require OTP 25.0
-  if Code.ensure_loaded?(:crypto) and function_exported?(:crypto, :hash_equals, 2) do
+  if false do  
    defp crypto_hash_equals(x, y) do
      :crypto.hash_equals(x, y)
    end
  else
    defp crypto_hash_equals(x, y) do
      legacy_secure_compare(x, y, 0)
    end

    defp legacy_secure_compare(<<x, left::binary>>, <<y, right::binary>>, acc) do
      import Bitwise
      xorred = bxor(x, y)
      legacy_secure_compare(left, right, acc ||| xorred)
    end

    defp legacy_secure_compare(<<>>, <<>>, acc) do
      acc === 0
    end
  end

_buildの ios_prodを削除して再度ビルドを実行します

動作確認

スクリーンショット 2023-12-18 0.32.57.png

ちゃんと起動して、TODOを追加できました! やったぜ!

最後に

iOSアプリで起動した時にエラーを解消した手順を解説しました

前回予告した起動時のローカスホスト通信は最新版のXcodeだと以下のような画面からポチポチすれば問題なく動きました

スクリーンショット 2023-12-18 0.35.12.png

次回はこの不具合を解消する過程の副産物で スマホアプリで動かすErlangバイナリのビルド方法について解説します

本記事は以上になりますありがとうございました

11
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
0