From: Christopher Date: Tue, 17 Mar 2020 02:24:53 +0000 (-0500) Subject: initial router authentication X-Git-Url: http://git.entropealabs.com/?a=commitdiff_plain;h=1f52c8b5bda987a2cf85e4a777d8b670ff4acb13;p=wampex_router.git initial router authentication --- diff --git a/lib/authentication.ex b/lib/authentication.ex index fbe743c..419fb2d 100644 --- a/lib/authentication.ex +++ b/lib/authentication.ex @@ -1,5 +1,8 @@ defmodule Wampex.Authentication do @moduledoc false + + alias Wampex.Crypto + @enforce_keys [:authid, :authmethods, :secret] defstruct [:authid, :authmethods, :secret] @@ -17,24 +20,13 @@ defmodule Wampex.Authentication do ) do {auth |> Map.get(:secret) - |> pbkdf2(salt, it, len) - |> hash_challenge(ch), %{}} + |> Crypto.pbkdf2(salt, it, len) + |> Crypto.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) + |> Crypto.hash_challenge(ch), %{}} end end diff --git a/lib/crypto.ex b/lib/crypto.ex new file mode 100644 index 0000000..a88dcf0 --- /dev/null +++ b/lib/crypto.ex @@ -0,0 +1,19 @@ +defmodule Wampex.Crypto do + @moduledoc false + def hash_challenge(secret, challenge) do + :sha256 + |> :crypto.hmac(secret, challenge) + |> :base64.encode() + end + + def pbkdf2(secret, salt, iterations, keylen) do + {:ok, derived} = :pbkdf2.pbkdf2(:sha256, secret, salt, iterations, keylen) + Base.encode64(derived) + end + + def random_string(length) do + length + |> :crypto.strong_rand_bytes() + |> Base.encode64() + end +end diff --git a/lib/router/authentication.ex b/lib/router/authentication.ex new file mode 100644 index 0000000..d7eb2ca --- /dev/null +++ b/lib/router/authentication.ex @@ -0,0 +1,59 @@ +defmodule Wampex.Router.Authentication do + @moduledoc false + + alias Wampex.Crypto + alias Wampex.Serializers.JSON + + @wampcra "wampcra" + @salt_length 32 + @key_length 64 + @auth_provider "userdb" + @auth_role "user" + + def authenticate?(methods) do + can_auth(methods) + end + + def method, do: @wampcra + + def challenge(authid, session_id) do + nonce = Crypto.random_string(@salt_length) + salt = Crypto.random_string(@salt_length) + now = DateTime.to_iso8601(DateTime.utc_now()) + + %{ + challenge: + JSON.serialize!(%{ + nonce: nonce, + authprovider: @auth_provider, + authid: authid, + timestamp: now, + authrole: @auth_role, + authmethod: @wampcra, + session: session_id + }), + salt: salt, + keylen: @key_length, + iterations: Enum.random(10_000..22_000) + } + end + + def authenticate(signature, authid, %{ + challenge: challenge, + salt: salt, + keylen: keylen, + iterations: iters + }) do + authid + |> get_secret() + |> Crypto.pbkdf2(salt, iters, keylen) + |> Crypto.hash_challenge(challenge) + |> :pbkdf2.compare_secure(signature) + end + + defp get_secret(_authid), do: "test1234" + + defp can_auth([]), do: false + defp can_auth([@wampcra | _]), do: true + defp can_auth([_ | t]), do: can_auth(t) +end diff --git a/lib/router/session.ex b/lib/router/session.ex index b7c19f4..db54c40 100644 --- a/lib/router/session.ex +++ b/lib/router/session.ex @@ -6,14 +6,16 @@ defmodule Wampex.Router.Session do @max_id 9_007_199_254_740_992 + alias __MODULE__, as: Sess alias StatesLanguage, as: SL alias Wampex.Realm alias Wampex.Roles.{Broker, Dealer, Peer} alias Wampex.Roles.Peer.{Challenge, Welcome, Abort} alias Wampex.Router - alias __MODULE__, as: Sess + alias Wampex.Serializers.JSON alias Broker.{Subscribed, Published, Event} alias Dealer.{Invocation, Registered, Result, Unregistered} + alias Router.Authentication @enforce_keys [:transport, :transport_pid] defstruct [ @@ -36,7 +38,9 @@ defmodule Wampex.Router.Session do :name, :goodbye, :peer_information, + :challenge, error: "wamp.error.protocol_violation", + authentication: Authentication, roles: [Peer, Broker, Dealer], registrations: [], subscriptions: [], @@ -65,6 +69,7 @@ defmodule Wampex.Router.Session do goodbye: binary() | nil, realm: Realm.t(), name: module() | nil, + challenge: map() | nil, roles: [module()], registrations: [], subscriptions: [], @@ -115,7 +120,7 @@ defmodule Wampex.Router.Session do @handle_init, _, @init, - %SL{data: %Sess{transport_pid: t}} = data + data ) do debug("Init") @@ -145,7 +150,7 @@ defmodule Wampex.Router.Session do {data, actions} = case handle_message(msg, roles) do - {actions, id, response} -> + {actions, _id, response} -> {data, response} = maybe_update_response(data, response) debug("Response: #{inspect(response)}") {data, actions} @@ -169,23 +174,98 @@ defmodule Wampex.Router.Session do transport: tt, transport_pid: t, hello: {:hello, realm, dets}, - hello_received: false + hello_received: false, + authentication: auth } = data } = sl ) do - # TODO check for authentication request - send_to_peer( - Peer.welcome(%Welcome{ - session_id: id, - options: %{agent: "WAMPex Router", roles: %{broker: %{}, dealer: %{}}} - }), - tt, - t - ) + {actions, challenge} = + case dets do + %{"authid" => ai, "authmethods" => am} -> + case auth.authenticate?(am) do + true -> + ch = auth.challenge(ai, id) + + send_to_peer( + Peer.challenge(%Challenge{ + auth_method: auth.method, + options: ch + }), + tt, + t + ) + + {[], ch} + + false -> + {[{:next_event, :internal, :abort}], nil} + end + + %{} -> + {[{:next_event, :internal, :abort}], nil} + end {:ok, - %SL{sl | data: %Sess{data | hello_received: true, realm: realm, peer_information: dets}}, - [{:next_event, :internal, :transition}]} + %SL{ + sl + | data: %Sess{ + data + | challenge: challenge, + hello_received: true, + realm: realm, + peer_information: dets + } + }, [{:next_event, :internal, :transition}] ++ actions} + end + + @impl true + def handle_resource( + @handle_authenticate, + _, + @authenticate, + %SL{ + data: %Sess{ + transport: tt, + transport_pid: t, + authentication: auth, + challenge: challenge, + authenticate: {:authenticate, sig, dets} + } + } = sl + ) do + ch = JSON.deserialize!(challenge.challenge) + session_id = get_in(ch, ["session"]) + authid = get_in(ch, ["authid"]) + authrole = get_in(ch, ["authrole"]) + authmethod = get_in(ch, ["authmethod"]) + authprovider = get_in(ch, ["authprovider"]) + + actions = + case auth.authenticate(sig, get_in(ch, ["authid"]), challenge) do + true -> + send_to_peer( + Peer.welcome(%Welcome{ + session_id: session_id, + options: %{ + agent: "WAMPex Router", + authid: authid, + authrole: authrole, + authmethod: authmethod, + authprovider: authprovider, + roles: %{broker: %{}, dealer: %{}} + } + }), + tt, + t + ) + + [{:next_event, :internal, :transition}] + + false -> + [{:next_event, :internal, :transition}, {:next_event, :internal, :abort}] + end + + {:ok, sl, actions} end @impl true diff --git a/test/wampex_test.exs b/test/wampex_test.exs index 30cd19a..c5b562e 100644 --- a/test/wampex_test.exs +++ b/test/wampex_test.exs @@ -19,7 +19,7 @@ defmodule WampexTest do @url "ws://localhost:4000/ws" @auth %Authentication{authid: "entone", authmethods: ["wampcra"], secret: "test1234"} - @realm %Realm{name: "org.entropealabs.iot"} + @realm %Realm{name: "org.entropealabs.iot", authentication: @auth} @roles [Callee, Caller, Publisher, Subscriber] @device "as987d9a8sd79a87ds" @@ -272,15 +272,6 @@ defmodule WampexTest do assert_receive {:registered, id} end - @tag :client - test "authentication" do - callee_name = TestCalleeRespondAuthenticated - session = %Session{@session | realm: %Realm{@session.realm | authentication: @auth}} - Client.start_link(name: callee_name, session: session) - TestCallee.start_link(self(), callee_name, @device) - assert_receive {:registered, id} - end - @tag :abort test "abort" do Process.flag(:trap_exit, true) @@ -306,8 +297,7 @@ defmodule WampexTest do procedure: "com.actuator.#{@device}.light", arg_list: [1], arg_kw: %{color: "#FFFFFF"} - }), - 60 + }) ) assert_receive {:invocation, _, _, _, _, _}