Skip to content

Commit 8644397

Browse files
committed
Start extracting device auth for an rpc experiment
1 parent 4356461 commit 8644397

File tree

4 files changed

+101
-76
lines changed

4 files changed

+101
-76
lines changed

lib/nerves_hub/products.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,4 +313,11 @@ defmodule NervesHub.Products do
313313
:nope
314314
end)
315315
end
316+
317+
def shared_secrets_enabled?() do
318+
# NervesHubWeb.DeviceSocket should really be :device_auth or :product_auth
319+
Application.get_env(:nerves_hub, NervesHubWeb.DeviceSocket, [])
320+
|> Keyword.get(:shared_secrets, [])
321+
|> Keyword.get(:enabled, false)
322+
end
316323
end

lib/nerves_hub/rpc/device_auth.ex

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
defmodule NervesHub.RPC.DeviceAuth do
2+
@moduledoc false
3+
4+
alias NervesHub.Devices
5+
alias NervesHub.Products
6+
7+
alias Plug.Crypto
8+
9+
# Default 90 seconds max age for the signature
10+
@default_max_hmac_age 90
11+
12+
def connect_device({:shared_secrets, x_headers}) do
13+
headers = Map.new(x_headers)
14+
15+
with :ok <- check_shared_secret_enabled(),
16+
{:ok, key, salt, verification_opts} <- decode_from_headers(headers),
17+
{:ok, auth} <- get_shared_secret_auth(key),
18+
{:ok, signature} <- Map.fetch(headers, "x-nh-signature"),
19+
{:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts),
20+
{:ok, device} <- get_or_maybe_create_device(auth, identifier) do
21+
{:ok, Devices.preload_product(device)}
22+
else
23+
error ->
24+
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
25+
auth: :shared_secrets,
26+
reason: error,
27+
product_key: Map.get(headers, "x-nh-key", "*empty*")
28+
})
29+
30+
{:error, :invalid_auth}
31+
end
32+
end
33+
34+
defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do
35+
with [digest_str, iter_str, klen_str] <- String.split(alg, "-"),
36+
digest <- String.to_existing_atom(String.downcase(digest_str)),
37+
{iterations, ""} <- Integer.parse(iter_str),
38+
{key_length, ""} <- Integer.parse(klen_str),
39+
{signed_at, ""} <- Integer.parse(headers["x-nh-time"]),
40+
{:ok, key} <- Map.fetch(headers, "x-nh-key") do
41+
expected_salt = """
42+
NH1:device-socket:shared-secret:connect
43+
44+
x-nh-alg=NH1-HMAC-#{alg}
45+
x-nh-key=#{key}
46+
x-nh-time=#{signed_at}
47+
"""
48+
49+
opts = [
50+
key_length: key_length,
51+
key_iterations: iterations,
52+
key_digest: digest,
53+
signed_at: signed_at,
54+
max_age: max_hmac_age()
55+
]
56+
57+
{:ok, key, expected_salt, opts}
58+
end
59+
end
60+
61+
defp decode_from_headers(_headers), do: {:error, :headers_decode_failed}
62+
63+
defp get_shared_secret_auth("nhp_" <> _ = key), do: Products.get_shared_secret_auth(key)
64+
defp get_shared_secret_auth(key), do: Devices.get_shared_secret_auth(key)
65+
66+
defp get_or_maybe_create_device(%Products.SharedSecretAuth{} = auth, identifier) do
67+
# TODO: Support JITP profile here to decide if enabled or what tags to use
68+
Devices.get_or_create_device(auth, identifier)
69+
end
70+
71+
defp get_or_maybe_create_device(%{device: %{identifier: identifier} = device}, identifier),
72+
do: {:ok, device}
73+
74+
defp get_or_maybe_create_device(_auth, _identifier), do: {:error, :bad_identifier}
75+
76+
defp max_hmac_age() do
77+
Application.get_env(:nerves_hub, __MODULE__, [])
78+
|> Keyword.get(:max_age, @default_max_hmac_age)
79+
end
80+
81+
defp check_shared_secret_enabled() do
82+
if Products.shared_secrets_enabled?() do
83+
:ok
84+
else
85+
{:error, :shared_secrets_not_enabled}
86+
end
87+
end
88+
end

lib/nerves_hub_web/channels/device_socket.ex

Lines changed: 5 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,14 @@ defmodule NervesHubWeb.DeviceSocket do
77
alias NervesHub.Devices
88
alias NervesHub.Devices.Connections
99
alias NervesHub.Devices.DeviceConnection
10-
alias NervesHub.Products
1110
alias NervesHub.Tracker
1211

13-
alias Plug.Crypto
12+
alias NervesHub.RPC.DeviceAuth
1413

1514
channel("console", NervesHubWeb.ConsoleChannel)
1615
channel("device", NervesHubWeb.DeviceChannel)
1716
channel("extensions", NervesHubWeb.ExtensionsChannel)
1817

19-
# Default 90 seconds max age for the signature
20-
@default_max_hmac_age 90
21-
2218
defoverridable init: 1, handle_in: 2, terminate: 2
2319

