From 929a12208c205bc6ec513a001525612c160a8fc8 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Christopher=20Cot=C3=A9?= Date: Thu, 20 Feb 2020 16:42:27 -0600 Subject: [PATCH] adds authentication, will also queue messages until handshake is complete, once session is established, will send queued messages --- lib/wampex/authentication.ex | 35 +++++++++++++++- lib/wampex/roles/peer.ex | 31 +++++++++++--- lib/wampex/session.ex | 81 +++++++++++++++++++++++++++++++++--- mix.exs | 1 + mix.lock | 1 + priv/session.json | 26 +++++++++++- test/wampex_test.exs | 4 +- 7 files changed, 162 insertions(+), 17 deletions(-) diff --git a/lib/wampex/authentication.ex b/lib/wampex/authentication.ex index 1325179..93070fe 100644 --- a/lib/wampex/authentication.ex +++ b/lib/wampex/authentication.ex @@ -1,9 +1,40 @@ defmodule Wampex.Authentication do @moduledoc false - defstruct [:authid, :authmethods] + @enforce_keys [:authid, :authmethods, :secret] + defstruct [:authid, :authmethods, :secret] + + @callback handle(challenge :: binary(), auth :: Authentication.t()) :: {binary(), map()} @type t :: %__MODULE__{ authid: binary(), - authmethods: [binary()] + authmethods: [binary()], + secret: binary() } + + def handle( + {"wampcra", %{"challenge" => ch, "salt" => salt, "iterations" => it, "keylen" => len}}, + auth + ) do + {auth + |> Map.get(:secret) + |> pbkdf2(salt, it, len) + |> hash_challenge(ch), %{}} + end + + def handle({"wampcra", %{"challenge" => ch}}, auth) do + {auth + |> Map.get(:secret) + |> hash_challenge(ch), %{}} + end + + defp hash_challenge(secret, challenge) do + :sha256 + |> :crypto.hmac(secret, challenge) + |> :base64.encode() + end + + defp pbkdf2(secret, salt, iterations, keylen) do + {:ok, derived} = :pbkdf2.pbkdf2(:sha256, secret, salt, iterations, keylen) + Base.encode64(derived) + end end diff --git a/lib/wampex/roles/peer.ex b/lib/wampex/roles/peer.ex index 43662cc..5a6581b 100644 --- a/lib/wampex/roles/peer.ex +++ b/lib/wampex/roles/peer.ex @@ -2,15 +2,14 @@ defmodule Wampex.Role.Peer do @moduledoc """ Handles requests and responses for low-level Peer/Session interactions """ - alias StatesLanguage, as: SL - alias Wampex.Session - alias Wampex.Role @behaviour Role @hello 1 @welcome 2 @abort 3 + @challenge 4 + @authenticate 5 @goodbye 6 @error 8 @@ -38,6 +37,17 @@ defmodule Wampex.Role.Peer do } end + defmodule Authenticate do + @moduledoc false + @enforce_keys [:signature, :extra] + defstruct [:signature, :extra] + + @type t :: %__MODULE__{ + signature: binary(), + extra: map() + } + end + @impl true def add(roles), do: roles @@ -54,10 +64,14 @@ defmodule Wampex.Role.Peer do [@goodbye, opts, r] end + @spec authenticate(Authenticate.t()) :: Wampex.message() + def authenticate(%Authenticate{signature: s, extra: e}) do + [@authenticate, s, e] + end + @impl true - def handle([@welcome, session_id, _dets], %SL{data: %Session{} = data} = sl) do - {%SL{sl | data: %Session{data | id: session_id}}, [{:next_event, :internal, :noop}], nil, - {:ok, session_id}} + def handle([@welcome, session_id, _dets], sl) do + {sl, [{:next_event, :internal, :noop}], nil, {:update, :id, session_id}} end @impl true @@ -70,6 +84,11 @@ defmodule Wampex.Role.Peer do {sl, [{:next_event, :internal, :goodbye}], nil, {:update, :goodbye, reason}} end + @impl true + def handle([@challenge, auth_method, extra], sl) do + {sl, [{:next_event, :internal, :challenge}], nil, {:update, :challenge, {auth_method, extra}}} + end + @impl true def handle([@error, type, id, dets, error], sl) do handle([@error, type, id, dets, error, [], %{}], sl) diff --git a/lib/wampex/session.ex b/lib/wampex/session.ex index 82cbc03..86aedc2 100644 --- a/lib/wampex/session.ex +++ b/lib/wampex/session.ex @@ -10,7 +10,7 @@ defmodule Wampex.Session do alias Wampex.Realm alias Wampex.Authentication alias Wampex.Role.Peer - alias Wampex.Role.Peer.Hello + alias Wampex.Role.Peer.{Authenticate, Hello} alias Wampex.Serializer.MessagePack alias __MODULE__, as: Sess alias Wampex.Transport.WebSocket @@ -32,6 +32,8 @@ defmodule Wampex.Session do :name, :roles, :authentication, + :challenge, + message_queue: [], request_id: 0, protocol: "wamp.2.msgpack", transport: WebSocket, @@ -50,7 +52,9 @@ defmodule Wampex.Session do realm: Realm.t(), name: module() | nil, roles: [module()], - authentication: Authentication.t(), + authentication: Authentication.t() | nil, + challenge: {binary(), binary()} | nil, + message_queue: [], request_id: integer(), protocol: binary(), transport: module(), @@ -73,7 +77,7 @@ defmodule Wampex.Session do def handle_call( {:send_request, request}, from, - _, + "Established", %SL{data: %Sess{request_id: r_id, transport: tt, transport_pid: t} = sess} = data ) do request_id = do_send(r_id, tt, t, request) @@ -90,9 +94,20 @@ defmodule Wampex.Session do end @impl true - def handle_cast( + def handle_call( {:send_request, request}, + from, _, + %SL{data: %Sess{message_queue: mq} = sess} = data + ) do + Logger.info("Queueing request: #{inspect(request)}") + {:ok, %SL{data | data: %Sess{sess | message_queue: [{request, from} | mq]}}, []} + end + + @impl true + def handle_cast( + {:send_request, request}, + "Established", %SL{data: %Sess{request_id: r_id, transport: tt, transport_pid: t} = sess} = data ) do request_id = do_send(r_id, tt, t, request) @@ -104,6 +119,16 @@ defmodule Wampex.Session do }, []} end + @impl true + def handle_cast( + {:send_request, request}, + _, + %SL{data: %Sess{message_queue: mq} = sess} = data + ) do + Logger.info("Queueing request: #{inspect(request)}") + {:ok, %SL{data | data: %Sess{sess | message_queue: [{request, nil} | mq]}}, []} + end + @impl true def handle_resource( "Hello", @@ -124,6 +149,20 @@ defmodule Wampex.Session do {:ok, data, [{:next_event, :internal, :hello_sent}]} end + @impl true + def handle_resource( + "HandleChallenge", + _, + "Challenge", + %SL{ + data: %Sess{transport: tt, transport_pid: t, challenge: challenge, authentication: auth} + } = sl + ) do + {signature, extra} = auth.__struct__.handle(challenge, auth) + tt.send_request(t, Peer.authenticate(%Authenticate{signature: signature, extra: extra})) + {:ok, sl, [{:next_event, :internal, :challenged}]} + end + @impl true def handle_resource( "GoodBye", @@ -168,7 +207,37 @@ defmodule Wampex.Session do end @impl true - def handle_resource("Established", _, "Established", data) do + def handle_resource( + "Established", + _, + "Established", + %SL{ + data: %Sess{request_id: r_id, transport: tt, transport_pid: t, message_queue: mq} = sess + } = data + ) do + {request_id, requests} = + mq + |> Enum.reverse() + |> Enum.reduce({r_id, []}, fn {request, from}, {id, requests} -> + r = do_send(id, tt, t, request) + {r, [{r, from} | requests]} + end) + + requests = + Enum.filter(requests, fn + {_, nil} -> false + {_, _} -> true + end) + + {:ok, + %SL{ + data + | data: %Sess{sess | requests: requests, request_id: request_id, message_queue: []} + }, []} + end + + @impl true + def handle_resource("HandleHandshake", _, "Handshake", data) do {:ok, data, []} end @@ -214,7 +283,7 @@ defmodule Wampex.Session do end @impl true - def handle_info({:set_message, message}, "Established", %SL{data: %Sess{} = sess} = data) do + def handle_info({:set_message, message}, _, %SL{data: %Sess{} = sess} = data) do {:ok, %SL{data | data: %Sess{sess | message: message}}, [{:next_event, :internal, :message_received}]} end diff --git a/mix.exs b/mix.exs index e2d9279..f28dd69 100644 --- a/mix.exs +++ b/mix.exs @@ -43,6 +43,7 @@ defmodule Wampex.MixProject do {:ex_doc, "~> 0.21", only: :dev, runtime: false}, {:jason, "~> 1.1"}, {:msgpack, "~> 0.7.0"}, + {:pbkdf2, "~> 2.0"}, {:states_language, "~> 0.2"}, {:websockex, "~> 0.4"} ] diff --git a/mix.lock b/mix.lock index 16d1737..5fb72e4 100644 --- a/mix.lock +++ b/mix.lock @@ -21,6 +21,7 @@ "msgpack": {:hex, :msgpack, "0.7.0", "128ae0a2227c7e7a2847c0f0f73551c268464f8c1ee96bffb920bc0a5712b295", [:rebar3], [], "hexpm", "4649353da003e6f438d105e4b1e0f17757f6f5ec8687a6f30875ff3ac4ce2a51"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "pbkdf2": {:hex, :pbkdf2, "2.0.0", "11c23279fded5c0027ab3996cfae77805521d7ef4babde2bd7ec04a9086cf499", [:rebar3], [], "hexpm", "1e793ce6fdb0576613115714deae9dfc1d1537eaba74f07efb36de139774488d"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, "states_language": {:hex, :states_language, "0.2.8", "f9dfd3c0bd9a9d7bda25ef315f2d90944cd6b2022a7f3c403deb1d4ec451825e", [:mix], [{:elixpath, "~> 0.1.0", [hex: :elixpath, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4.0", [hex: :json_xema, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xema, "~> 0.11.0", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "a5231691e7cb37fe32dc7de54c2dc86d1d60e84c4f0379f3246e55be2a85ec78"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"}, diff --git a/priv/session.json b/priv/session.json index 439a1b5..1329a99 100644 --- a/priv/session.json +++ b/priv/session.json @@ -18,7 +18,7 @@ "Type": "Task", "Resource": "Hello", "TransitionEvent": ":hello_sent", - "Next": "Established", + "Next": "Handshake", "Catch": [ { "ErrorEquals": [":abort"], @@ -27,6 +27,20 @@ ] }, + "Handshake": { + "Type": "Choice", + "Resource": "HandleHandshake", + "Choices": [ + { + "StringEquals": ":message_received", + "Next": "HandleMessage" + }, + { + "StringEquals": ":abort", + "Next": "Abort" + } + ] + }, "Established": { "Type": "Choice", "Resource": "Established", @@ -68,9 +82,19 @@ { "StringEquals": ":goodbye", "Next": "GoodBye" + }, + { + "StringEquals": ":challenge", + "Next": "Challenge" } ] }, + "Challenge": { + "Type": "Task", + "Resource": "HandleChallenge", + "Next": "Handshake", + "TransitionEvent": ":challenged" + }, "Event": { "Type": "Task", "Resource": "HandleEvent", diff --git a/test/wampex_test.exs b/test/wampex_test.exs index d0ea1b5..6b8ae23 100644 --- a/test/wampex_test.exs +++ b/test/wampex_test.exs @@ -15,7 +15,7 @@ defmodule WampexTest do @realm %Realm{name: "com.myrealm"} @roles [Callee, Caller, Publisher, Subscriber] @device "as987d9a8sd79a87ds" - @auth %Authentication{authid: "entone", authmethods: ["wampcra"]} + @auth %Authentication{authid: "entone", authmethods: ["wampcra"], secret: "test1234"} @session %Session{url: @url, realm: @realm, roles: @roles, authentication: @auth} @@ -172,6 +172,6 @@ defmodule WampexTest do }) ) - assert_receive {:event, _, _, _, _, _} + assert_receive {:event, _, _, _, _, _}, 2000 end end -- 2.45.3