diff --git a/lib/gc_index_relay/nostr.ex b/lib/gc_index_relay/nostr.ex index c3be2b8..586a691 100644 --- a/lib/gc_index_relay/nostr.ex +++ b/lib/gc_index_relay/nostr.ex @@ -10,6 +10,7 @@ defmodule GcIndexRelay.Nostr do """ import Ecto.Query, warn: false + alias GcIndexRelay.Nostr.Validator alias GcIndexRelay.Nostr.PubEvent alias GcIndexRelay.Repo alias GcIndexRelay.Nostr.Event @@ -30,6 +31,18 @@ defmodule GcIndexRelay.Nostr do Writes a `GcIndexRelay.Nostr.PubEvent` to the database. """ def create_event(event) when is_struct(event, PubEvent) do + with {:ok, event} <- Validator.validate_id(event), + {:ok, event} <- Validator.validate_signature(event) do + case do_create_event(event) do + {:ok, result} -> {:ok, result} + {:error, reason} -> {:error, reason} + end + else + {:error, reason} -> {:error, reason} + end + end + + defp do_create_event(event) when is_struct(event, PubEvent) do db_event = PubEvent.to_db(event) %Event{} diff --git a/lib/gc_index_relay/nostr/validator.ex b/lib/gc_index_relay/nostr/validator.ex new file mode 100644 index 0000000..467be11 --- /dev/null +++ b/lib/gc_index_relay/nostr/validator.ex @@ -0,0 +1,55 @@ +defmodule GcIndexRelay.Nostr.Validator do + @moduledoc """ + Nostr key and signature validation. + + Uses [Curvy](https://hexdocs.pm/curvy/Curvy.html). + + Will migrate to [noscrypt](https://www.vaughnnugent.com/resources/software/modules/noscrypt) (via + NIF integration) at a later date. + """ + + alias GcIndexRelay.Nostr.PubEvent + + @doc """ + Validates a Nostr event ID per [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md). + """ + def validate_id(event) when is_struct(event, PubEvent) do + if valid_id?(event), + do: {:ok, event}, + else: {:error, "ID #{event.id} is invalid for event:\n#{event}"} + end + + defp valid_id?(event) + when is_struct(event, PubEvent) do + computed_id = compute_id!(event) + + event.id == computed_id + end + + @doc """ + Validates a Nostr event signature per [NIP-01](https://github.com/nostr-protocol/nips/blob/master/01.md). + """ + def validate_signature(event) when is_struct(event, PubEvent) do + if valid_signature?(event), + do: {:ok, event}, + else: {:error, "Signature #{event.sig} is invalid for event:\n#{event}"} + end + + defp valid_signature?(event) when is_struct(event, PubEvent) do + data = compute_id!(event) + Curvy.verify(event.sig, data, event.pubkey) + end + + defp compute_id!(event) when is_struct(event, PubEvent) do + Jason.encode!([0, event.pubkey, event.created_at, event.kind, event.tags, event.content]) + |> sha256(:lowercase_hex) + end + + defp sha256(data, :binary = output_type) when is_binary(data) and is_atom(output_type), + do: :crypto.hash(:sha256, data) + + defp sha256(data, :lowercase_hex = output_type) when is_binary(data) and is_atom(output_type) do + sha256(data, :binary) + |> Base.encode16(case: :lower) + end +end diff --git a/lib/gc_index_relay_web/controllers/event_controller.ex b/lib/gc_index_relay_web/controllers/event_controller.ex index aa8215e..cb6e25f 100644 --- a/lib/gc_index_relay_web/controllers/event_controller.ex +++ b/lib/gc_index_relay_web/controllers/event_controller.ex @@ -7,6 +7,7 @@ defmodule GcIndexRelayWeb.EventController do action_fallback GcIndexRelayWeb.FallbackController def create(conn, %{"event" => event_params}) do + # TODO: Add 400 for invalid event in FallbackController with {:ok, %Event{} = event} <- Nostr.create_event(event_params) do conn |> put_status(:created) @@ -16,6 +17,7 @@ defmodule GcIndexRelayWeb.EventController do end def show(conn, %{"id" => id}) do + # TODO: Add 404 for event not found in FallbackController event = Nostr.get_event!(id) render(conn, :show, event: event) end diff --git a/mix.exs b/mix.exs index 1179ef7..2e60cff 100644 --- a/mix.exs +++ b/mix.exs @@ -56,7 +56,8 @@ defmodule GcIndexRelay.MixProject do {:gettext, "~> 1.0"}, {:jason, "~> 1.2"}, {:dns_cluster, "~> 0.2.0"}, - {:bandit, "~> 1.5"} + {:bandit, "~> 1.5"}, + {:curvy, "~> 0.3.1"} ] end diff --git a/mix.lock b/mix.lock index 2d1fda8..4c08f03 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "bandit": {:hex, :bandit, "1.10.1", "6b1f8609d947ae2a74da5bba8aee938c94348634e54e5625eef622ca0bbbb062", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b4c35f273030e44268ace53bf3d5991dfc385c77374244e2f960876547671aa"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, + "curvy": {:hex, :curvy, "0.3.1", "2645a11452743a37de2393da4d2e60700632498b166413b4f73bc34c57a911e1", [:mix], [], "hexpm", "82df293452f7b751becabc29e8aad0f7d88ffdcd790ac7a2ea16ea1544681d8a"}, "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},