Skip to content

Commit

Permalink
TokenAuth -> SharedSecretAuth
Browse files Browse the repository at this point in the history
plus changing from `access_id` to `key`
  • Loading branch information
joshk committed Dec 11, 2023
1 parent 34c0326 commit 6f3160b
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 73 deletions.
9 changes: 5 additions & 4 deletions lib/nerves_hub/devices.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule NervesHub.Devices do
alias NervesHub.Firmwares.FirmwareMetadata
alias NervesHub.Products
alias NervesHub.Products.Product
alias NervesHub.Products.SharedSecretAuth
alias NervesHub.Repo
alias NervesHub.TaskSupervisor, as: Tasks

Expand Down Expand Up @@ -239,13 +240,13 @@ defmodule NervesHub.Devices do
end
end

@spec get_or_create_device(TokenAuth.t(), String.t()) ::
@spec get_or_create_device(SharedSecretAuth.t(), String.t()) ::
{:ok, Device.t()} | {:error, :not_found}
def get_or_create_device(%TokenAuth{} = token_auth, identifier) do
def get_or_create_device(%SharedSecretAuth{} = auth, identifier) do
with {:error, :not_found} <-
get_active_device(product_id: token_auth.product_id, identifier: identifier),
get_active_device(product_id: auth.product_id, identifier: identifier),
{:ok, product} <-
Products.get_product(token_auth.product_id) do
Products.get_product(auth.product_id) do
create_device(%{
org_id: product.org_id,
product_id: product.id,
Expand Down
16 changes: 10 additions & 6 deletions lib/nerves_hub/products.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule NervesHub.Products do

alias Ecto.Multi
alias NervesHub.{Certificate, Repo}
alias NervesHub.Products.{Product, TokenAuth}
alias NervesHub.Products.{Product, SharedSecretAuth}
alias NervesHub.Accounts.{User, Org, OrgUser}

alias NimbleCSV.RFC4180, as: CSV
Expand Down Expand Up @@ -140,13 +140,17 @@ defmodule NervesHub.Products do
Product.changeset(product, %{})
end

def get_token_auth(access_id: access_id) do
TokenAuth
|> join(:inner, [ta], p in assoc(ta, :product))
|> where([ta], ta.access_id == ^access_id)
|> where([ta], is_nil(ta.deactivated_at))
def get_shared_secret_auth(access_id) do
SharedSecretAuth
|> join(:inner, [ssa], p in assoc(ssa, :product))
|> where([ssa], ssa.access_id == ^access_id)
|> where([ssa], is_nil(ssa.deactivated_at))
|> where([p], is_nil(p.deleted_at))
|> Repo.one()
|> case do
nil -> {:error, :not_found}
auth -> {:ok, auth}
end
end

def devices_csv(%Product{} = product) do
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule NervesHub.Products.TokenAuth do
defmodule NervesHub.Products.SharedSecretAuth do
use Ecto.Schema

import Ecto.Changeset
Expand All @@ -7,12 +7,12 @@ defmodule NervesHub.Products.TokenAuth do

@type t :: %__MODULE__{}

@access_id_prefix "nhp"
@key_prefix "nhp"

schema "product_auth_tokens" do
schema "product_shared_secret_auth" do
belongs_to(:product, Product)

field(:access_id, :string)
field(:key, :string)
field(:secret, :string)

field(:deactivated_at, :utc_datetime)
Expand All @@ -22,13 +22,13 @@ defmodule NervesHub.Products.TokenAuth do

def create_changeset(%Product{} = product) do
change(%__MODULE__{}, product_id: product.id)
|> put_change(:access_id, "#{@access_id_prefix}_#{generate_token()}")
|> put_change(:key, "#{@key_prefix}_#{generate_token()}")
|> put_change(:secret, generate_token())
|> validate_required([:product_id, :access_id, :secret])
|> validate_format(:access_id, ~r/^nhp_[a-zA-Z0-9\-\/\+]{43}$/)
|> validate_required([:product_id, :key, :secret])
|> validate_format(:key, ~r/^#{@key_prefix}_[a-zA-Z0-9\-\/\+]{43}$/)
|> validate_format(:secret, ~r/^[a-zA-Z0-9\-\/\+]{43}$/)
|> foreign_key_constraint(:product_id)
|> unique_constraint(:access_id)
|> unique_constraint(:key)
|> unique_constraint(:secret)
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
defmodule NervesHubWeb.DeviceSocketTokenAuth do
defmodule NervesHubWeb.DeviceSocketSharedSecretAuth do
use Phoenix.Socket

alias NervesHub.Devices
alias NervesHub.Products

alias Plug.Crypto

channel("console", NervesHubWeb.ConsoleChannel)
channel("device", NervesHubWeb.DeviceChannel)

@salt_headers [
"x-nh-key-digest",
"x-nh-key-iterations",
"x-nh-key-length",
"x-nh-access-id",
"x-nh-digest",
"x-nh-iterations",
"x-nh-length",
"x-nh-key",
"x-nh-time"
]

# Default 15 min max age for the signature
@default_max_age 900
# Default 1 min max age for the signature
@default_max_age 60

def connect(_params, socket, %{x_headers: headers}) do
parsed_data = parse_headers(headers)
verification_options = verification_options(parsed_data)
parsed_headers = parse_headers(headers)
verification_options = verification_options(parsed_headers)
salt = expected_salt(headers)

with {:ok, true} <- Application.fetch_env(:nerves_hub, __MODULE__)[:enabled],
{:ok, access_id} <- Keyword.fetch(parsed_data, :access_id),
{:ok, token_auth} <- get_product_token_auth(access_id),
{:ok, signature} <- Keyword.fetch(parsed_data, :signature),
{:ok, identifier} <-
Crypto.verify(token_auth.secret, salt, signature, verification_options),
{:ok, device} <-
Devices.get_or_create_device(token_auth, identifier) do
{:ok, key} <- Keyword.fetch(parsed_headers, :key),
{:ok, auth} <- Products.get_shared_secret_auth(key),
{:ok, signature} <- Keyword.fetch(parsed_headers, :signature),
{:ok, identifier} <- Crypto.verify(auth.secret, salt, signature, verification_options),
{:ok, device} <- Devices.get_or_create_device(auth, identifier) do
socket =
socket
|> assign(:device, device)
Expand All @@ -49,25 +48,25 @@ defmodule NervesHubWeb.DeviceSocketTokenAuth do
defp parse_headers(headers) do
for {k, v} <- headers do
case String.downcase(k) do
"x-nh-time" ->
{:signed_at, String.to_integer(v)}

"x-nh-key-length" ->
{:key_length, String.to_integer(v)}
"x-nh-digest" ->
"NH1-HMAC-" <> digest_str = v
{:key_digest, String.to_existing_atom(String.downcase(digest_str))}

"x-nh-key-iterations" ->
"x-nh-iterations" ->
{:key_iterations, String.to_integer(v)}

"x-nh-key-digest" ->
"NH1-HMAC-" <> digest_str = v
{:key_digest, String.to_existing_atom(String.downcase(digest_str))}
"x-nh-length" ->
{:key_length, String.to_integer(v)}

"x-nh-access-id" ->
{:access_id, v}
"x-nh-key" ->
{:key, v}

"x-nh-signature" ->
{:signature, v}

"x-nh-time" ->
{:signed_at, String.to_integer(v)}

_ ->
# Skip unknown x headers.
# It's not uncommon for x headers to be added by Load balancers
Expand All @@ -78,7 +77,6 @@ defmodule NervesHubWeb.DeviceSocketTokenAuth do

defp verification_options(parsed_data) do
Keyword.take(parsed_data, [:key_digest, :key_iterations, :key_length, :signed_at])
# TODO: Make max_age configurable?
|> Keyword.put(:max_age, @default_max_age)
end

Expand All @@ -95,13 +93,6 @@ defmodule NervesHubWeb.DeviceSocketTokenAuth do
"""
end

defp get_product_token_auth(access_id) do
case NervesHub.Products.get_token_auth(access_id: access_id) do
nil -> {:error, :unknown_access_id}
token_auth -> {:ok, token_auth}
end
end

defp generate_reference_id() do
Base.encode32(:crypto.strong_rand_bytes(2), padding: false)
end
Expand Down
2 changes: 1 addition & 1 deletion lib/nerves_hub_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ defmodule NervesHubWeb.Endpoint do
websocket: [connect_info: [session: @session_options]]
)

socket("/device-socket", NervesHubWeb.DeviceSocketTokenAuth,
socket("/device-socket", NervesHubWeb.DeviceSocketSharedSecretAuth,
websocket: [connect_info: [:x_headers]]
)

Expand Down
19 changes: 0 additions & 19 deletions priv/repo/migrations/20231209055552_create_product_auth_tokens.exs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule NervesHub.Repo.Migrations.CreateProductSharedSecretAuth do
use Ecto.Migration

def change do
create table(:product_shared_secret_auth) do
add(:product_id, references(:products), null: false)

add(:key, :string, null: false)
add(:secret, :string, null: false)

add(:deactivated_at, :utc_datetime)

timestamps()
end

create index(:product_shared_secret_auth, [:key], unique: true)
create index(:product_shared_secret_auth, [:secret], unique: true)
end
end

0 comments on commit 6f3160b

Please sign in to comment.