---
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
| `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:
| 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.