Skip to content
Sonr Crypto
Esc
navigateopen⌘Jpreview
On this page

Symmetric & Secrets

Bulk encryption, password-based key derivation, and the secret-hygiene helpers — how to pick between them and how they compose.

This is the “boring” half of the library, and the half you will actually touch on every request path. Nothing here is generic over the elliptic-curve abstraction: every package on these pages takes and returns []byte. That makes the layer easy to reason about and easy to misuse, so each page is explicit about the exact key sizes, ciphertext layouts, and constants involved.

The packages split into three jobs:

  • Encrypt bytes. aead is randomized AES-256-GCM — the default choice. daed is AES-SIV, a deterministic AEAD for the narrow cases where you need the same plaintext to produce the same ciphertext.
  • Turn a secret into a key. argon2 stretches a human password into key material and produces PHC-encoded password hashes. subtle is the HKDF + X25519 layer for turning an already-high-entropy secret (a shared secret, a master key) into per-purpose subkeys.
  • Handle the secret carefully. secure, salt, password, and subtle/random are small hygiene helpers: zeroization, constant-time comparison, salt generation and storage, password policy checks, and raw randomness.

Pick one

Your goal Package Entry point
Encrypt a payload, blob, or message aead NewAESGCM(key)Encrypt(pt, aad)
Encrypt a lookup key or identifier you must still be able to search by daed NewAESSIV(key)EncryptDeterministically
Wrap key material with no nonce to manage daed NewAESSIV(key)
Derive an encryption key from a user’s password argon2 New(DefaultConfig()).DeriveKey(pw, salt)
Store a verifiable password hash argon2 HashPassword / VerifyPassword
Split one master secret into several purpose-bound subkeys subtle ComputeHKDF("SHA256", key, salt, info, 32)
Agree on a key with a remote peer subtle X25519 trio → ComputeHKDF
Generate a salt salt or argon2 salt.GenerateDefault() / kdf.GenerateSalt()
Generate raw random bytes secure SecureRandom(buf)
Compare two secrets without leaking timing argon2 CompareHashes(a, b)
Zero a key out of memory after use secure Zeroize(key)
Enforce a password policy at signup password NewValidator(nil).Validate(pw)

How they compose

The realistic pipeline is: get entropy, stretch or expand it into a 32-byte key, encrypt with that key, then zero the key.

package vault

import (
	"github.com/sonr-io/crypto/aead"
	"github.com/sonr-io/crypto/argon2"
	"github.com/sonr-io/crypto/secure"
)

func sealWithPassword(password, plaintext, aad []byte) (key, salt, ct []byte, err error) {
	kdf := argon2.New(argon2.DefaultConfig()) // Argon2id, 64 MiB, t=1, p=4

	salt, err = kdf.GenerateSalt() // 32 bytes
	if err != nil {
		return nil, nil, nil, err
	}

	key = kdf.DeriveKey(password, salt) // 32 bytes == aead.KeySize
	defer secure.Zeroize(key)

	cipher, err := aead.NewAESGCM(key)
	if err != nil {
		return nil, nil, nil, err
	}

	ct, err = cipher.Encrypt(plaintext, aad) // nonce || ciphertext || tag
	return key, salt, ct, err
}

Two things make that snippet work, and both are worth internalizing:

  1. argon2.DefaultConfig().KeyLength is 32, and aead.KeySize is 32. The KDF output plugs straight into the AEAD constructor with no truncation or padding.
  2. aead.Encrypt generates its own nonce and prepends it, so the only thing you have to persist alongside the ciphertext is the salt.

Sizes at a glance

Every one of these is a compile-time constant in the package named, not a default you can override. Getting a size wrong is a constructor error, never a silent truncation.

Constant Value Where
aead.KeySize 32 AES-256-GCM key — the only length accepted
aead.NonceSize 12 GCM nonce, generated and prepended by Encrypt
aead.TagSize 16 GCM authentication tag
daed.AESSIVKeySize 64 AES-SIV double-length key: 32-byte CMAC key ‖ 32-byte CTR key
salt.DefaultSaltSize 32 Recommended salt size
salt.MinSaltSize / salt.MaxSaltSize 16 / 1024 Hard bounds enforced by salt.Generate

An aead ciphertext is therefore always len(plaintext) + 28 bytes; an AES-SIV ciphertext is always len(plaintext) + 16. Neither carries a version byte or key identifier, so any framing you need is yours to add.

What stays your responsibility

These packages cover the primitive, not the protocol. Everything below is out of scope for this layer and has to live in your application:

  • Key lifetime and rotation. Nothing tracks how many messages a key has protected, and nothing versions a key. aead will happily encrypt forever under one key.
  • Salt and hash persistence. salt.SaltStore is an in-memory map with no mutex; argon2.HashPassword is the only helper that packages a salt into something durable.
  • Replay and freshness. AEAD authenticity says “this ciphertext was produced by someone holding the key”, never “recently” or “once”. Bind timestamps or counters into the AAD.
  • Rate limiting on password paths. argon2 makes each guess expensive; it does not make guessing impossible.
  • Unicode normalization of anything a human types, before both validation and derivation.

What this layer does not do

There is no key storage here — no keystore, no envelope format, no versioned header. aead.Encrypt hands you nonce || ciphertext || tag and nothing else; if you need an algorithm identifier or a key ID on the wire, you frame it yourself. Likewise there is no ChaCha20-Poly1305, no AES-128, and no streaming/chunked API: aead is AES-256-GCM one-shot only.

Next

Everything outside this layer — signatures, threshold protocols, zero-knowledge proofs — is generic over the curve abstraction described in Foundations: curves.

Last updated on September 2, 2026

Was this page helpful?