Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/universal_proxy/audio/input/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 30 additions & 19 deletions lib/universal_proxy/sendspin/noise.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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>>
Expand Down Expand Up @@ -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
Expand All @@ -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))}
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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}
Expand All @@ -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}
Expand All @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
4 changes: 2 additions & 2 deletions test/support/sendspin_source_peer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion test/universal_proxy/sendspin/noise_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading