secretbox

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 31, 2026 License: MIT Imports: 14 Imported by: 0

README

go-secretbox

Password-based encryption for data at rest, in Go. Argon2id + AES-256-GCM, done right.

Go Version Go Reference GitHub release (latest by date) CI

go-secretbox is the boring, correct version of the encryption code everyone ends up writing once: stretch a password with a slow KDF, encrypt with an authenticated cipher, prepend the nonce, store a salt, verify the password without storing it. It gets the parts that are easy to get wrong — nonce generation, constant-time checks, key zeroing, self-describing formats — out of your codebase.

It was extracted from matcha's "secure mode," which encrypts a mail client's config and caches behind a master password.

Features

  • Two layers, one set of primitives.
    • Seal / Unseal — one-shot, self-describing blobs. The salt and KDF parameters travel inside the ciphertext, so a blob is decryptable years later with only the password.
    • Vault — the long-lived "secure mode" pattern: a metadata file with a salt + encrypted sentinel, an in-memory session key, transparent file encryption, password change, and key rotation.
  • Sentinel password verification. No password, and no hash of it, is ever stored. Unlock decrypts a known sentinel and compares in constant time.
  • Pluggable KDF and cipher. Argon2id + AES-256-GCM by default; swap in XChaCha20-Poly1305 (or your own KDF/Cipher) via options. The choice is recorded in metadata, so Unlock/Unseal always reconstruct the right algorithm.
  • Key hygiene. Derived keys are zeroed after use and on Lock. Rekey decrypts-all-then-rotates so a failure can't leave files stranded.
  • Small surface, single dependency. Just golang.org/x/crypto.

Install

go get github.com/floatpane/go-secretbox

Requires Go 1.26+.

Usage

One-shot: encrypt a blob with a password
package main

import (
    "fmt"
    "log"

    "github.com/floatpane/go-secretbox"
)

func main() {
    blob, err := secretbox.Seal([]byte("attack at dawn"), "correct horse battery staple")
    if err != nil {
        log.Fatal(err)
    }
    // blob is safe to write to disk — it carries its own salt + KDF params.

    plain, err := secretbox.Unseal(blob, "correct horse battery staple")
    if err != nil {
        log.Fatal(err) // ErrDecrypt on wrong password or tampering
    }
    fmt.Println(string(plain)) // attack at dawn
}
Vault: "secure mode" with a master password
v := secretbox.NewVault("/home/me/.config/app/secure.meta")

// First run — turn secure mode on.
if !v.Initialized() {
    if err := v.Init(masterPassword); err != nil {
        log.Fatal(err)
    }
}

// Later runs — unlock with the master password.
if err := v.Unlock(masterPassword); err != nil {
    log.Fatal(err) // ErrWrongPassword
}
defer v.Lock() // zeroes the session key

// Transparent file encryption while unlocked.
v.WriteFile("/home/me/.config/app/config.json", configBytes, 0o600)
data, _ := v.ReadFile("/home/me/.config/app/config.json")
Rotate the master password (and migrate files)
// Decrypts every file with the old key, rotates the vault, re-encrypts with
// the new key. Phase-ordered so a crash can't strand your data.
err := v.Rekey(newPassword, []string{
    "/home/me/.config/app/config.json",
    "/home/me/.config/app/cache.db",
})
Choose a different cipher
v := secretbox.NewVault(metaPath,
    secretbox.WithCipher(secretbox.ChaCha20Poly1305{}),
    secretbox.WithKDF(secretbox.NewArgon2id(secretbox.Argon2idParams{
        Time: 4, Memory: 128 * 1024, Threads: 4,
    })),
)

Defaults

Knob Default Notes
KDF Argon2id Time=3, Memory=64 MiB, Threads=4 (interactive-login baseline)
Cipher AES-256-GCM 32-byte key, 12-byte random nonce, prepended to ciphertext
Salt 16 random bytes fresh per Init/Seal/Rekey
Sentinel secretbox-verified encrypted under the key, compared constant-time on Unlock

