Skip to content

Commit 4058a4d

Browse files
committed
Start extracting device auth for an rpc experiment
1 parent 595c13a commit 4058a4d

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
@@ -8,18 +8,14 @@ defmodule NervesHubWeb.DeviceSocket do
88
alias NervesHub.Devices.Connections
99
alias NervesHub.Devices.Device
1010
alias NervesHub.Devices.DeviceConnection
11-
alias NervesHub.Products
1211
alias NervesHub.Tracker
1312

14-
alias Plug.Crypto
13+
alias NervesHub.RPC.DeviceAuth
1514

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

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

2521
@impl Phoenix.Socket.Transport
@@ -97,14 +93,10 @@ defmodule NervesHubWeb.DeviceSocket do
9793
when is_list(x_headers) and length(x_headers) > 0 do
9894
headers = Map.new(x_headers)
9995

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

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