age-ex/lib/age/x25519.ex
2023-06-22 05:21:48 +02:00

90 lines
2.5 KiB
Elixir

defmodule Age.X25519 do
@moduledoc """
Internal helper functions for the X25519 identity format.
"""
@type public_key :: binary()
@type private_key :: binary()
@doc """
Generates a new public/private keypair.
"""
@spec generate_keypair() :: {public_key(), private_key()}
def generate_keypair() do
:libsodium_crypto_box.keypair()
end
@doc """
Encodes the public key as a Bech32 string of format age1...
"""
@spec encode_public_key(public_key()) :: binary()
def encode_public_key(pub) do
Bech32.encode("age", pub)
end
@doc """
Decodes the public key from a Bech32 string of format age1...
"""
@spec decode_public_key(binary()) :: public_key()
def decode_public_key(pub) do
{:ok, "age", res} = Bech32.decode(pub)
res
end
@doc """
Encodes the private key as a Bech32 string of format AGE-SECRET-KEY-1...
"""
@spec encode_private_key(private_key()) :: binary()
def encode_private_key(priv) do
Bech32.encode("AGE-SECRET-KEY-", priv)
end
@doc """
Decodes the private key from a Bech32 string of format AGE-SECRET-KEY-1...
"""
@spec decode_private_key(binary()) :: private_key()
def decode_private_key(priv) do
{:ok, "age-secret-key-", res} = Bech32.decode(priv)
res
end
@doc """
Generates the stanza for file encryption from the given recipient and file key.
"""
@spec stanza(public_key(), binary()) :: {binary(), binary()}
def stanza(recipient, file_key) do
{eph_share, eph_secret} = generate_keypair()
shared_secret = :crypto.compute_key(:ecdh, recipient, eph_secret, :x25519)
wrap_key = derive_wrap_key(shared_secret, eph_share, recipient)
raw_body = :libsodium_crypto_aead_chacha20poly1305.ietf_encrypt(file_key, <<0::96>>, wrap_key)
32 = byte_size(raw_body)
header = "X25519 " <> (eph_share |> Base.encode64(padding: false))
body = raw_body |> Base.encode64(padding: false)
{header, body}
end
@doc """
X25519(secret, base) to derive a public key from a private key.
"""
@spec derive_public_key(private_key()) :: public_key()
def derive_public_key(private_key) do
:libsodium_crypto_scalarmult_curve25519.base(private_key)
end
@doc """
Helper function to derive the wrap key for the file key.
"""
@spec derive_wrap_key(binary(), binary(), public_key()) :: binary()
def derive_wrap_key(shared_secret, ephemeral_share, recipient) do
info = "age-encryption.org/v1/X25519"
salt = ephemeral_share <> recipient
HKDF.derive(:sha256, shared_secret, 32, salt, info)
end
end