What this is not

  • Not key management. It protects data with a password. If the password leaks, so does the data.
  • Not memory-hardened against root. While unlocked, the key lives in process memory. A privileged local attacker (or a core dump) can read it. Lock shortens that window; it does not close it against an attacker with ptrace.
  • Not a replacement for an OS keyring. It's complementary — matcha uses the keyring when secure mode is off and a Vault when it's on.

Documentation

Full API reference: pkg.go.dev/github.com/floatpane/go-secretbox

Guides and diagrams: see docs/.

Sister projects

Project Role
floatpane/matcha Reference consumer — uses this library for its config/cache "secure mode."
floatpane/go-uds-jsonrpc Sibling extraction — local daemon JSON-RPC over Unix sockets.

Contributing

PRs welcome. See CONTRIBUTING.md.

Security

Report vulnerabilities privately via SECURITY.md.

License

MIT. See LICENSE.

Documentation

Overview

Package secretbox encrypts data at rest with a password.

It pairs a key-derivation function (Argon2id by default) with an authenticated cipher (AES-256-GCM by default) behind two layers:

  • One-shot functions Seal and Unseal produce a self-describing blob that carries its own salt and KDF parameters — decryptable years later with only the password.
  • A Vault manages the long-lived "secure mode" pattern: a metadata file with a salt and an encrypted sentinel, an in-memory session key, and transparent file encryption, plus password change and key rotation.

Both layers default to the same primitives matcha uses, and both accept custom KDF/Cipher implementations via options.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrDecrypt is returned for any decryption failure: wrong key, truncated
	// input, or authentication-tag mismatch. It deliberately does not say
	// which, to avoid leaking information to an attacker.
	ErrDecrypt = errors.New("secretbox: decryption failed")
	// ErrWrongPassword is returned by Vault.Unlock when the password fails the
	// sentinel check.
	ErrWrongPassword = errors.New("secretbox: incorrect password")
	// ErrLocked is returned by Vault operations that need the session key while
	// the vault is locked.
	ErrLocked = errors.New("secretbox: vault is locked")
	// ErrNotInitialized is returned when a vault's metadata file is absent.
	ErrNotInitialized = errors.New("secretbox: vault not initialized")
	// ErrUnsupported is returned when metadata names a KDF or cipher this build
	// does not know how to construct.
	ErrUnsupported = errors.New("secretbox: unsupported algorithm")
)

Sentinel errors. Compare with errors.Is.

View Source
var DefaultArgon2id = Argon2idParams{Time: 3, Memory: 64 * 1024, Threads: 4}

DefaultArgon2id is a sensible interactive-login baseline: 3 passes over 64 MiB across 4 lanes. It matches the parameters matcha ships with.

Functions

func Seal

func Seal(plaintext []byte, password string) ([]byte, error)

Seal encrypts plaintext with password using the default primitives (Argon2id + AES-256-GCM) and returns a self-describing blob.

The blob embeds a fresh random salt and the KDF parameters, so Unseal needs only the password. Use SealWith to choose different primitives.

func SealWith

func SealWith(plaintext []byte, password string, kdf KDF, c Cipher) ([]byte, error)

SealWith is Seal with an explicit KDF and cipher.

func Unseal

func Unseal(blob []byte, password string) ([]byte, error)

Unseal reverses Seal/SealWith. It reads the embedded header to reconstruct the KDF and cipher, so the caller supplies only the password.

Types

type AESGCM

type AESGCM struct{}

AESGCM is the default cipher: AES-256 in Galois/Counter Mode. The key must be exactly 32 bytes. This matches matcha's on-disk format.

func (AESGCM) Decrypt

func (AESGCM) Decrypt(ciphertext, key []byte) ([]byte, error)

Decrypt implements Cipher.

func (AESGCM) Encrypt

func (AESGCM) Encrypt(plaintext, key []byte) ([]byte, error)

Encrypt implements Cipher.

func (AESGCM) ID

func (AESGCM) ID() string

ID implements Cipher.

func (AESGCM) KeySize

func (AESGCM) KeySize() int

KeySize implements Cipher.

type Argon2id

type Argon2id struct {
	// contains filtered or unexported fields
}

Argon2id is the default KDF. It implements KDF using the Argon2id variant, which resists both GPU and side-channel attacks.

