Skip to content

Latest commit

 

History

History
118 lines (84 loc) · 5.02 KB

File metadata and controls

118 lines (84 loc) · 5.02 KB

Design Document

Motivation

Tools like shipkey rely on 1Password or Bitwarden CLI as secret backends. Coffer provides a self-hosted local alternative with a compatible CLI interface, removing the dependency on third-party services.

Architecture: Library + Binary

coffer-core (library)        coffer-cli (binary)
┌─────────────────────┐      ┌──────────────────┐
│  Coffer API          │◄────│  clap parsing     │
│  ├── crypto.rs       │      │  rpassword prompt │
│  ├── store.rs        │      └──────────────────┘
│  └── error.rs        │
└─────────────────────┘
         ▲
         │ (future)
    macOS GUI App
    via uniffi / C FFI

coffer-core is a pure library with no terminal I/O, no stdin/stdout assumptions. This separation exists so that:

  • The CLI is just one thin consumer of the library
  • A future macOS SwiftUI app can link coffer-core directly via uniffi or cbindgen C FFI
  • macOS Keychain / Touch ID integration can be layered on top without modifying core logic

Storage Model

All data lives in a single SQLite file for easy backup and migration:

~/.coffer/
└── store.db

Schema

secrets table — one row per secret:

Column Type Notes
vault, provider, project, env, field TEXT Composite unique key, stored in plaintext for queryability
encrypted_value BLOB AES-256-GCM ciphertext
nonce BLOB 12-byte random nonce (unique per write)
created_at, updated_at TEXT RFC 3339 timestamps

meta table — key-value store for:

Key Value
password_salt 32-byte random salt for Argon2id
password_verify Encrypted known-plaintext token
password_verify_nonce Nonce for the verify token
version Schema version (1)

Encryption Design

Key Derivation

User Password ──[Argon2id(default params) + 32-byte salt]──> 256-bit Master Key
  • Argon2id is the winner of the Password Hashing Competition, resistant to both side-channel and GPU attacks
  • Salt is generated once at init time via OS CSPRNG and stored in the meta table
  • The master password is never stored; only the derived key exists transiently in memory

Field-Level Encryption

Secret Value ──[AES-256-GCM + 12-byte random nonce]──> (ciphertext, nonce)
  • AES-256-GCM provides authenticated encryption (confidentiality + integrity + authenticity)
  • A fresh random nonce is generated for every write, including updates to existing secrets
  • Tampering with ciphertext is detected at decryption time (GCM authentication tag verification)

Password Verification

On init, the string "coffer-password-verify" is encrypted with the master key. On subsequent open calls, this token is decrypted — if decryption succeeds and the plaintext matches, the password is correct. This avoids storing any password hash that could be attacked offline independently of the encrypted data.

What Is NOT Encrypted

Metadata fields (vault, provider, project, env, field) are stored in plaintext. This is a deliberate trade-off:

  • Enables SQL WHERE filtering without decrypting every row
  • Secret names are typically not sensitive (e.g., GITHUB_TOKEN); the values are
  • If metadata confidentiality is needed in the future, the schema can be extended without breaking existing stores

CLI Design

The CLI interface is designed to be compatible with shipkey's SecretBackend interface:

CLI Command SecretBackend Method
coffer status checkStatus() / isAvailable()
coffer get --vault ... --field ... read(ref)
coffer set --vault ... --field ... --value ... write(entry)
coffer list [--project ...] [--env ...] list(project?, env?)
coffer delete --vault ... --field ... (extension)

Output conventions:

  • get prints the raw secret value to stdout (no trailing newline), suitable for $(coffer get ...)
  • list and status output JSON to stdout
  • Human-readable messages (Secret saved, Secret deleted) go to stderr

Upsert Strategy

set uses an update-first approach: attempt UPDATE by the 5-field composite key, and if zero rows were affected, fall back to INSERT. This avoids SQLite's ON CONFLICT upsert syntax while keeping the logic simple and the UNIQUE constraint enforced.

Future Directions

  • shipkey backend integration — a coffer.ts backend in shipkey that shells out to the coffer binary
  • macOS GUI App — SwiftUI app linking coffer-core via uniffi, with Keychain-stored master password and Touch ID unlock
  • Password change — re-encrypt all secrets with a new master key derived from a new password
  • Export/import — plaintext JSON export (encrypted at rest, decrypted for transfer) for migration between machines