From ed5f98265f9fe8b820316bc4d8e8f6120e78e67d Mon Sep 17 00:00:00 2001 From: Shreyan Jain Date: Thu, 22 Feb 2024 22:34:18 -0800 Subject: [PATCH] Uhh Git hell (#27) * remove .so file, upgrade rustler * begin DidPlc * started DidPlc classes (don't look please) * closer to working did generation * doctest for cbor encoding * okay fine * use to_bytes * decode16 * make mix task stop crashing * docstrings * working cbor (#25) * DIdPlc stuff (#21) * remove .so file, upgrade rustler * begin DidPlc * started DidPlc classes (don't look please) * closer to working did generation * doctest for cbor encoding * okay fine * use to_bytes * decode16 * add round trip cbor test * fix map key syntax * trying libipld for encoding dag-cbor * missing cid tag maybe? * what if we construct a tagged IPLD node with the CID tag and insert it into the result map * manually tag it? idk * fortune favors the bold * mission failed, we'll get 'em next time * update README to reflect new goals :) (#23) * fix example case in dagcbor docs (#24) --------- Co-authored-by: flicknow Co-authored-by: nova <146671001+ovnanova@users.noreply.github.com> Co-authored-by: Samuel Newman Co-authored-by: mark <133474605+flicknow@users.noreply.github.com> * cbor lists * little fixes * remove extra sha256 * phew --------- Co-authored-by: flicknow Co-authored-by: nova <146671001+ovnanova@users.noreply.github.com> Co-authored-by: Samuel Newman Co-authored-by: mark <133474605+flicknow@users.noreply.github.com> --- lib/hexpds/dagcbor.ex | 21 +++++++++++++++++---- lib/hexpds/didgenerator.ex | 16 ++++++++-------- lib/hexpds/didplc.ex | 6 ++++-- lib/hexpds/k256.ex | 31 +++++++++++++++++++++++++------ lib/hexpds/tid.ex | 35 +++++++++++++++++++++++++++++++---- lib/mix/tasks/didgen.ex | 2 ++ test/hexpds_dagcbor_test.exs | 6 +++--- 7 files changed, 90 insertions(+), 27 deletions(-) diff --git a/lib/hexpds/dagcbor.ex b/lib/hexpds/dagcbor.ex index 4ee5f96..9f9203f 100644 --- a/lib/hexpds/dagcbor.ex +++ b/lib/hexpds/dagcbor.ex @@ -7,25 +7,38 @@ defmodule Hexpds.DagCBOR do def decode_dag_cbor(_cbor), do: :erlang.nif_error(:nif_not_loaded) end - @spec encode_json(binary()) :: {:error, binary()} | {:ok, binary()} + @spec encode(binary() | map()) :: {:error, binary()} | {:ok, binary()} @doc """ - Encodes a JSON string into a CBOR binary. + Encodes a JSON string or a map into a CBOR binary. Examples: - iex> Hexpds.DagCBOR.encode_json(Jason.encode!(%{apple: "banana", cranberry: "dragonfruit"})) + iex> Hexpds.DagCBOR.encode(%{apple: "banana", cranberry: "dragonfruit"}) ...> |> elem(1) ...> |> Base.encode16() "A2656170706C656662616E616E61696372616E62657272796B647261676F6E6672756974" """ - def encode_json(json) do + def encode("" <> json) do with {:ok, cbor} <- Internal.encode_dag_cbor(json) do {:ok, to_string(cbor)} end end + def encode(%{} = json) do + with {:ok, json} <- Jason.encode(json), do: encode(json) + end + + def encode([_ | _] = l) do + with {:ok, json} <- Jason.encode(l), do: encode(json) + end + @spec decode_json(binary()) :: {:error, binary()} | {:ok, String.t()} def decode_json(cbor) do Internal.decode_dag_cbor(cbor) end + + @spec decode(binary()) :: {:error, binary() | Jason.DecodeError.t()} | {:ok, any()} + def decode (cbor) do + with {:ok, json} <- decode_json(cbor), do: Jason.decode(json) + end end diff --git a/lib/hexpds/didgenerator.ex b/lib/hexpds/didgenerator.ex index dd729c8..b8f4209 100644 --- a/lib/hexpds/didgenerator.ex +++ b/lib/hexpds/didgenerator.ex @@ -2,7 +2,7 @@ defmodule Hexpds.DidGenerator do require Logger alias Hexpds.K256, as: K256 - @spec genesis_to_did(map()) :: <<_::64, _::_*8>> + @spec genesis_to_did(map()) :: String.t() def genesis_to_did(%{"type" => "plc_operation", "prev" => nil} = signed_genesis) do "did:plc:" <> with {:ok, signed_genesis_json} <- @@ -10,15 +10,15 @@ defmodule Hexpds.DidGenerator do |> Jason.encode(), {:ok, signed_genesis_cbor} <- signed_genesis_json - |> Hexpds.DagCBOR.encode_json() do - :crypto.hash(:sha256, signed_genesis_cbor) - |> Base.encode32(case: :lower) - |> String.slice(0..23) - |> String.downcase() - end + |> Hexpds.DagCBOR.encode(), + do: + :crypto.hash(:sha256, signed_genesis_cbor) + |> Base.encode32(case: :lower) + |> String.slice(0..23) + |> String.downcase() end - @spec publish_to_plc(map(), <<_::64, _::_*8>>) :: + @spec publish_to_plc(map(), String.t()) :: {:error, %{ :__exception__ => true, diff --git a/lib/hexpds/didplc.ex b/lib/hexpds/didplc.ex index dcd145d..1de438a 100644 --- a/lib/hexpds/didplc.ex +++ b/lib/hexpds/didplc.ex @@ -65,13 +65,13 @@ defmodule Hexpds.DidPlc do }) end - @spec sign(Hexpds.DidPlc.Operation.t(), Hexpds.K256.PrivateKey.t()) :: + @spec sign(t(), Hexpds.K256.PrivateKey.t()) :: {:ok, binary()} | {:error, String.t()} def sign(%__MODULE__{} = operation, %Hexpds.K256.PrivateKey{} = privkey) do with {:ok, cbor} <- operation |> to_json() - |> Hexpds.DagCBOR.encode_json(), + |> Hexpds.DagCBOR.encode(), do: {:ok, privkey @@ -80,6 +80,8 @@ defmodule Hexpds.DidPlc do |> String.replace("=", "")} end + @spec add_sig(t(), Hexpds.K256.PrivateKey.t()) :: + {:error, binary()} | map() def add_sig(%__MODULE__{} = operation, %Hexpds.K256.PrivateKey{} = privkey) do with {:ok, sig} <- sign(operation, privkey) do operation diff --git a/lib/hexpds/k256.ex b/lib/hexpds/k256.ex index 722d4a4..ca8048f 100644 --- a/lib/hexpds/k256.ex +++ b/lib/hexpds/k256.ex @@ -7,21 +7,38 @@ defmodule Hexpds.K256 do """ defmodule PrivateKey do defstruct [:privkey] + + @typedoc """ + A Secp256k1 private key. Contains the raw bytes of the key, and wraps the `k256` crate's `k256::SecretKey` type. + Should always be 32 bytes (256 bits) long. All operations on `Hexpds.K256.PrivateKey` are type-safe. + """ @type t :: %__MODULE__{privkey: <<_::256>>} defguard is_valid_key(privkey) when is_binary(privkey) and byte_size(privkey) == 32 @spec create() :: t() + @doc """ + Generates a new Secp256k1 private key. + """ def create(), do: %__MODULE__{privkey: :crypto.strong_rand_bytes(32)} @spec from_binary(binary()) :: t() - def from_binary(privkey), do: %__MODULE__{privkey: privkey} + + @doc """ + Wraps a Secp256k1 private key from its raw bytes. + """ + def from_binary(privkey) when is_valid_key(privkey), do: %__MODULE__{privkey: privkey} @spec from_hex(String.t()) :: t() def from_hex(hex), do: from_binary(Base.decode16!(hex, case: :lower)) @spec to_hex(t()) :: String.t() - def to_hex(%__MODULE__{} = privkey), do: Base.encode16(privkey.privkey, case: :lower) + + @doc """ + Converts a Secp256k1 private key to a hex-encoded string. + """ + def to_hex(%__MODULE__{privkey: privkey}) when is_valid_key(privkey), + do: Base.encode16(privkey, case: :lower) @spec to_pubkey(t()) :: Hexpds.K256.PublicKey.t() def to_pubkey(%__MODULE__{} = privkey) when is_valid_key(privkey.privkey), @@ -31,10 +48,12 @@ defmodule Hexpds.K256 do @doc """ Signs a binary message with a Secp256k1 private key. Returns a binary signature. """ - def sign(%__MODULE__{privkey: privkey}, message) when is_binary(message) do - with {:ok, sig_hex} <- Hexpds.K256.Internal.sign_message(privkey, message), - {:ok, sig} <- Base.decode16(sig_hex, case: :lower), - do: sig + + def sign(%__MODULE__{privkey: privkey}, message) + when is_binary(message) and is_valid_key(privkey) do + with {:ok, sig} <- Hexpds.K256.Internal.sign_message(privkey, message), + {:ok, sig_bytes} <- Base.decode16(sig, case: :lower), + do: sig_bytes end @spec sign!(t(), binary()) :: binary() diff --git a/lib/hexpds/tid.ex b/lib/hexpds/tid.ex index 1f6b9f3..1b8a243 100644 --- a/lib/hexpds/tid.ex +++ b/lib/hexpds/tid.ex @@ -1,8 +1,35 @@ defmodule Hexpds.Tid do import Bitwise - @type t :: %__MODULE__{timestamp: non_neg_integer(), clock_id: non_neg_integer()} defstruct [:timestamp, :clock_id] + + @typedoc """ + A TID is a 13-character string. + TID is short for "timestamp identifier," and the name is derived from the creation time of the record. + + The characteristics of a TID are: + + - 64-bit integer + - big-endian byte ordering + - encoded as base32-sortable. That is, encoded with characters 234567abcdefghijklmnopqrstuvwxyz, with no padding, yielding 13 ASCII characters. + - hyphens should not be included in a TID (unlike in previous iterations of the scheme) + + The layout of the 64-bit integer is: + + - The top bit is always 0 + - The next 53 bits represent microseconds since the UNIX epoch. 53 bits is chosen as the maximum safe integer precision in a 64-bit floating point number, as used by Javascript. + - The final 10 bits are a random "clock identifier." + + This struct holds the timestamp in microseconds since the UNIX epoch, and the clock_id, which is a random number in the range 0..1023. + + """ + @type t :: %__MODULE__{timestamp: unix_microseconds(), clock_id: non_neg_integer()} + + @typedoc """ + A number of microseconds since the UNIX epoch, as a 64-bit non-negative integer + """ + @type unix_microseconds :: non_neg_integer() + @b32_charset "234567abcdefghijklmnopqrstuvwxyz" @spec from_string(String.t()) :: t() | {:error, String.t()} @@ -21,13 +48,13 @@ defmodule Hexpds.Tid do {timestamp_acc, clock_id_acc <<< 5 ||| (pos &&& 0x1F)} _ -> - raise "Invalid TID" + throw("Invalid TID") end end) %__MODULE__{timestamp: timestamp, clock_id: clock_id} - rescue - _ -> {:error, "Invalid TID"} + catch + :throw, e -> {:error, e} end end diff --git a/lib/mix/tasks/didgen.ex b/lib/mix/tasks/didgen.ex index 27860b1..9d4b3c8 100644 --- a/lib/mix/tasks/didgen.ex +++ b/lib/mix/tasks/didgen.ex @@ -8,6 +8,8 @@ defmodule Mix.Tasks.DidPlc.Generate do use Mix.Task alias Hexpds.DidGenerator + HTTPoison.start() + @shortdoc "Generate a DID:PLC: and publish it to the PLC server set in config/config.exs - pass in a handle" @impl Mix.Task def run([handle | _]) do diff --git a/test/hexpds_dagcbor_test.exs b/test/hexpds_dagcbor_test.exs index 631340e..1cca1ee 100644 --- a/test/hexpds_dagcbor_test.exs +++ b/test/hexpds_dagcbor_test.exs @@ -14,9 +14,9 @@ defmodule HexpdsDagcborTest do test "cbor roundtrip" do for input <- test_cases() do - {:ok, cbor_encoded} = Hexpds.DagCBOR.encode_json(Jason.encode!(input)) - {:ok, json_encoded} = Hexpds.DagCBOR.decode_json(cbor_encoded) - assert input == Jason.decode!(json_encoded) + {:ok, cbor_encoded} = Hexpds.DagCBOR.encode(Jason.encode!(input)) + {:ok, original} = Hexpds.DagCBOR.decode(cbor_encoded) + assert input == original end end end