func NewArgon2id

func NewArgon2id(p Argon2idParams) Argon2id

NewArgon2id returns an Argon2id KDF with the given parameters. Passing the zero value falls back to DefaultArgon2id.

func (Argon2id) DeriveKey

func (a Argon2id) DeriveKey(password string, salt []byte, keyLen uint32) []byte

DeriveKey implements KDF.

func (Argon2id) ID

func (Argon2id) ID() string

ID implements KDF.

func (Argon2id) Params

func (a Argon2id) Params() map[string]uint32

Params implements KDF.

type Argon2idParams

type Argon2idParams struct {
	Time    uint32 // number of passes over memory
	Memory  uint32 // memory in KiB
	Threads uint8  // parallelism
}

Argon2idParams configures the Argon2id key-derivation function.

The zero value is not usable; start from DefaultArgon2id and adjust. Higher Time/Memory raise the cost of a brute-force attack at the expense of latency on the legitimate path.

type ChaCha20Poly1305

type ChaCha20Poly1305 struct{}

ChaCha20Poly1305 is an alternative cipher using the XChaCha20-Poly1305 AEAD, which has a 24-byte nonce (safe to generate randomly without a counter). The key must be exactly 32 bytes. Prefer this on platforms without AES hardware acceleration.

func (ChaCha20Poly1305) Decrypt

func (ChaCha20Poly1305) Decrypt(ciphertext, key []byte) ([]byte, error)

Decrypt implements Cipher.

func (ChaCha20Poly1305) Encrypt

func (ChaCha20Poly1305) Encrypt(plaintext, key []byte) ([]byte, error)

Encrypt implements Cipher.

func (ChaCha20Poly1305) ID

func (ChaCha20Poly1305) ID() string

ID implements Cipher.

func (ChaCha20Poly1305) KeySize

func (ChaCha20Poly1305) KeySize() int

KeySize implements Cipher.

type Cipher

type Cipher interface {
	// Encrypt seals plaintext under key, returning nonce-prefixed ciphertext.
	Encrypt(plaintext, key []byte) ([]byte, error)
	// Decrypt opens ciphertext produced by Encrypt. It returns ErrDecrypt on
	// any authentication or format failure.
	Decrypt(ciphertext, key []byte) ([]byte, error)
	// ID is the stable identifier persisted in vault metadata.
	ID() string
	// KeySize is the required key length in bytes.
	KeySize() int
}

Cipher is an authenticated encryption scheme keyed by a KDF-derived key.

Encrypt must generate a fresh random nonce per call and prepend it to the returned ciphertext; Decrypt reverses that framing. Implementations are stateless and safe for concurrent use.

type KDF

type KDF interface {
	// DeriveKey stretches password+salt into a key of keyLen bytes.
	DeriveKey(password string, salt []byte, keyLen uint32) []byte
	// ID is the stable identifier persisted in vault metadata (e.g. "argon2id").
	ID() string
	// Params returns the tunable parameters so they can be stored alongside the
	// salt and reproduced on unlock.
	Params() map[string]uint32
}

KDF derives a symmetric key from a low-entropy password and a random salt.

Implementations must be deterministic: the same password, salt, and parameters always produce the same key. The returned key length must equal the cipher's KeySize.

type Meta

type Meta struct {
	Version  int               `json:"version"`
	KDF      string            `json:"kdf"`
	Cipher   string            `json:"cipher"`
	Salt     string            `json:"salt"`     // base64
	Sentinel string            `json:"sentinel"` // base64 ciphertext of sentinelPlaintext
	Params   map[string]uint32 `json:"params"`   // KDF parameters
}

Meta is the JSON document written to the vault's metadata file. It holds everything needed to re-derive the key from a password — except the password itself. It is safe to store in plaintext.

type Option

type Option func(*Vault)

Option configures a Vault.

func WithCipher

func WithCipher(c Cipher) Option

WithCipher sets the cipher used for new vaults. On Unlock the cipher is reconstructed from metadata. Defaults to AES-256-GCM.

func WithKDF

func WithKDF(kdf KDF) Option

