defmodule RNS.Destination do @moduledoc """ Reticulum destination struct and destination-related utilities. Copied from https://github.com/Sgiath/reticulum but actually heavily modified. """ # TODO: Maybe one day we should separate the struct for the destination store and destination stuff for sending packets(and others). @type t() :: tuple() @type type() :: :single | :group | :plain | :link @type hash() :: RNS.Crypto.truncated_hash() @type expiry() :: NaiveDateTime.t() | :never @type app_name() :: String.t() @type aspects() :: list(String.t()) @type expanded_name() :: String.t() @type name_hash() :: <<_::80>> require Record Record.defrecord(:destination, identity: nil, name_hash: nil, hops: 0, name: nil, hash: nil, interface: nil, type: :single, app_data: <<>>, expiry: :never, announce: nil ) require RNS.Packet @doc "Get the identity of a destination record." @spec identity(t()) :: RNS.Identity.t() | nil def identity(destination), do: destination(destination, :identity) @doc "Get the name hash of a destination record." @spec name_hash(t()) :: name_hash() | nil def name_hash(destination), do: destination(destination, :name_hash) @doc "Get the hops of a destination record." @spec hops(t()) :: RNS.Packet.hops() def hops(destination), do: destination(destination, :hops) @doc "Get the name of a destination record." @spec name(t()) :: expanded_name() | nil def name(destination), do: destination(destination, :name) @doc "Get the hash of a destination record." @spec hash(t()) :: hash() def hash(destination), do: destination(destination, :hash) @doc "Get the interface of a destination record." @spec interface(t()) :: RNS.Interface.t() def interface(destination), do: destination(destination, :interface) @doc "Get the type of a destination record." @spec type(t()) :: type() def type(destination), do: destination(destination, :type) @doc "Get the app_data of a destination record." @spec app_data(t()) :: binary() def app_data(destination), do: destination(destination, :app_data) @doc "Get the expiry of a destination record." @spec expiry(t()) :: expiry() def expiry(destination), do: destination(destination, :expiry) @doc "Get theannounce of a detination record." @spec announce(t()) :: RNS.Packet.t() | nil def announce(destination), do: destination(destination, :announce) @doc "Get the position of the hash in a destination record." @spec hash_position() :: non_neg_integer() def hash_position(), do: destination(:hash) @doc "Create a new destination record." @spec new( app_name: app_name(), identity: RNS.Identity.t(), aspects: [String.t()], interface: RNS.Interface.t(), hops: RNS.Packet.hops(), expiry: expiry(), type: type(), app_data: binary(), announce: RNS.Packet.t(), hash: RNS.Destination.hash(), name_hash: RNS.Destination.name_hash(), store: boolean() ) :: t() def new(opts \\ []) do identity = Keyword.get(opts, :identity, RNS.Identity.new()) identity_hash = RNS.Identity.hash(identity) aspects = Keyword.get(opts, :aspects, []) hash = Keyword.get(opts, :hash, nil) app_name = Keyword.get(opts, :app_name, nil) expanded_name = if app_name == nil, do: nil, else: expand_name(nil, app_name, aspects) name_hash = Keyword.get(opts, :name_hash, nil) destination = destination( identity: identity, name_hash: if expanded_name == nil or name_hash != nil do name_hash else generate_name_hash(expanded_name) end, interface: Keyword.get(opts, :interface, nil), hops: Keyword.get(opts, :hops, 0), expiry: Keyword.get(opts, :expiry, :never), name: expanded_name, hash: if(hash == nil, do: hash(identity_hash, expanded_name), else: hash), type: Keyword.get(opts, :type, :single), app_data: Keyword.get(opts, :app_data, <<>>), announce: Keyword.get(opts, :announce, nil) ) if Keyword.get(opts, :store, false) do RNS.DestinationStore.put(destination) end destination end @doc "Returns a string containing the full human-readable name of the destination, from an app name and a number of aspects." @spec expand_name(RNS.Crypto.truncated_hash() | nil, app_name(), aspects()) :: expanded_name() def expand_name(nil, app_name, aspects) do [app_name | aspects] |> Enum.join(".") end def expand_name(identity_hash, app_name, aspects) when is_binary(identity_hash) do full_name = [app_name | aspects] |> Enum.reverse() # Append identity hash to the end of the string. identity_hash_string = Base.encode16(identity_hash) |> String.downcase() [identity_hash_string | full_name] |> Enum.reverse() |> Enum.join(".") end @doc "Creates the name hash, a hash of the expanded name truncated to the name hash length(10 bytes as of now)." @spec generate_name_hash(expanded_name()) :: name_hash() def generate_name_hash(expanded_name) do name_hash = expanded_name |> :unicode.characters_to_binary() |> RNS.Crypto.truncated_sha256() <> = name_hash truncated_name_hash end @doc "Creates the destination hash, the \"address\" used for routing from the identity hash and the app name." @spec hash(RNS.Identity.hash() | <<>>, expanded_name() | nil) :: hash() def hash(nil, expanded_name), do: hash("", expanded_name) def hash(_identity_hash, nil), do: nil def hash(identity_hash, expanded_name) do name_hash = expanded_name |> generate_name_hash() hash_material = <> RNS.Crypto.truncated_sha256(hash_material) end @doc """ Updates the announce of a destination. This will generated a new announce for the destination and updated the destination in the store. """ @spec update_announce(RNS.Destination.t()) :: {:ok, RNS.Destination.t()} | {:error, :not_all_identity_keys} def update_announce(destination) do case RNS.Packet.Announce.new(destination) do {:ok, announce} -> {:ok, destination(destination, announce: announce)} {:error, :not_all_identity_keys} -> {:error, :not_all_identity_keys} end end @doc """ Broadcasts the announce of a given destination. If `:update_announce` is set to true, the announce will be updated before being broadcasted, and then the destination in the store will be updated, and the new destination will be returned. """ @spec broadcast_announce(RNS.Destination.t(), update_announce: boolean()) :: :ok | {:ok, RNS.Destination.t()} | {:error, :not_all_identity_keys} def broadcast_announce(destination, opts \\ []) do if Keyword.get(opts, :update_announce, false) do case update_announce(destination) do {:ok, new_destination} -> new_destination |> announce() |> RNS.Interface.send() new_destination {:error, :not_all_identity_keys} -> {:error, :not_all_identity_keys} end else destination |> announce() |> RNS.Interface.send() end end @doc """ Checks if the name of the destination is the one that is specified. As the announce only contains a (truncated) hash of the app name, we can only verify if it's equal to another not know it's value directly. """ @spec name_is?(t(), expanded_name()) :: boolean() def name_is?(destination, expanded_name) when Record.is_record(destination, :destination) do destination_name_hash = destination(destination, :name_hash) test_name_hash = RNS.Crypto.truncated_sha256(expanded_name) |> RNS.Crypto.truncate(10) test_name_hash == destination_name_hash end @spec to_string(t() | hash()) :: String.t() def to_string(destination) when Record.is_record(destination, :destination) do destination(destination, :hash) |> __MODULE__.to_string() end def to_string(destination_hash) when is_binary(destination_hash) do hash_string = Base.encode16(destination_hash) |> String.downcase() "<#{hash_string}>" end @spec from_string(String.t()) :: {:ok, hash()} | :error def from_string(<<"<"::binary, string::binary-size(32), ">"::binary>>) do Base.decode16(string, case: :lower) end def from_string(<>) do Base.decode16(string, case: :lower) end @doc "Checks if a destination has expired." @spec expired?(t()) :: boolean() def expired?(destination) do expiry = expiry(destination) case expiry do :never -> false _ -> NaiveDateTime.after?(NaiveDateTime.utc_now(), expiry) end end @doc "Checks if a ratchet has expired." @spec ratchet_expired?(tuple()) :: boolean() def ratchet_expired?({_, _, expiry}) do case expiry do :never -> false _ -> NaiveDateTime.after?(NaiveDateTime.utc_now(), expiry) end end @doc "Changes the interface of a destination." @spec change_interface(t(), RNS.Interface.t()) :: t() def change_interface(destination, interface) when Record.is_record(interface, :interface) do change_interface(destination, RNS.Interface.id(interface)) end def change_interface(destination, interface) do destination = destination(destination, interface: interface) RNS.DestinationStore.put(destination) destination end end