# SPDX-FileCopyrightText: 2018 Frank Hunleth
# SPDX-FileCopyrightText: 2018 Justin Schneck
# SPDX-FileCopyrightText: 2021 Alex McLain
# SPDX-FileCopyrightText: 2022 Connor Rigby
# SPDX-FileCopyrightText: 2022 Digit
# SPDX-FileCopyrightText: 2025 Lars Wikman
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule NervesKey do
  @moduledoc """
  This is a high level interface to provisioning and using the NervesKey
  or any ATECC508A/608A that can be configured similarly.
  """

  alias NervesKey.{Config, OTP, Data, ProvisioningInfo}

  @build_year DateTime.utc_now().year
  @settings_slots_nerves_key [8, 7, 6, 5]
  @settings_slot_trust_and_go 8

  @typedoc "Which device/signer certificate pair to use"
  @type certificate_pair() :: :primary | :aux

  @typedoc "Which type of device to use"
  @type device_type() :: :nerves_key | :trust_and_go

  @doc """
  Detect if a NervesKey is available on the transport
  """
  @spec detected?(ATECC508A.Transport.t()) :: boolean()
  defdelegate detected?(transport), to: ATECC508A.Transport

  @doc """
  Check whether the NervesKey has been provisioned
  """
  @spec provisioned?(ATECC508A.Transport.t()) :: boolean()
  def provisioned?(transport) do
    {:ok, config} = ATECC508A.Configuration.read(transport)

    # If the OTP and data sections are locked, then this chip has been provisioned.
    config.lock_value == 0
  end

  @doc """
  Create a signing key pair

  This returns a tuple that contains a new signer certificate and private key.
  It is compatible with the ATECC508A certificate compression.

  Options:

  * :years_valid - how many years this key is valid for
  """
  @spec create_signing_key_pair(keyword()) :: {X509.Certificate.t(), X509.PrivateKey.t()}
  def create_signing_key_pair(opts \\ []) do
    years_valid = Keyword.get(opts, :years_valid, 1)
    ATECC508A.Certificate.new_signer(years_valid)
  end

  @doc """
  Read the manufacturer's serial number
  """
  @spec manufacturer_sn(ATECC508A.Transport.t(), device_type()) :: binary()
  def manufacturer_sn(transport, type \\ :nerves_key)

  def manufacturer_sn(transport, :nerves_key) do
    {:ok, %OTP{manufacturer_sn: serial_number}} = OTP.read(transport)
    serial_number
  end

  def manufacturer_sn(transport, :trust_and_go) do
    {:ok, config} = ATECC508A.Configuration.read(transport)
    %ATECC508A.Configuration{serial_number: sn_bytes} = config
    Base.encode16(sn_bytes)
  end

  @doc """
  IEEE EUI-48 MAC address that can be used as a unique identifier in LAN networking
  This is only available on `:trust_and_go`
  """
  def manufacturer_mac(transport, :trust_and_go) do
    {:ok, <<eui48::bytes-12, _::binary>>} = ATECC508A.DataZone.read(transport, 5)
    eui48
  end

  @doc """
  Sign a SHA256 digest
  """
  @spec sign_digest(ATECC508A.Transport.t(), binary()) ::
          {:ok, binary()} | {:error, atom()}
  def sign_digest(transport, digest) do
    private_key_slot_id = 0
    ATECC508A.Request.sign_digest(transport, private_key_slot_id, digest)
  end

  @doc """
  Return ssl_opts for using the NervesKey

  Pass an engine and optionally which certificate that you'd like to use.
  """
  @spec ssl_opts(ATECC508A.Transport.t(), certificate_pair(), device_type()) :: keyword()
  def ssl_opts(transport, which \\ :primary, type \\ :nerves_key) do
    {:ok, engine} = NervesKey.PKCS11.load_engine()

    cert =
      NervesKey.device_cert(transport, which, type)
      |> X509.Certificate.to_der()

    signer_cert =
      NervesKey.signer_cert(transport, which, type)
      |> X509.Certificate.to_der()

    transport_info = ATECC508A.Transport.info(transport)

    key =
      NervesKey.PKCS11.private_key(engine, i2c: i2c_instance(transport_info.bus_name), type: type)

    cacerts = [signer_cert]

    [key: key, cert: cert, cacerts: cacerts]
  end

  defp i2c_instance(<<"i2c-", instance::binary>>) do
    String.to_integer(instance)
  end

  @doc """
  Read the device certificate from the slot

  The device must be programmed for this to work.

  Examples:

  ```
  iex> NervesKey.device_cert(transport, :primary, :nerves_key)
  {:OTPCertificate, ...}

  iex> NervesKey.device_cert(transport, :primary, :trust_and_go)
  {:OTPCertificate, ...}
  ```
  """
  @spec device_cert(ATECC508A.Transport.t(), certificate_pair(), device_type()) ::
          X509.Certificate.t()
  def device_cert(transport, which \\ :primary, type \\ :nerves_key)

  def device_cert(transport, which, :nerves_key) do
    {:ok, device_sn} = Config.device_sn(transport)
    {:ok, device_data} = ATECC508A.DataZone.read(transport, Data.device_cert_slot(which))

    {:ok, <<signer_public_key_raw::64-bytes, _pad::8-bytes>>} =
      ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which))

    signer_public_key = ATECC508A.Certificate.raw_to_public_key(signer_public_key_raw)
    {:ok, %OTP{manufacturer_sn: serial_number}} = OTP.read(transport)
    {:ok, public_key_raw} = Data.genkey_raw(transport, false)

    template = ATECC508A.Certificate.NervesKeyTemplate.device(serial_number, signer_public_key)

    compressed = %ATECC508A.Certificate.Compressed{
      data: device_data,
      device_sn: device_sn,
      public_key: public_key_raw,
      template: template,
      issuer_rdn: X509.RDNSequence.new("/CN=Signer", :otp),
      subject_rdn: X509.RDNSequence.new("/CN=" <> serial_number, :otp)
    }

    ATECC508A.Certificate.decompress(compressed)
  end

  def device_cert(transport, which, :trust_and_go) do
    {:ok, device_sn} = NervesKey.Config.device_sn(transport)

    {:ok, device_data} =
      ATECC508A.DataZone.read(transport, NervesKey.Data.device_cert_slot(which))

    {:ok,
     <<_pad1::bytes-4, signer_public_key_raw_x::bytes-32, _pad2::bytes-4,
       signer_public_key_raw_y::bytes-32>>} =
      ATECC508A.DataZone.read(transport, NervesKey.Data.signer_pubkey_slot(which))

    signer_public_key_raw = signer_public_key_raw_x <> signer_public_key_raw_y

    {:ok, public_key_raw} = NervesKey.Data.genkey_raw(transport, false)

    <<
      _compressed_signature::binary-size(64),
      _compressed_validity::binary-size(3),
      signer_id::size(16),
      template_id::size(4),
      _chain_id::size(4),
      _serial_number_source::size(4),
      _format_version::size(4),
      0::size(8)
    >> = device_data

    aki = :crypto.hash(:sha, <<4>> <> signer_public_key_raw)
    ski = :crypto.hash(:sha, <<4>> <> public_key_raw)
    signer_id_hex_str = Integer.to_string(signer_id, 16)

    eui48 = manufacturer_mac(transport, :trust_and_go)

    template =
      ATECC508A.Certificate.TrustAndGoTemplate.device(
        device_sn,
        signer_id,
        template_id,
        "eui48_#{eui48}",
        ski,
        aki
      )

    issuer_rdn =
      X509.RDNSequence.new(
        "/O=Microchip Technology Inc/CN=Crypto Authentication Signer #{signer_id_hex_str}",
        :otp
      )

    subject_rdn =
      X509.RDNSequence.new(
        "/O=Microchip Technology Inc/CN=sn" <> Base.encode16(device_sn),
        :otp
      )

    compressed = %ATECC508A.Certificate.Compressed{
      data: device_data,
      device_sn: device_sn,
      public_key: public_key_raw,
      template: template,
      issuer_rdn: issuer_rdn,
      subject_rdn: subject_rdn
    }

    ATECC508A.Certificate.decompress(compressed)
  end

  @doc """
  Read the signer certificate from the slot
  """
  @spec signer_cert(ATECC508A.Transport.t(), certificate_pair(), device_type()) ::
          X509.Certificate.t()
  def signer_cert(transport, which \\ :primary, type \\ :nerves_key)

  def signer_cert(transport, which, :nerves_key) do
    {:ok, signer_data} = ATECC508A.DataZone.read(transport, Data.signer_cert_slot(which))

    {:ok, <<signer_public_key_raw::64-bytes, _pad::8-bytes>>} =
      ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which))

    signer_public_key = ATECC508A.Certificate.raw_to_public_key(signer_public_key_raw)
    template = ATECC508A.Certificate.NervesKeyTemplate.signer(signer_public_key)

    compressed = %ATECC508A.Certificate.Compressed{
      data: signer_data,
      public_key: signer_public_key_raw,
      template: template,
      issuer_rdn: X509.RDNSequence.new("/CN=Signer", :otp),
      subject_rdn: X509.RDNSequence.new("/CN=Signer", :otp)
    }

    ATECC508A.Certificate.decompress(compressed)
  end

  def signer_cert(transport, which, :trust_and_go) do
    {:ok, signer_data} = ATECC508A.DataZone.read(transport, Data.signer_cert_slot(which))

    {:ok,
     <<_pad1::bytes-4, signer_public_key_raw_x::bytes-32, _pad2::bytes-4,
       signer_public_key_raw_y::bytes-32>>} =
      ATECC508A.DataZone.read(transport, Data.signer_pubkey_slot(which))

    signer_public_key_raw = signer_public_key_raw_x <> signer_public_key_raw_y

    <<
      _compressed_signature::binary-size(64),
      _compressed_validity::binary-size(3),
      signer_id::size(16),
      _template_id::size(4),
      _chain_id::size(4),
      _serial_number_source::size(4),
      _format_version::size(4),
      0::size(8)
    >> = signer_data

    root_public_key_raw =
      <<189, 84, 230, 109, 227, 135, 84, 132, 0, 107, 83, 174, 21, 128, 213, 10, 160, 105, 231,
        138, 223, 85, 120, 216, 92, 226, 213, 77, 213, 184, 48, 41, 107, 255, 221, 110, 111, 114,
        86, 251, 217, 158, 241, 161, 22, 177, 29, 51, 173, 73, 16, 58, 161, 133, 135, 57, 220,
        250, 228, 55, 225, 157, 99, 78>>

    aki = :crypto.hash(:sha, <<4>> <> root_public_key_raw)
    ski = :crypto.hash(:sha, <<4>> <> signer_public_key_raw)

    template = ATECC508A.Certificate.TrustAndGoTemplate.signer(signer_id, ski, aki)

    signer_id_hex_str = Integer.to_string(signer_id, 16)

    issuer_rdn =
      X509.RDNSequence.new(
        "/O=Microchip Technology Inc/CN=Crypto Authentication Root CA 002",
        :otp
      )

    subject_rdn =
      X509.RDNSequence.new(
        "/O=Microchip Technology Inc/CN=Crypto Authentication Signer #{signer_id_hex_str}",
        :otp
      )

    compressed = %ATECC508A.Certificate.Compressed{
      data: signer_data,
      public_key: signer_public_key_raw,
      template: template,
      issuer_rdn: issuer_rdn,
      subject_rdn: subject_rdn
    }

    ATECC508A.Certificate.decompress(compressed)
  end

  @doc """
  Provision a NervesKey in one step.

  See the README.md for how to use this. This function locks the
  ATECC508A down, so you'll want to be sure what you pass it is
  correct.

  This function does it all. It requires the signer's private key so
  handle that with care. Alternatively, please consider sending a PR
  for supporting off-device signatures so that HSMs can be used.
  """
  @spec provision(
          ATECC508A.Transport.t(),
          ProvisioningInfo.t(),
          X509.Certificate.t(),
          X509.PrivateKey.t()
        ) :: :ok
  def provision(transport, info, signer_cert, signer_key) do
    check_time()

    :ok = configure(transport)
    otp_info = OTP.new(info.board_name, info.manufacturer_sn)
    otp_data = OTP.to_raw(otp_info)
    :ok = OTP.write(transport, otp_data)

    {:ok, device_public_key} = Data.genkey(transport)
    {:ok, device_sn} = Config.device_sn(transport)

    device_cert =
      ATECC508A.Certificate.new_device(
        device_public_key,
        device_sn,
        info.manufacturer_sn,
        signer_cert,
        signer_key
      )

    slot_data = Data.slot_data(device_sn, device_cert, signer_cert)

    :ok = Data.write_slots(transport, slot_data)

    # This is the point of no return!!

    # Lock the data and OTP zones
    :ok = Data.lock(transport, otp_data, slot_data)

    # Lock the slot that contains the private key to prevent calls to GenKey
    # from changing it. See datasheet for how GenKey doesn't check the zone
    # lock.
    :ok = ATECC508A.Request.lock_slot(transport, 0)
  end

  @doc """
  Provision a NervesKey for the volatile key config in one step.

  See the README.md for how to use this. This function locks the
  ATECC508A down, so you'll want to be sure what you pass it is
  correct. Ensure you have the activation key in safe storage, it
  will be required to use the NervesKey.

  This function does it all. It requires the signer's private key so
  handle that with care. Alternatively, please consider sending a PR
  for supporting off-device signatures so that HSMs can be used.
  """
  @spec volatile_provision(
          ATECC508A.Transport.t(),
          ProvisioningInfo.t(),
          X509.Certificate.t(),
          X509.PrivateKey.t(),
          binary(),
          binary()
        ) :: :ok
  def volatile_provision(
        transport,
        info,
        signer_cert,
        signer_key,
        <<_::128>> = activation_key,
        <<_::128>> = encryption_key
      ) do
    check_time()

    {:ok, config} = ATECC508A.Configuration.read(transport)
    # Ensure the device supports volatile key config
    true = ATECC508A.Configuration.supports_volatile?(config)

    :ok = volatile_configure(transport)
    otp_info = OTP.new(info.board_name, info.manufacturer_sn)
    otp_data = OTP.to_raw(otp_info)
    :ok = OTP.write(transport, otp_data)

    {:ok, device_public_key} = Data.genkey(transport)
    {:ok, device_sn} = Config.device_sn(transport)

    device_cert =
      ATECC508A.Certificate.new_device(
        device_public_key,
        device_sn,
        info.manufacturer_sn,
        signer_cert,
        signer_key
      )

    slot_data =
      Data.volatile_slot_data(device_sn, device_cert, signer_cert, activation_key, encryption_key)

    :ok = Data.write_slots(transport, slot_data)

    # This is the point of no return!!

    # Lock the data and OTP zones
    :ok = Data.lock(transport, otp_data, slot_data)

    # Lock the slot that contains the private key to prevent calls to GenKey
    # from changing it. See datasheet for how GenKey doesn't check the zone
    # lock.
    # Lock the slots containing the activation and encryption keys.
    :ok = ATECC508A.Request.lock_slot(transport, 0)
    :ok = ATECC508A.Request.lock_slot(transport, 1)
    :ok = ATECC508A.Request.lock_slot(transport, 2)
  end

  @doc """
  Provision the auxiliary device/signer certificates on a NervesKey.

  This function creates and saves the auxiliary certificates. These
  are only needed if the ones written by `provision/4` are not
  usable. They are not used unless explicitly requested. See the
  README.md for details.

  You may call this function multiple times after the ATECC508A
  has been provisioned.
  """
  @spec provision_aux_certificates(
          ATECC508A.Transport.t(),
          X509.Certificate.t(),
          X509.PrivateKey.t(),
          device_type()
        ) :: :ok
  def provision_aux_certificates(transport, signer_cert, signer_key, type \\ :nerves_key) do
    check_time()

    manufacturer_sn =
      if type == :nerves_key do
        manufacturer_sn(transport)
      else
        manufacturer_mac(transport, :trust_and_go)
      end

    {:ok, device_public_key} = Data.genkey(transport, false)
    {:ok, device_sn} = Config.device_sn(transport)

    device_cert =
      ATECC508A.Certificate.new_device(
        device_public_key,
        device_sn,
        manufacturer_sn,
        signer_cert,
        signer_key
      )

    Data.write_aux_certs(transport, device_sn, device_cert, signer_cert)
  end

  @doc """
  Clear out the auxiliary certificates

  This function overwrites the auxiliary certificate slots with
  """
  @spec clear_aux_certificates(ATECC508A.Transport.t()) :: :ok
  def clear_aux_certificates(transport) do
    Data.clear_aux_certs(transport)
  end

  @doc """
  Check whether the auxiliary certificates were programmed
  """
  @spec has_aux_certificates?(ATECC508A.Transport.t()) :: boolean()
  def has_aux_certificates?(transport) do
    slot = Data.device_cert_slot(:aux)
    slot_size = ATECC508A.DataZone.slot_size(slot)
    {:ok, slot_contents} = ATECC508A.DataZone.read(transport, slot)
    slot_contents != <<0::size(slot_size)-unit(8)>>
  end

  @doc """
  Return default provisioning info for a NervesKey

  This function is used for pre-programmed NervesKey devices. The
  serial number is a Base32-encoded version of the ATECC508A/608A's globally unique
  serial number. No additional care is needed to keep the number unique.
  """
  @spec default_info(ATECC508A.Transport.t()) :: ProvisioningInfo.t()
  def default_info(transport) do
    {:ok, sn} = Config.device_sn(transport)

    %ProvisioningInfo{manufacturer_sn: Base.encode32(sn, padding: false), board_name: "NervesKey"}
  end

  @doc """
  Return the max length of settings
  """
  @spec max_settings_len(device_type()) :: integer()
  def max_settings_len(:nerves_key) do
    Enum.reduce(@settings_slots_nerves_key, 0, fn slot, acc ->
      acc + ATECC508A.DataZone.slot_size(slot)
    end)
  end

  def max_settings_len(:trust_and_go) do
    ATECC508A.DataZone.slot_size(@settings_slot_trust_and_go)
  end

  @doc """
  Return the settings block as a binary
  """
  @spec get_raw_settings(ATECC508A.Transport.t(), device_type()) ::
          {:ok, binary()} | {:error, atom()}
  def get_raw_settings(transport, device_type \\ :nerves_key) do
    if device_type == :nerves_key do
      all_reads = Enum.map(@settings_slots_nerves_key, &ATECC508A.DataZone.read(transport, &1))

      case Enum.find(all_reads, fn {result, _} -> result != :ok end) do
        nil ->
          raw = Enum.map_join(all_reads, fn {:ok, contents} -> contents end)
          {:ok, raw}

        error ->
          error
      end
    else
      case ATECC508A.DataZone.read(transport, @settings_slot_trust_and_go) do
        {:ok, data} ->
          {:ok, data}

        error ->
          error
      end
    end
  end

  @doc """
  Return all of the setting stored in the NervesKey as a map
  """
  @spec get_settings(ATECC508A.Transport.t(), device_type()) :: {:ok, map()} | {:error, atom()}
  def get_settings(transport, device_type \\ :nerves_key) do
    with {:ok, raw_settings} <- get_raw_settings(transport, device_type) do
      try do
        settings = :erlang.binary_to_term(raw_settings, [:safe])
        {:ok, settings}
      catch
        _, _ -> {:error, :corrupt}
      end
    end
  end

  @doc """
  Store settings on the NervesKey

  This overwrites all of the settings that are currently on the key and should
  be used with care since there's no protection against a race condition with
  other NervesKey users.
  """
  @spec put_settings(ATECC508A.Transport.t(), map(), device_type()) :: :ok
  def put_settings(transport, settings, device_type \\ :nerves_key) when is_map(settings) do
    raw_settings = :erlang.term_to_binary(settings)
    put_raw_settings(transport, raw_settings, device_type)
  end

  @doc """
  Store raw settings on the Nerves Key

  This overwrites all of the settings and should be used with care since there's
  no protection against race conditions with other users of this API.
  """
  @spec put_raw_settings(ATECC508A.Transport.t(), binary(), device_type()) :: :ok
  def put_raw_settings(transport, raw_settings, device_type) when is_binary(raw_settings) do
    if byte_size(raw_settings) > max_settings_len(device_type) do
      raise "Settings are too large and won't fit in the NervesKey. The max raw size is #{max_settings_len(device_type)}."
    end

    padded_settings = pad(raw_settings, max_settings_len(device_type))

    slots =
      case device_type do
        :nerves_key -> break_into_slots(padded_settings, @settings_slots_nerves_key)
        :trust_and_go -> break_into_slots(padded_settings, [@settings_slot_trust_and_go])
      end

    Enum.each(slots, fn {slot, data} -> ATECC508A.DataZone.write(transport, slot, data) end)
  end

  defp pad(bin, len) when byte_size(bin) < len do
    to_pad = 8 * (len - byte_size(bin))
    <<bin::binary, 0::size(to_pad)>>
  end

  defp pad(bin, _len), do: bin

  defp break_into_slots(bin, slots) do
    break_into_slots(bin, slots, [])
    |> Enum.reverse()
  end

  defp break_into_slots(<<>>, [], result), do: result

  defp break_into_slots(bin, [slot | rest], result) do
    slot_len = ATECC508A.DataZone.slot_size(slot)
    {contents, next} = :erlang.split_binary(bin, slot_len)
    break_into_slots(next, rest, [{slot, contents} | result])
  end

  # Configure an ATECC508A or ATECC608A as a NervesKey.
  #
  # This is called from `provision/4`. It can be called multiple
  # times and so long as the part is configured in a compatible
  # way, then it succeeds. This is needed to recover from failures
  # in the provisioning process.
  defp configure(transport) do
    cond do
      Config.config_compatible?(transport) == {:ok, true} -> :ok
      Config.configured?(transport) == {:ok, true} -> {:error, :config_locked}
      true -> Config.configure(transport)
    end
  end

  defp volatile_configure(transport) do
    cond do
      Config.volatile_config_compatible?(transport) == {:ok, true} -> :ok
      Config.configured?(transport) == {:ok, true} -> {:error, :config_locked}
      true -> Config.configure_volatile(transport)
    end
  end

  defp check_time() do
    unless DateTime.utc_now().year >= @build_year do
      raise """
      It doesn't look like the clock has been set. Check that `nerves_time` is running
      or something else is providing time.
      """
    end
  end
end
