Current section
Files
Jump to
Current section
Files
lib/rns/identity.ex
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 = <<encryption_public::binary, signature_public::binary>>
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(<<ephemeral_public::binary-size(32), cipher_text::binary>>, 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