WithKDF sets the key-derivation function used for new vaults and one-shot sealing. On Unlock the KDF is reconstructed from metadata instead, so this only affects Init. Defaults to Argon2id with DefaultArgon2id parameters.

type Vault

type Vault struct {
	// contains filtered or unexported fields
}

Vault implements the "secure mode" pattern: a metadata file with a salt and an encrypted sentinel, an in-memory session key derived on Unlock, and transparent file encryption while unlocked.

A Vault is safe for concurrent use once unlocked. Lock zeroes the key.

func NewVault

func NewVault(metaPath string, opts ...Option) *Vault

NewVault returns a vault backed by the metadata file at metaPath. It does not touch the filesystem; call Init to create a new vault or Unlock to open an existing one.

func (*Vault) ChangePassword

func (v *Vault) ChangePassword(newPassword string) error

ChangePassword re-keys the vault to newPassword. The vault must be unlocked (newer code can Unlock with the old password first). It generates a fresh salt, derives a new key, re-encrypts the sentinel, and rewrites metadata.

Existing files encrypted with the old key are NOT touched — pass them to Rekey instead, which migrates files and rotates the password atomically from the caller's perspective.

func (*Vault) Decrypt

func (v *Vault) Decrypt(ciphertext []byte) ([]byte, error)

Decrypt opens ciphertext with the session key. Returns ErrLocked if locked.

func (*Vault) Encrypt

func (v *Vault) Encrypt(plaintext []byte) ([]byte, error)

Encrypt seals plaintext with the session key. Returns ErrLocked if locked.

func (*Vault) Init

func (v *Vault) Init(password string) error

Init creates a new vault: it generates a random salt, derives the key from password, encrypts the sentinel, writes the metadata file, and leaves the vault unlocked with the session key set.

It fails if the vault is already initialized.

func (*Vault) Initialized

func (v *Vault) Initialized() bool

Initialized reports whether the metadata file exists, i.e. whether secure mode has been enabled for this vault.

func (*Vault) Key added in v0.1.0

func (v *Vault) Key() []byte

Key returns a copy of the current session key, or nil if the vault is locked.

The returned slice is an independent copy — mutating it does not affect the key stored inside the vault, and the vault's Lock method will not zero the copy. Callers that hold the key beyond the immediate call should zero it explicitly (e.g. with a deferred loop) once it is no longer needed, to shorten the window during which key material sits in memory.

The primary use case is bridging to an API that stores or passes around a raw key (as opposed to driving all encryption through the vault itself). For normal encrypt/decrypt operations, prefer Encrypt, Decrypt, ReadFile, and WriteFile, which never expose the key.

func (*Vault) Lock

func (v *Vault) Lock()

Lock zeroes and discards the session key. After Lock, Encrypt/Decrypt/ ReadFile/WriteFile return ErrLocked until the next Unlock.

func (*Vault) Locked

func (v *Vault) Locked() bool

Locked reports whether the session key is absent.

func (*Vault) ReadFile

func (v *Vault) ReadFile(path string) ([]byte, error)

ReadFile reads path and decrypts it with the session key. Returns ErrLocked if locked.

func (*Vault) Rekey

func (v *Vault) Rekey(newPassword string, files []string) error

Rekey migrates files to a new password: it decrypts each path with the current key, rotates the vault to newPassword, then re-encrypts each path with the new key. The vault must be unlocked.

Files that do not exist are skipped. If a file fails to decrypt with the current key it is left untouched and an error is returned before any metadata change, so a wrong assumption cannot corrupt data.

func (*Vault) Unlock

func (v *Vault) Unlock(password string) error

Unlock derives the key from password, verifies it against the stored sentinel, and on success stores the session key. It reconstructs the KDF and cipher from metadata, so a vault created with non-default primitives unlocks correctly regardless of the options passed to NewVault.

It returns ErrWrongPassword on mismatch and ErrNotInitialized if no metadata file exists.

func (*Vault) WriteFile

func (v *Vault) WriteFile(path string, data []byte, perm os.FileMode) error

WriteFile encrypts data and writes it to path with the given permissions. Returns ErrLocked if locked.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL