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

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/00638016-af42-0cf0-6023-20bcb1260445.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
 ```

 https://github.com/phoenixframework/phoenix/blob/a66e7c33019a3d564c4f7f2e48b77efb63c54ad7/lib/phoenix/socket/transport.ex#L498C16-L498C16

```elixir
 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トークンのチェックらしい


https://github.com/elixir-plug/plug/blob/53f304fc6b39976d342c48ac15a971a986271ef5/lib/plug/csrf_protection.ex#L207

```elixir
  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?`

 https://github.com/elixir-plug/plug/blob/53f304fc6b39976d342c48ac15a971a986271ef5/lib/plug/csrf_protection.ex#L378

 ```elixir
   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`


 https://github.com/elixir-plug/plug_crypto/blob/94373d1249697a9c84f72f35ab984ce4aac10da1/lib/plug/crypto.ex#L113C18-L113C18

 ```elixir
   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
 ```

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

https://github.com/elixir-plug/plug_crypto/blob/94373d1249697a9c84f72f35ab984ce4aac10da1/lib/plug/crypto.ex#L134

```elixir
   # 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にライブラリのファイルがあるので手を加えて

```diff_elixir: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/fe986735-a247-1f44-36be-8527ecd3a6e7.png)

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

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

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

![スクリーンショット 2023-12-18 0.35.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/fb9a09e0-34d7-6664-8e75-3da0fbc29ff5.png)

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

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