Current section
Files
Jump to
Current section
Files
lib/rns/destination.ex
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()
<<truncated_name_hash::binary-size(10), _rest::binary>> = 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 = <<name_hash::binary, identity_hash::binary>>
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(<<string::binary-size(32)>>) 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