Lockr
A local-first CLI that keeps API keys in one AES-256-GCM vault on your own machine — no accounts, no sync, no servers.
Overview
What is Lockr
Lockr is a local-first CLI for the API keys and credentials you need on your own machine. Everything lives in one AES-256-GCM vault file, sealed with an Argon2id-derived key, and the master password is typed once and cached in the OS keychain. There is no account, no sync and no server anywhere in the tool.
- Role
- Creator & maintainer — designed the vault format, the crypto layer, the CLI and the release pipeline.
- Platform
- macOS · Linux · Windows
- Stack
- Go · Cobra · Argon2id · AES-256-GCM
- Storage
- one local vault file, mode 0600
- Release
- v1.1.0 · GoReleaser · Homebrew tap
Contribution
What I built
- 01Designed the vault: one encrypted file at ~/.lockr/vault.enc (mode 0600) — no server, account, sync or network dependency anywhere in the tool.
- 02Built the crypto layer: Argon2id key derivation (one pass, 64 MiB, four threads, 32-byte key) over a fresh 16-byte salt, and AES-256-GCM sealing the whole vault under a new nonce on every write.
- 03Made it fast to use: the master password is cached in the native keychain on macOS, Linux and Windows, typed once and flushed with `lockr lock` (or wiped by `lockr destroy`).
- 04Shaped the CLI around real work: group namespacing (work/stripe_key), `list` that masks values and flags secrets older than 90 days, `get --copy` straight to the clipboard, and `export` that prints shell assignments.
- 05Shipped it properly: a `v*` tag drives GoReleaser on GitHub Actions to build CGO-free static binaries for five OS/arch targets and update the Homebrew tap in the same run.
How it works
How the data moves
The core paths through the system — a dot travels each hop, and the caption says what happens there.
Storing a secret
Type a value once; the whole vault is re-derived and re-sealed around it.
Swipe sideways to see the whole chart.
setYou store a secret under a group: work/stripe_key.
From the code
A few lines that matter
// DeriveKey derives a 32-byte encryption key from a
// master password and salt. It uses Argon2id, which is
// resistant to both GPU and side-channel attacks.
func DeriveKey(password string, salt []byte) []byte {
// time=1, memory=64MB, threads=4, keyLen=32 bytes
return argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
}One pass over 64 MiB with four lanes — slow enough to bother an attacker, instant for you.
func Encrypt(key, plaintext []byte) ([]byte, []byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, nil, err
}
// Generate a random nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, nil, err
}
// Seal encrypts and authenticates the plaintext
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
return ciphertext, nonce, nil
}AES-256-GCM authenticates as well as encrypts; the nonce is fresh on every write.
Decisions
Calls that shaped it
A single encrypted file instead of a service
A vault you can back up, diff and delete beats another account — and there is nothing to breach on a server.
Re-derive and re-seal the whole vault on every write
A fresh salt, key and nonce per write keeps GCM safe and the code simple; the vault is small enough that it costs nothing.
Keychain cache cleared explicitly, not by timer
Retrieval stays one command; `lockr lock` is the deliberate way to forget the password.
Print environment assignments rather than wrap the process
`eval $(lockr export work)` fits any shell and tool; the trade-off — values live in the shell environment — is explicit.
Stack
- Go
- Cobra
- AES-256-GCM
- Argon2id
- OS Keychain
- GoReleaser
- GitHub Actions
- Homebrew