rtb-credentials 0.6.1

OS keychain credential storage. Part of the phpboyscout Rust toolkit.
Documentation
---
title: rtb-credentials
description: OS keychain credential storage with precedence-aware resolution.
---

# `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`](https://crates.io/crates/secrecy): `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](https://rust.phpboyscout.uk);
extracted from — and battle-tested by —
[rust-tool-base](https://gitlab.com/phpboyscout/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.env``std::env::var(name)`.
2. `cref.keychain``store.get(service, account)`.
3. `cref.literal` — refused via `CredentialError::LiteralRefusedInCi`
   when `CI=true`.
4. `cref.fallback_env``std::env::var(name)`.

## Public API

```rust
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`]https://crates.io/crates/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 `CredentialRef`s 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](https://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]https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/engineering-standards.md
  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](https://gitlab.com/phpboyscout/rust-tool-base/-/blob/main/docs/development/specs/2026-04-22-rtb-credentials-v0.1.md).
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.