# `Ankusa.Crypto.Envelope`
[🔗](https://github.com/jamescarr/ankusa/blob/ankusa-v0.7.0/packages/ankusa/lib/ankusa/crypto/envelope.ex#L1)

Envelope encryption for the secrets Ankusa stores: a source's `verify.secret`
and its sinks' credentials (URLs with a password, header values, tokens, keys),
which `Ankusa.SourceStore.Persistent` keeps in this node's RocksDB store (and
so in every backup of it) and `Ankusa.SourceStore.Redis` keeps in Redis.

## Value format

One encrypted value is

    <<"AENC", 1::8, key_id_len::8, key_id::binary, iv::96, tag::128, ciphertext::binary>>

AES-256-GCM (`:crypto.crypto_one_time_aead/7`) under the key-encryption key
(KEK) named `key_id`, with a fresh random 96-bit IV per value. The bytes up to
and including `key_id` are the authenticated data, so a value cannot be
re-labelled with another key's id. In a stored JSON spec each hidden string is
replaced by `"AENC:" <> Base.encode64(value)`.

## Which fields

The fields `Ankusa.Admin.Redact` hides from the admin API are the fields this
module encrypts (`Ankusa.Admin.Redact.reduce_hidden/3`): one allowlist decides
both what the API may show and what may rest in the clear. Identity and
structure (`type`, `tenant`, `signature_header`, topics, regions, …) stay
readable, so a stored spec can still be inspected and diffed.

## Keys

`Ankusa.Config`'s `secrets` section:

  * `:kek` — the current key: 32 random bytes, base64 (`openssl rand -base64
    32`), optionally `"<id>:<base64>"`. The id names the key in every value it
    encrypts; without one it is the first eight hex characters of the key's
    SHA-256.
  * `:kek_provider` — `{module, fun, args}` instead of `:kek`, called once when
    a store starts, returning `{:ok, [kek | previous]}`: each element is a
    `:kek` string or an `{id, 32-byte key}` pair, the first one current.
  * `:kek_previous` — keys that may still decrypt, never encrypt, so a rotation
    can be rolled out node by node and rows rewritten with
    `mix ankusa.secrets.rewrap`.

With no KEK new rows are stored as plain JSON, as before; rows written earlier
under a KEK are then **refused at boot** rather than served as ciphertext.

# `keyring`

```elixir
@type keyring() :: Ankusa.Crypto.Envelope.Keyring.t()
```

# `reason`

```elixir
@type reason() ::
  :no_kek
  | :not_encrypted
  | :malformed
  | :tampered
  | {:unknown_key, String.t()}
  | {:unsupported_version, non_neg_integer()}
  | {:bad_kek, :not_base64 | :wrong_size | :bad_id | :not_a_key | :duplicate_id}
  | {:kek_provider, term()}
```

Why a value could not be read or a keyring built. Never carries key bytes.

# `configured?`

```elixir
@spec configured?(keyring()) :: boolean()
```

Is there a key to encrypt with?

# `current_key_id`

```elixir
@spec current_key_id(keyring()) :: String.t() | nil
```

The id of the key new values are encrypted under, or `nil` with no KEK.

# `decrypt`

```elixir
@spec decrypt(keyring(), binary()) :: {:ok, binary()} | {:error, reason()}
```

Decrypt a value `encrypt/2` produced, under whichever of the keyring's keys named it.

# `describe`

```elixir
@spec describe(Ankusa.Config.t()) :: String.t()
```

One line for the startup log: how the secrets rest. Does not call
`:kek_provider`, so it names the provider, not its key.

# `encrypt`

```elixir
@spec encrypt(keyring(), iodata()) :: binary()
```

Encrypt `plaintext` under the current key. Raises with no KEK.

# `encrypted?`

```elixir
@spec encrypted?(binary()) :: boolean()
```

Is this binary an encrypted value (by its magic)?

# `keyring`

```elixir
@spec keyring(map()) :: {:ok, keyring()} | {:error, reason()}
```

Resolve a config's `secrets` section into a keyring. Calls `:kek_provider`
once, if there is one.

# `rewrap_spec`

```elixir
@spec rewrap_spec(keyring(), map()) :: {:ok, map()} | :current | {:error, reason()}
```

The spec to store when a row is moved to the current key: `{:ok, sealed}`
when it holds a plaintext hidden field or a value under another key, `:current`
when there is nothing to do.

# `seal`

```elixir
@spec seal(keyring(), map()) :: map()
```

Encrypt every hidden field of a source spec (see the module doc). With no KEK
the spec is returned as it is.

# `unseal`

```elixir
@spec unseal(keyring(), map()) :: {:ok, map()} | {:error, reason()}
```

Decrypt every encrypted string of a stored spec. A plain spec comes back
unchanged; an encrypted one that no key of the keyring opens is an error
(`:no_kek`, `{:unknown_key, id}`, `:tampered`), never half-decrypted.

# `validate_config!`

```elixir
@spec validate_config!(Ankusa.Config.t()) :: :ok
```

Check the `secrets` section's shape. Never calls `:kek_provider` (that is
I/O, and happens once when a store starts), and never prints a key.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
