defmodule RNS.Identity do @moduledoc """ Reticulum identity. Lazily copied from https://github.com/Sgiath/reticulum. """ @type t() :: tuple() @type hash() :: RNS.Crypto.truncated_hash() @type key() :: RNS.Crypto.key() @type expiry() :: NaiveDateTime.t() | :never @type signature() :: RNS.Crypto.signature() @type ratchet() :: RNS.Crypto.ratchet() require Record Record.defrecord(:identity, encryption_secret: nil, encryption_public: nil, signature_secret: nil, signature_public: nil, hash: nil, expiry: :never ) alias RNS.Crypto alias RNS.Crypto.Fernet @doc "Get the secret encryption key of an identity record." @spec encryption_secret(t()) :: key() | nil def encryption_secret(identity), do: identity(identity, :encryption_secret) @doc "Get the public encryption key of an identity record." @spec encryption_public(t()) :: key() | nil def encryption_public(identity), do: identity(identity, :encryption_public) @doc "Get the secret signature key of an identity record." @spec signature_secret(t()) :: key() | nil def signature_secret(identity), do: identity(identity, :signature_secret) @doc "Get the public signature key of an identity record." @spec signature_public(t()) :: key() | nil def signature_public(identity), do: identity(identity, :signature_public) @doc "Get the hash of an identity record." @spec hash(t()) :: hash() def hash(identity), do: identity(identity, :hash) @doc "Get the expiry of an identity record." @spec expiry(t()) :: expiry() def expiry(identity), do: identity(identity, :expiry) @doc "Get the position of the hash in a destination record." @spec hash_position() :: non_neg_integer() def hash_position(), do: identity(:hash) @doc """ Creates a new interface. By default, the keys are automatically created(even if you give some as options), so in this case, you need to disable their creation: If you want to disable the creation of all keys, then just set `create_keys` to false. If you only want to disable encryption or signature keys, you can set `create_encryption_keys` or `create_signature_keys` to false. """ @spec new( create_keys: boolean(), create_encryption_keys: boolean(), create_signature_keys: boolean(), encryption_secret: key(), encryption_public: key(), signature_secret: key(), signature_public: key(), expiry: expiry(), store: boolean() ) :: t() def new(opts \\ []) do create_keys = Keyword.get(opts, :create_keys, true) create_encryption_keys_opt = Keyword.get(opts, :create_encryption_keys) create_signature_keys_opt = Keyword.get(opts, :create_signature_keys) create_encryption_keys = if create_encryption_keys_opt == nil, do: create_keys, else: create_encryption_keys_opt create_signature_keys = if create_signature_keys_opt == nil, do: create_keys, else: create_signature_keys_opt identity = identity( encryption_secret: Keyword.get(opts, :encryption_secret), encryption_public: Keyword.get(opts, :encryption_public), signature_secret: Keyword.get(opts, :signature_secret), signature_public: Keyword.get(opts, :signature_public), expiry: Keyword.get(opts, :expiry, :never) ) |> maybe_create_encryption_keys(create_encryption_keys) |> maybe_create_signature_keys(create_signature_keys) |> update_hash() if Keyword.get(opts, :store, true) do RNS.IdentityStore.put(identity) end identity end @spec maybe_create_encryption_keys(t(), boolean()) :: t() def maybe_create_encryption_keys(identity, create) do if create do {public, secret} = Crypto.x25519() identity(identity, encryption_secret: secret, encryption_public: public) else identity end end @spec maybe_create_signature_keys(t(), boolean) :: t() def maybe_create_signature_keys(identity, create) do if create do {public, secret} = Crypto.ed25519() identity(identity, signature_secret: secret, signature_public: public) else identity end end @spec update_hash(t()) :: t() def update_hash(identity) when identity(identity, :encryption_public) == nil or identity(identity, :signature_public) == nil, do: identity def update_hash(identity) do encryption_public = identity(identity, :encryption_public) signature_public = identity(identity, :signature_public) data = <> identity(identity, hash: Crypto.truncated_sha256(data)) end @doc "Encrypts some data using an identity record or a public encryption key." @spec encrypt(binary(), t(), ratchet() | nil) :: binary() def encrypt(plain_text, identity, ratchet \\ nil) do encryption_public = identity(identity, :encryption_public) hash = identity(identity, :hash) {ephemeral_public, ephemeral_secret} = Crypto.x25519() fernet = if ratchet == nil do Crypto.compute(ephemeral_secret, encryption_public) else Crypto.compute(ephemeral_secret, ratchet) end |> Crypto.hkdf(hash) |> Fernet.new() ephemeral_public <> Fernet.encrypt(plain_text, fernet) end @spec decrypt(<<_::256, _::_*1>>, t()) :: {:ok, binary()} | {:error, :invalid_signature} def decrypt(<>, identity) do encryption_secret = identity(identity, :encryption_secret) hash = identity(identity, :hash) fernet = Crypto.compute(encryption_secret, ephemeral_public) |> Crypto.hkdf(hash) |> Fernet.new() Fernet.decrypt(cipher_text, fernet) end @spec sign(binary(), t() | key()) :: signature() def sign(data, identity) when Record.is_record(identity) do signature_secret = identity(identity, :signature_secret) sign(data, signature_secret) end @spec sign(key(), binary()) :: signature() def sign(data, signature_secret) when is_binary(signature_secret) do Crypto.sign(signature_secret, data) end @spec validate?(binary(), signature(), t()) :: boolean() def validate?(data, signature, identity) do signature_public = identity(identity, :signature_public) Crypto.validate?(signature_public, data, signature) end @spec random_hash() :: hash() def random_hash() do :crypto.strong_rand_bytes(16) |> Crypto.truncated_sha256() end @doc "Checks if an identity has expired." @spec expired?(t()) :: boolean() def expired?(identity) do expiry = expiry(identity) case expiry do :never -> false _ -> NaiveDateTime.after?(NaiveDateTime.utc_now(), expiry) end end end