2420
@impl Phoenix.Socket.Transport
@@ -96,14 +92,10 @@ defmodule NervesHubWeb.DeviceSocket do
9692
when is_list(x_headers) and length(x_headers) > 0 do
9793
headers = Map.new(x_headers)
9894

99-
with :ok <- check_shared_secret_enabled(),
100-
{:ok, key, salt, verification_opts} <- decode_from_headers(headers),
101-
{:ok, auth} <- get_shared_secret_auth(key),
102-
{:ok, signature} <- Map.fetch(headers, "x-nh-signature"),
103-
{:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_opts),
104-
{:ok, device} <- get_or_maybe_create_device(auth, identifier) do
105-
socket_and_assigns(socket, device)
106-
else
95+
case DeviceAuth.connect_device({:shared_secrets, x_headers}) do
96+
{:ok, device} ->
97+
socket_and_assigns(socket, Devices.preload_product(device))
98+
10799
error ->
108100
:telemetry.execute([:nerves_hub, :devices, :invalid_auth], %{count: 1}, %{
109101
auth: :shared_secrets,
@@ -133,61 +125,6 @@ defmodule NervesHubWeb.DeviceSocket do
133125
]
134126
end
135127

136-
defp decode_from_headers(%{"x-nh-alg" => "NH1-HMAC-" <> alg} = headers) do
137-
with [digest_str, iter_str, klen_str] <- String.split(alg, "-"),
138-
digest <- String.to_existing_atom(String.downcase(digest_str)),
139-
{iterations, ""} <- Integer.parse(iter_str),
140-
{key_length, ""} <- Integer.parse(klen_str),
141-
{signed_at, ""} <- Integer.parse(headers["x-nh-time"]),
142-
{:ok, key} <- Map.fetch(headers, "x-nh-key") do
143-
expected_salt = """
144-
NH1:device-socket:shared-secret:connect
145-
146-
x-nh-alg=NH1-HMAC-#{alg}
147-
x-nh-key=#{key}
148-
x-nh-time=#{signed_at}
149-
"""
150-
151-
opts = [
152-
key_length: key_length,
153-
key_iterations: iterations,
154-
key_digest: digest,
155-
signed_at: signed_at,
156-
max_age: max_hmac_age()
157-
]
158-
159-
{:ok, key, expected_salt, opts}
160-
end
161-
end
162-
163-
defp decode_from_headers(_headers), do: {:error, :headers_decode_failed}
164-
165-
defp get_shared_secret_auth("nhp_" <> _ = key), do: Products.get_shared_secret_auth(key)
166-
defp get_shared_secret_auth(key), do: Devices.get_shared_secret_auth(key)
167-
168-
defp get_or_maybe_create_device(%Products.SharedSecretAuth{} = auth, identifier) do
169-
# TODO: Support JITP profile here to decide if enabled or what tags to use
170-
Devices.get_or_create_device(auth, identifier)
171-
end
172-
173-
defp get_or_maybe_create_device(%{device: %{identifier: identifier} = device}, identifier),
174-
do: {:ok, device}
175-
176-
defp get_or_maybe_create_device(_auth, _identifier), do: {:error, :bad_identifier}
177-
178-
defp max_hmac_age() do
179-
Application.get_env(:nerves_hub, __MODULE__, [])
180-
|> Keyword.get(:max_age, @default_max_hmac_age)
181-
end
182-
183-
defp check_shared_secret_enabled() do
184-
if shared_secrets_enabled?() do
185-
:ok
186-
else
187-
{:error, :shared_secrets_not_enabled}
188-
end
189-
end
190-
191128
defp socket_and_assigns(socket, device) do
192129
# disconnect devices using the same identifier
193130
_ = socket.endpoint.broadcast_from(self(), "device_socket:#{device.id}", "disconnect", %{})
@@ -273,10 +210,4 @@ defmodule NervesHubWeb.DeviceSocket do
273210
defp last_seen_update_interval() do
274211
Application.get_env(:nerves_hub, :device_last_seen_update_interval_minutes)
275212
end
276-
277-
def shared_secrets_enabled?() do
278-
Application.get_env(:nerves_hub, __MODULE__, [])
279-
|> Keyword.get(:shared_secrets, [])
280-
|> Keyword.get(:enabled, false)
281-
end
282213
end

lib/nerves_hub_web/live/product/settings.ex

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ defmodule NervesHubWeb.Live.Product.Settings do
33

44
alias NervesHub.Extensions
55
alias NervesHub.Products
6-
alias NervesHubWeb.DeviceSocket
76

87
def mount(_params, _session, socket) do
98
product = Products.load_shared_secret_auth(socket.assigns.product)
@@ -14,7 +13,7 @@ defmodule NervesHubWeb.Live.Product.Settings do
1413
|> sidebar_tab(:settings)
1514
|> assign(:product, product)
1615
|> assign(:shared_secrets, product.shared_secret_auths)
17-
|> assign(:shared_auth_enabled, DeviceSocket.shared_secrets_enabled?())
16+
|> assign(:shared_auth_enabled, Products.shared_secrets_enabled?())
1817
|> assign(:form, to_form(Ecto.Changeset.change(product)))
1918
|> assign(:available_extensions, extensions())
2019

0 commit comments

Comments
 (0)