rtb-credentials¶
Secret-handling layer for CLI tools: the CredentialStore async trait,
four built-in stores, a Resolver that walks the canonical precedence
chain, and CredentialRef — the deserialise-only config shape tool
authors embed in their typed configs.
Secrets cross every boundary as
secrecy::SecretString: Debug
renders [REDACTED], memory is zeroed on drop. The crate treats
&str/String for a secret as a type error, not a style preference.
Part of the phpboyscout Rust toolkit; extracted from — and battle-tested by — rust-tool-base.
Storage modes¶
Three storage modes are supported:
| Mode | Lives in | Notes |
|---|---|---|
| Env var | Process environment (shell profile, CI secret injection) | Recommended default. |
| OS keychain | Platform-native (macOS Keychain / Linux keyutils / Windows Credential Manager) | Linux default is pure-Rust kernel keyutils; the linux-persistent feature adds D-Bus Secret Service. |
| Literal | Config file | Legacy. Refused under CI=true. |
Resolver walks all three in order, plus an ecosystem-default env-var
fallback (ANTHROPIC_API_KEY, GITHUB_TOKEN, …). The precedence chain
is normative — do not reorder:
cref.env→std::env::var(name).cref.keychain→store.get(service, account).cref.literal— refused viaCredentialError::LiteralRefusedInCiwhenCI=true.cref.fallback_env→std::env::var(name).
Public API¶
use rtb_credentials::{CredentialRef, CredentialStore, KeyringStore, Resolver};
use std::sync::Arc;
#[derive(serde::Deserialize)]
struct MyCfg {
anthropic: CredentialRef,
}
let cfg: MyCfg = /* ... */;
let store: Arc<dyn CredentialStore> = Arc::new(KeyringStore::new());
let resolver = Resolver::new(store);
let api_key = resolver.resolve(&cfg.anthropic).await?;
use secrecy::ExposeSecret;
request.header("authorization", format!("Bearer {}", api_key.expose_secret()));
| Item | Purpose |
|---|---|
CredentialStore |
Async trait: get / set / delete, all in SecretString. |
KeyringStore |
Platform-native keychain via the keyring crate. Blocking calls wrapped in tokio::task::spawn_blocking. |
EnvStore |
Read-only lookup against the process environment. |
LiteralStore |
Read-only single in-memory secret; test harnesses. |
MemoryStore |
HashMap-backed store behind RwLock — exported test fixture. |
CredentialRef, KeychainRef |
Deserialise-only config shapes (Serialize deliberately not derived — no blind round-trip leaks). |
Resolver (+ probe, ResolutionSource, ResolutionOutcome) |
Walks env > keychain > literal > fallback_env; probe reports which leg would win without exposing the secret. |
CredentialBearing |
Introspection seam: a typed config enumerates its CredentialRefs for credentials list-style tooling. Object-safe; blanket impl for (). |
CredentialError |
thiserror + miette::Diagnostic, Clone-able (Io wraps Arc<std::io::Error>). |
| Re-exports | SecretString, ExposeSecret from secrecy. |
Full API reference: docs.rs/rtb-credentials.
Platform behaviour and the keychain opt-in¶
On Linux the default keyring backend is kernel keyutils
(keyring/linux-native) — pure Rust, no system deps, session-scoped
persistence. This keeps builds hermetic: no libdbus-1-dev /
pkg-config required.
Tools that need reboot-persistent Linux storage enable the
linux-persistent Cargo feature, which extends keyring with the
freedesktop Secret Service (D-Bus) backend. Building with it requires
pkg-config and libdbus-1-dev on the host.
macOS Keychain and Windows Credential Manager store cross-session by default; no feature flag needed.
Keychain support is an opt-in story for downstream tools: enable the
crate (or a wrapping framework feature) only where it is wanted —
regulated downstreams omit it, and link-time dead-code elimination keeps
keyring and its transitive deps out of the binary.
Security¶
#![forbid(unsafe_code)]at the crate root.- Every public fn that touches a secret takes or returns
SecretString. DebugofSecretStringrenders[REDACTED].- Literal-in-CI refusal is a policy check, not a technical one —
tools that want stricter enforcement set
CI=truethemselves. - See the rust-tool-base Engineering Standards §1.2 for the full secret-handling rules.
Design record¶
The authoritative contract is the crate's v0.1 spec, retained in the rust-tool-base spec series. Design rationale highlights:
SecretStringeverywhere.LiteralStore::getreturnsself.secret.clone()so the secret never bounces through a bareString;secrecy0.10+ deliberately omitsSerializeforSecretString, andCredentialRefinherits that limitation.- CI detection via
CI=trueonly — the convention shared by GitHub Actions, GitLab CI, CircleCI, Buildkite and others; broader detection produces false positives in developer shells. - Keyring blocking calls in
spawn_blocking— platform keyring APIs are synchronous; wrapping keeps the async trait honest on any runtime. Arc<std::io::Error>for theIovariant — soCredentialErrorcan deriveCloneand fan out to multiple consumers without re-allocating.