Skip to content

feat(secrets): AES-GCM encrypted upstream api_keys + grob secrets CLI#275

Merged
Destynova2 merged 1 commit intomainfrom
feat/grob-secrets
Apr 25, 2026
Merged

feat(secrets): AES-GCM encrypted upstream api_keys + grob secrets CLI#275
Destynova2 merged 1 commit intomainfrom
feat/grob-secrets

Conversation

@Destynova2
Copy link
Copy Markdown
Contributor

Summary

Adds an encrypted-at-rest store for upstream provider API keys, reusing the existing StorageCipher (AES-256-GCM) used for OAuth tokens. The cleartext key never touches config.toml, shell history, or dotfiles.

Three modes for [[providers]] api_key (resolved at startup)

Mode Where the value lives Status
secret:<name> ~/.grob/secrets/<name>.enc (AES-GCM) ✅ recommended
$ENV_VAR process env now actually resolved (was silent before)
<plain string> cleartext in TOML ⚠️ accepted for dev only

A startup log line confirms each resolution:

🔐 Resolved api_key for provider 'minimax' from grob secret 'minimax'
🔓 Resolved api_key for provider 'groq' from env var $GROQ_API_KEY

Public surface

grob secrets add minimax           # prompts for value (or pipe via stdin)
grob secrets list [--json]         # names only — never values
grob secrets show minimax          # redacted (sk-a...XJ7Q)
grob secrets show minimax --unsafe-show
grob secrets rm minimax [--force]

add reads the value from stdin and rejects empty input. list never displays values (verified by unit test). show is redacted by default; --unsafe-show reveals.

Files changed

  • src/storage/mod.rsset_secret/get_secret/list_secrets/remove_secret + 4 unit tests
  • src/commands/secrets.rs — new module (4 actions + 2 redaction unit tests)
  • src/commands/mod.rs — module declaration
  • src/cli/args.rsCommands::Secrets + enum SecretsAction
  • src/main.rs — dispatch
  • src/server/init.rsresolve_provider_secrets() clones the providers list and substitutes resolved keys (backward compat: ProviderConfig struct unchanged)
  • NEW docs/how-to/manage-secrets.md — migration steps from env vars, trade-offs vs Vault/KMS

Test plan

  • cargo test --lib storage::tests::test_secret — 4 passed
  • cargo test --lib commands::secrets:: — 2 passed
  • E2E verified: printf 'sk-x' | grob secrets add foo, grob secrets list (no value leak), grob secrets show foo (redacted), grob secrets show foo --unsafe-show (revealed), grob secrets rm foo --force
  • cargo clippy --lib --tests --bin grob -- -D warnings clean
  • All prek pre-commit + pre-push hooks green
  • CI green (auto-merge enabled below)

Out of scope (follow-up)

  • grob secrets export-key --to <file> --password <prompt> and import-key for portable backup of the master AES key (PBKDF2 password wrap)
  • SecretBackend trait + File / Vault / KMS backends — see P-276 (feat/secret-backend-trait, drafted)

Migration

Drop-in compatible with existing $VAR providers (now actually resolved). For new secrets:

printf '%s' "$MINIMAX_API_KEY" | grob secrets add minimax
# Then in your TOML:
api_key = "secret:minimax"
# Then unset the env var.

Adds an encrypted-at-rest store for upstream provider API keys, reusing
the existing StorageCipher (AES-256-GCM) used for OAuth tokens. The
cleartext key never touches config.toml, shell history, or dotfiles.

Three modes for [[providers]] api_key (resolved at startup):
  - "secret:<name>" → looked up in ~/.grob/secrets/<name>.enc
  - "$ENV_VAR"      → resolved from process env (formerly silent)
  - "<plain>"       → used as-is (least secure, accepted for dev)

Public surface:
  - GrobStore::set_secret / get_secret / list_secrets / remove_secret
  - grob secrets add  (reads value from stdin, supports `printf | grob ...`)
  - grob secrets list (names only, --json supported)
  - grob secrets show (redacted by default, --unsafe-show to reveal)
  - grob secrets rm   (interactive confirm, --force to skip)

Resolution happens once in init_core_services::resolve_provider_secrets,
which clones config.providers and substitutes resolved api_keys. The
ProviderConfig struct itself is unchanged (full backward compatibility).

Tests: 4 unit tests for the storage roundtrip + list-no-leak + remove +
overwrite; 2 unit tests for the CLI redaction helper. End-to-end
verified via the actual `grob secrets {add,list,show,rm}` flow.

Doc: NEW docs/how-to/manage-secrets.md with migration steps from env
vars and trade-offs vs Vault / KMS (coming via SecretBackend trait,
P-276).

Out of scope (follow-up):
  - grob secrets export-key / import-key (PBKDF2 password wrap)
  - SecretBackend trait + File / Vault / KMS backends (P-276)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant