A small, dependency-light Go library for storing credentials and secrets at rest — password hashing, envelope encryption, and context-bound authenticated data (AAD) that stops an attacker with database access from relocating one row's ciphertext into another.
Three focused packages:
| Package | Purpose |
|---|---|
pwhash |
Argon2id password hashing + verification (constant-time, tunable cost). |
cryptoenv |
AES-256-GCM envelope encryption under a single master key — Encrypt/Decrypt and the AAD-bound EncryptCtx/DecryptCtx. |
cryptoctx |
The Additional Authenticated Data (AAD) bound to each encrypted column, centralized so the encrypt and decrypt sites can never drift. |
Extracted and generalized from a production system where every stored secret (OAuth tokens, passwords) was encrypted at rest and bound to its owner.
Encrypting a column with a single master key stops an attacker who can read the database. It does
not stop an attacker who can write it: with plain AES-GCM they can copy account A's encrypted
password into account B's row, and it still decrypts — now they can log in as B with A's known
password. Binding the ciphertext to a stable per-row identity via AAD closes that hole: the AAD
won't match on Open, so the relocated blob fails to decrypt.
cryptoctx centralizes those bindings so the encrypt site and the decrypt site always compute the
same AAD:
aad := cryptoctx.AccessToken(accountID) // "cred:v1:access_token_enc:<id>"
blob, _ := cipher.EncryptCtx(plaintext, aad) // bound to this account
// A DB-write attacker who moves `blob` into another account's row:
_, err := cipher.DecryptCtx(blob, cryptoctx.AccessToken(otherID))
// err != nil — the AAD no longer matches.Two binding strengths are provided: per-row (AccessToken, RefreshToken, AccountPassword —
includes a stable row id/login) and domain-tag (OAuthTxn — a per-purpose constant, used where a
clean row key isn't available at every site).
import (
"github.com/biglill/credvault/pwhash"
"github.com/biglill/credvault/cryptoenv"
"github.com/biglill/credvault/cryptoctx"
)
// --- Passwords -------------------------------------------------------------
hash, _ := pwhash.Hash("correct horse battery staple")
ok := pwhash.Verify(hash, "correct horse battery staple") // true, constant-time
// --- Secrets at rest -------------------------------------------------------
cipher, _ := cryptoenv.New(masterKey) // 32-byte key from env/KMS
blob, _ := cipher.EncryptCtx(token, cryptoctx.RefreshToken(accountID))
token, _ := cipher.DecryptCtx(blob, cryptoctx.RefreshToken(accountID))DecryptCtx falls back to a nil-AAD Open when the bound Open fails, so ciphertext written before
context-binding was introduced keeps decrypting; new writes are bound, and legacy rows upgrade to a
bound form on their next rewrite.
- One dependency. Only
golang.org/x/crypto(for Argon2id); everything else is the standard library. - Master key stays yours.
cryptoenvtakes a 32-byte key you supply from your own secret store (env, KMS, Vault); the library never persists or logs it. - Tested.
go test ./...covers hashing, round-trips, the AAD substitution attack, and the legacy-blob fallback.
go test ./...MIT — see LICENSE.