diff --git a/lib/universal_proxy/audio/input/source.ex b/lib/universal_proxy/audio/input/source.ex index 16601a7..47d83ef 100644 --- a/lib/universal_proxy/audio/input/source.ex +++ b/lib/universal_proxy/audio/input/source.ex @@ -2161,6 +2161,9 @@ defmodule UniversalProxy.Audio.Input.Source do defp close_noise(nil), do: :ok + # The rescue is required: decibel 1.0 raises `Decibel.SessionError` on an + # already-closed session, and several teardown paths here can reach the same + # session twice (a re-handshake retiring `previous`, then terminate/2). defp close_noise(session) do Noise.close(session) rescue diff --git a/lib/universal_proxy/sendspin/noise.ex b/lib/universal_proxy/sendspin/noise.ex index 64de306..4a9c9f2 100644 --- a/lib/universal_proxy/sendspin/noise.ex +++ b/lib/universal_proxy/sendspin/noise.ex @@ -27,21 +27,23 @@ defmodule UniversalProxy.Sendspin.Noise do ## Single-process ownership - Decibel keeps session state in the **process dictionary**, keyed by the ref - returned from `Decibel.new/4`. A session therefore only works in the process - that called `start/1`; the struct records that owner and every function - raises `ArgumentError` when called from anywhere else. Drive one session - from one connection process (and hand it no further once that process dies — - the state dies with it). + Decibel keeps session state in the **process dictionary**, keyed by the + opaque handle returned from `Decibel.new/4`. A session therefore only works + in the process that called `start/1`; the struct records that owner and every + function raises `ArgumentError` when called from anywhere else. (Decibel 1.0 + also owner-checks the handle itself and raises `Decibel.SessionError`, but we + keep our own check so the error names the Sendspin session.) Drive one + session from one connection process (and hand it no further once that process + dies — the state dies with it). Distinct sessions in the same process are independent (each has its own - ref), so a process may hold several at once, e.g. across a re-handshake. + handle), so a process may hold several at once, e.g. across a re-handshake. """ @enforce_keys [:ref, :owner, :protocol] defstruct [:ref, :owner, :protocol] - @opaque t :: %__MODULE__{ref: reference(), owner: pid(), protocol: String.t()} + @opaque t :: %__MODULE__{ref: Decibel.session(), owner: pid(), protocol: String.t()} @type suite :: String.t() @type key :: <<_::256>> @@ -134,15 +136,20 @@ defmodule UniversalProxy.Sendspin.Noise do def read_handshake(%__MODULE__{} = session, message) do ref = ref!(session) - if Decibel.is_handshake_complete?(ref) do + if Decibel.handshake_complete?(ref) do {:error, :handshake_complete} else try do {:ok, IO.iodata_to_binary(Decibel.handshake_decrypt(ref, message))} rescue - Decibel.DecryptionError -> {:error, :decrypt_failed} - # A truncated message runs the token reader off the end of the buffer. - MatchError -> {:error, :malformed_handshake} + # Decibel 1.0 reports both failures as DecryptionError and tells them + # apart by `:reason`; a message too short to hold the pattern's tokens + # is `:truncated`, anything else failed to authenticate. + e in Decibel.DecryptionError -> + case e.reason do + :truncated -> {:error, :malformed_handshake} + _other -> {:error, :decrypt_failed} + end end end end @@ -162,7 +169,7 @@ defmodule UniversalProxy.Sendspin.Noise do def write_handshake(%__MODULE__{} = session, payload) do ref = ref!(session) - if Decibel.is_handshake_complete?(ref) do + if Decibel.handshake_complete?(ref) do {:error, :handshake_complete} else {:ok, IO.iodata_to_binary(Decibel.handshake_encrypt(ref, payload))} @@ -173,7 +180,7 @@ defmodule UniversalProxy.Sendspin.Noise do `true` once the handshake has completed and transport mode is active. """ @spec finished?(t()) :: boolean() - def finished?(%__MODULE__{} = session), do: Decibel.is_handshake_complete?(ref!(session)) + def finished?(%__MODULE__{} = session), do: Decibel.handshake_complete?(ref!(session)) @doc """ The 32-byte handshake hash of the completed handshake, or `nil` before that. @@ -182,7 +189,7 @@ defmodule UniversalProxy.Sendspin.Noise do value, so it must be read from the session that ran the handshake. """ @spec handshake_hash(t()) :: key() | nil - def handshake_hash(%__MODULE__{} = session), do: Decibel.get_handshake_hash(ref!(session)) + def handshake_hash(%__MODULE__{} = session), do: Decibel.handshake_hash(ref!(session)) @doc """ Encrypt an outbound transport message. The result is the websocket binary @@ -192,7 +199,7 @@ defmodule UniversalProxy.Sendspin.Noise do def encrypt(%__MODULE__{} = session, plaintext) do ref = ref!(session) - if Decibel.is_handshake_complete?(ref) do + if Decibel.handshake_complete?(ref) do {:ok, IO.iodata_to_binary(Decibel.encrypt(ref, plaintext))} else {:error, :handshake_incomplete} @@ -210,13 +217,13 @@ defmodule UniversalProxy.Sendspin.Noise do def decrypt(%__MODULE__{} = session, ciphertext) do ref = ref!(session) - if Decibel.is_handshake_complete?(ref) do + if Decibel.handshake_complete?(ref) do try do + # Covers a tampered frame (`:authentication_failed`) and one too short + # to hold the AEAD tag (`:truncated`) alike. {:ok, IO.iodata_to_binary(Decibel.decrypt(ref, ciphertext))} rescue Decibel.DecryptionError -> {:error, :decrypt_failed} - # Anything shorter than the AEAD tag never reaches the cipher. - ArgumentError -> {:error, :decrypt_failed} end else {:error, :handshake_incomplete} @@ -225,6 +232,10 @@ defmodule UniversalProxy.Sendspin.Noise do @doc """ Discard the session's keys. Implicit when the owning process exits. + + Not idempotent: decibel 1.0 raises `Decibel.SessionError` on a second close, + so callers that can reach the same session twice must handle it (see + `UniversalProxy.Audio.Input.Source`'s `close_noise/1`). """ @spec close(t()) :: :ok def close(%__MODULE__{} = session), do: Decibel.close(ref!(session)) diff --git a/mix.exs b/mix.exs index 676523f..4d412b8 100644 --- a/mix.exs +++ b/mix.exs @@ -102,7 +102,7 @@ defmodule UniversalProxy.MixProject do {:jason, "~> 1.2"}, {:plug_cowboy, "~> 2.5"}, {:req, "~> 0.7"}, - {:decibel, "~> 0.2"}, + {:decibel, "~> 1.0"}, # Static analysis {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 1f2afd7..1f3e59b 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.20.0", "bb525377ba634cd6d68bac7bff5d98571f738806139d335daca827717c5dc172", [:make, :rebar3], [], "hexpm", "7d41a0dd2c093041ff3779ac5fe8a1585a68ec7cb2dd1de0536bdd2452fd7ba1"}, "credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"}, - "decibel": {:hex, :decibel, "0.2.4", "ed0f0bc2ef76becf2b3a17ef8d43cecb39e2ff762ee7202dbdbddc35f1500e45", [:mix], [{:typedstruct, "~> 0.5.0", [hex: :typedstruct, repo: "hexpm", optional: false]}], "hexpm", "cbc36710a2eba5d40f0203740a629de6813bdc82de7b86c1ecc48b36e06e1090"}, + "decibel": {:hex, :decibel, "1.0.1", "a3d1a090d646870f97ff8699e377aabc13b1a0682228a9094b4d44caa2896451", [:mix], [{:typedstruct, "~> 0.5.0", [hex: :typedstruct, repo: "hexpm", optional: false]}], "hexpm", "2460ce63d915a92e1c9ad761c7632c020e8e9b7f2583263ee29415823970460f"}, "dialyxir": {:hex, :dialyxir, "1.4.8", "7ef671a8aff9948b091d8c30f09467fbb16e77305cda451bce48109a0f5e021c", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cbd5a851571e5dfeb32aaf2e840bfa98b7864cb3071bf2ef5d95d1276b12e072"}, "elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"}, "erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"}, diff --git a/test/support/sendspin_source_peer.ex b/test/support/sendspin_source_peer.ex index ec55917..79c3f3b 100644 --- a/test/support/sendspin_source_peer.ex +++ b/test/support/sendspin_source_peer.ex @@ -143,7 +143,7 @@ defmodule UniversalProxy.SendspinSourcePeer do %{ peer | noise: noise, - handshake_hash: Decibel.get_handshake_hash(noise), + handshake_hash: Decibel.handshake_hash(noise), suite: suite, client_id: client_id } @@ -206,7 +206,7 @@ defmodule UniversalProxy.SendspinSourcePeer do %{ peer | noise: noise, - handshake_hash: Decibel.get_handshake_hash(noise), + handshake_hash: Decibel.handshake_hash(noise), psk: psk, psk_id: psk_id } diff --git a/test/universal_proxy/sendspin/noise_test.exs b/test/universal_proxy/sendspin/noise_test.exs index e170eba..15c2a3f 100644 --- a/test/universal_proxy/sendspin/noise_test.exs +++ b/test/universal_proxy/sendspin/noise_test.exs @@ -160,7 +160,7 @@ defmodule UniversalProxy.Sendspin.NoiseTest do hash = Noise.handshake_hash(session) assert byte_size(hash) == 32 - assert hash == Decibel.get_handshake_hash(ini) + assert hash == Decibel.handshake_hash(ini) end test "transport messages round-trip in both directions", ctx do