Current section

Files

Jump to
nerves_hub_link lib nerves_hub_link configurators shared_secret.ex
Raw

lib/nerves_hub_link/configurators/shared_secret.ex

# SPDX-FileCopyrightText: 2023 Jon Carstens
# SPDX-FileCopyrightText: 2024 Josh Kalderimis
# SPDX-FileCopyrightText: 2024 Lars Wikman
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule NervesHubLink.Configurator.SharedSecret do
@moduledoc """
Configurator allowing authentication with a shared secret.
"""
@behaviour NervesHubLink.Configurator
alias Nerves.Runtime.KV
alias NervesHubLink.Certificate
alias NervesHubLink.Configurator.Config
@impl NervesHubLink.Configurator
def build(%Config{ssl: ssl, socket: socket} = config) do
ssl =
ssl
|> Keyword.drop([:key, :cert])
|> Keyword.put_new(:cacerts, Certificate.ca_certs())
# Shared Secret Auth uses a different socket path
url = URI.merge(socket[:url], "/device-socket/websocket")
%{config | ssl: ssl, socket: Keyword.merge(socket, headers: headers(config), url: url)}
end
@doc """
Generate headers for Shared Secret Auth
"""
@spec headers(Config.t()) :: [{String.t(), String.t()}]
def headers(%{shared_secret: shared_secret}) do
opts =
(shared_secret || [])
|> Keyword.put_new(:key_digest, :sha256)
|> Keyword.put_new(:key_iterations, 1000)
|> Keyword.put_new(:key_length, 32)
|> Keyword.put_new(:signature_version, "NH1")
|> Keyword.put_new(:identifier, Nerves.Runtime.serial_number())
# Important to use os_time as system_time is not updated by Erlang as
# quickly. See https://www.erlang.org/doc/apps/erts/time_correction#erlang-system-time
|> Keyword.put(:signed_at, System.os_time(:second))
alg =
"#{opts[:signature_version]}-HMAC-#{opts[:key_digest]}-#{opts[:key_iterations]}-#{opts[:key_length]}"
# Lookup device first then product
# TODO: Support saving to file?
key =
KV.get("nh_shared_key") || opts[:key] ||
KV.get("nh_shared_product_key") || opts[:product_key]
secret =
case key do
"nhp_" <> _ ->
KV.get("nh_shared_product_secret") || opts[:product_secret]
_ ->
KV.get("nh_shared_secret") || opts[:secret]
end
salt = create_salt(opts[:signature_version], alg, key, opts[:signed_at])
[
{"x-nh-alg", alg},
{"x-nh-key", key},
{"x-nh-time", to_string(opts[:signed_at])},
{"x-nh-signature", Plug.Crypto.sign(secret, salt, opts[:identifier], opts)}
]
end
# Currently only support NH1
defp create_salt(_NH1, alg, key, time) do
"""
NH1:device-socket:shared-secret:connect
x-nh-alg=#{alg}
x-nh-key=#{key}
x-nh-time=#{time}
"""
end
end