Packages
nerves_hub_link
2.8.0
2.12.0
2.11.1
2.11.0
retired
2.10.2
2.10.1
2.10.0
2.9.0
2.9.0-rc.3
2.9.0-rc.2
2.9.0-rc.1
2.8.1
2.8.0
2.7.3
2.7.2
2.7.0
retired
2.6.0
2.5.2
2.5.1
2.5.0
2.4.0
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.0
1.4.1
1.4.0
1.3.0
1.2.0
1.1.0
1.0.1
1.0.0
0.13.1
0.13.0
0.12.1
0.12.0
0.11.0
0.10.2
0.10.1
retired
0.10.0
retired
0.10.0-rc.0
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.6
Manage your Nerves fleet by connecting it to NervesHub
Current section
Files
Jump to
Current section
Files
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