Skip to content

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:

  1. cref.envstd::env::var(name).
  2. cref.keychainstore.get(service, account).
  3. cref.literal — refused via CredentialError::LiteralRefusedInCi when CI=true.
  4. cref.fallback_envstd::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.
  • Debug of SecretString renders [REDACTED].
  • Literal-in-CI refusal is a policy check, not a technical one — tools that want stricter enforcement set CI=true themselves.
  • 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:

  • SecretString everywhere. LiteralStore::get returns self.secret.clone() so the secret never bounces through a bare String; secrecy 0.10+ deliberately omits Serialize for SecretString, and CredentialRef inherits that limitation.
  • CI detection via CI=true only — 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 the Io variant — so CredentialError can derive Clone and fan out to multiple consumers without re-allocating.