rtb-credentials 0.6.4

OS keychain credential storage. Part of the phpboyscout Rust toolkit.
Documentation
---
title: Credential stores
description: The CredentialStore trait and the four built-in implementations, including which methods each one refuses and what each returns on failure.
tags:
  - reference
  - stores
---

# Credential stores

A credential store is a backend that can retrieve, and sometimes write,
a secret identified by a `service` and an `account` pair.

## The `CredentialStore` trait

```rust
#[async_trait]
pub trait CredentialStore: Send + Sync + 'static {
    async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError>;
    async fn set(&self, service: &str, account: &str, secret: SecretString) -> Result<(), CredentialError>;
    async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError>;
}
```

The trait is object-safe, and the intended way to hold one is
`Arc<dyn CredentialStore>` — that is exactly what `Resolver::new`
takes.

The `Send + Sync + 'static` bound is part of the contract, not an
implementation detail: a store is expected to be shared across tasks and
threads for the life of the process.

Every method is `async` even though several implementations do no I/O at
all. Platform keychain APIs are blocking, so the trait has to be async
for the keychain implementation to avoid stalling an executor; the
in-memory implementations pay an unused `async` for uniformity.

## Which methods each built-in store supports

| Store | `get` | `set` | `delete` | Backed by |
|---|---|---|---|---|
| `MemoryStore` | yes | yes | yes | `HashMap` behind an `RwLock` |
| `KeyringStore` | yes | yes | yes | platform keychain via the `keyring` crate |
| `EnvStore` | yes | **`ReadOnly`** | **`ReadOnly`** | process environment |
| `LiteralStore` | yes | **`ReadOnly`** | **`ReadOnly`** | one fixed secret |

`EnvStore` and `LiteralStore` are read-only by design, and their `set`
and `delete` return `CredentialError::ReadOnly` rather than failing
silently or panicking.

## `MemoryStore` — read-write, in-process

Keyed on the `(service, account)` pair. Both strings are used; neither
is ignored.

- `get` on an absent key returns `NotFound` with the name
  `"{service}/{account}"`.
- `set` inserts or overwrites.
- `delete` removes the key. **Deleting an entry that is not present is
  not an error** — it returns `Ok(())`.
- If the internal `RwLock` is poisoned by a panic in another thread,
  every method returns `CredentialError::Keychain` with the message
  `"in-memory lock poisoned"`. Note the variant is `Keychain` despite no
  keychain being involved; there is no dedicated variant for lock
  poisoning.

`MemoryStore` is exported from the crate root, not hidden behind a
`test` feature, so downstream crates can use it in their own test
suites.

Contents do not survive the process. There is no persistence, no
snapshotting and no capacity limit.

## `KeyringStore` — the platform keychain

Delegates to `keyring::Entry`, constructed fresh per call from the
`service` and `account` strings. `KeyringStore::new()` is `const` and
holds no handles; nothing touches the platform keychain until the first
`get`, `set` or `delete`.

Error mapping:

| Condition | Returns |
|---|---|
| entry absent (`keyring::Error::NoEntry`) on `get` | `NotFound { name: "{service}/{account}" }` |
| any other `keyring` error on `get` or `set` | `Keychain(String)` carrying the backend message |
| `delete` on an absent entry | `Ok(())` — treated as success |
| any other `keyring` error on `delete` | `Keychain(String)` |
| the blocking task fails to join | `Keychain("join error: …")` |

### `KeyringStore` requires a Tokio runtime

Each method wraps its blocking `keyring` call in
`tokio::task::spawn_blocking`. That function panics when called outside
a Tokio runtime context.

`KeyringStore` therefore cannot be used on `async-std`, `smol` or any
other executor. Calling it there panics rather than returning an error,
so it is not something a `Result` can catch. The crate depends on
`tokio` with the `full` feature unconditionally; there is no
runtime-agnostic build.

The other three stores do no `spawn_blocking` and are runtime-agnostic.

## `EnvStore` — `account` is the variable name and `service` is ignored

`EnvStore::get` **ignores its `service` argument entirely** and treats
`account` as the name of an environment variable.

```rust
let store = EnvStore::new();
// reads $MYTOOL_TOKEN; the "unused" argument is discarded
let secret = store.get("unused", "MYTOOL_TOKEN").await?;
```

This is the crate's sharpest edge. Called with the conventional
argument order used everywhere else — `get("mytool", "anthropic")` —
`EnvStore` looks for a variable named `anthropic`, not one named
`mytool`, and returns `NotFound` naming `anthropic`.

A missing variable returns `NotFound` with the variable name as `name`.
A variable that is set but empty is a **hit**, returning an empty
`SecretString`: `std::env::var` succeeds on an empty value, and the
store does not check for it.

`set` and `delete` return `ReadOnly`. This is not an oversight —
mutating process environment variables is unsound across threads and
requires `unsafe` in Rust 2024, so the crate declines to offer it.

## `LiteralStore` — one secret, both arguments ignored

Constructed with a single `SecretString`, which `get` returns for **any**
`service` and `account`:

```rust
let store = LiteralStore::new(SecretString::from("baked-in".to_string()));
store.get("anything", "anywhere").await?; // -> "baked-in"
```

`get` never returns `NotFound`. `set` and `delete` return `ReadOnly`.

`get` returns `self.secret.clone()`. Cloning a `SecretString` produces
another zeroed-on-drop container rather than exposing the value through
a plain `String`, so the secret does not leave the protected type on the
way out.

Its intended use is a test harness or a tool hard-wired to exactly one
credential. As a `Resolver` keychain backend it makes every keychain
lookup succeed with the same value, which is rarely what production
wants.

## `Debug` on a store never prints a secret

`MemoryStore` and `LiteralStore` implement `Debug` by hand, both using
`finish_non_exhaustive()`, so neither renders the secrets it holds:

```text
MemoryStore { .. }
LiteralStore { .. }
```

`EnvStore` and `KeyringStore` derive `Debug` and are unit structs
holding no data.