lib/webauthn_components/cose_key.ex

defmodule WebauthnComponents.CoseKey do
  @moduledoc """
  Custom `Ecto.Type` for WebAuthn cose keys.

  Use this type for the `:public_key` field in an Ecto schema.

  ## Example

  ```elixir
  defmodule MyApp.Authentication.UserKey do
    use Ecto.Schema
    import Ecto.Changeset
    alias MyApp.Accounts.User
    alias WebauthnComponents.CoseKey

    @primary_key {:id, :binary_id, autogenerate: true}
    @foreign_key_type :binary_id
    @derive {Jason.Encoder, only: [:key_id, :public_key, :label, :last_used]}
    schema "user_keys" do
      field :label, :string, default: "default"
      field :key_id, :binary
      field :public_key, CoseKey
      belongs_to :user, User
      field :last_used, :utc_datetime

      timestamps()
    end

    ...
  end
  ```
  """
  use Ecto.Type

  def type, do: :binary

  def cast(value) when is_map(value) do
    try do
      {:ok, CBOR.encode(value)}
    rescue
      Protocol.UndefinedError -> :error
    end
  end

  def cast(value) when is_binary(value) do
    case CBOR.decode(value) do
      {:ok, _decoded_value, ""} -> value
      {:error, _error} -> :error
    end
  end

  def cast(_), do: :error

  def load(data) when is_binary(data) do
    case CBOR.decode(data) do
      {:ok, cose_key, ""} ->
        {:ok, cose_key}

      _other ->
        :error
    end
  end

  def dump(value) when is_map(value) do
    {:ok, CBOR.encode(value)}
  end

  def dump(value) when is_binary(value) do
    case CBOR.decode(value) do
      {:ok, _decoded_value, ""} -> {:ok, value}
      {:error, _error} -> :error
    end
  end

  def dump(value), do: {:ok, value}
end