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¶
#[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>, which 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 keyring-core |
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.
geton an absent key returnsNotFoundwith the name"{service}/{account}".setinserts or overwrites.deleteremoves the key. Deleting an entry that is not present is not an error: it returnsOk(()).- If the internal
RwLockis poisoned by a panic in another thread, every method returnsCredentialError::Keychainwith the message"in-memory lock poisoned". Note the variant isKeychaindespite 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 a keyring_core::Entry, built fresh per call from the
service and account strings by the platform store: the macOS
Keychain, the Windows Credential Manager, or the freedesktop Secret
Service on Linux and other Unix targets. KeyringStore::new() is
const and holds no handles; nothing touches the platform keychain
until the first get, set or delete.
The platform store is connected once per process, on first use, and
shared by every KeyringStore. A failed connection is not remembered,
so a session bus that appears later is picked up by the next call. The
store is held privately: keyring_core::set_default_store is never
called, so an application that sets its own default store is not
affected.
On Linux, items are found by their service and username attributes
and created in the default collection with the label
keyring:{account}@{service}. Releases 0.6.0 to 0.6.5 wrote through
a different Secret Service client with the same attributes, label and
collection, so the secrets they stored are found unchanged.
Error mapping:
| Condition | Returns |
|---|---|
entry absent (keyring_core::Error::NoEntry) on get |
NotFound { name: "{service}/{account}" } |
any other keychain error on get or set, including no store connection |
Keychain(String) carrying the backend message |
delete on an absent entry |
Ok(()), treated as success |
any other keychain error on delete |
Keychain(String) |
| the blocking task fails to join | Keychain("join error: …") |
KeyringStore requires a Tokio runtime¶
Each method wraps its blocking keychain 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.
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:
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:
EnvStore and KeyringStore derive Debug and are unit structs
holding no data.