From beb822dced1485e8bb6023150533b5aa5ceccee9 Mon Sep 17 00:00:00 2001 From: Duncan Tourolle Date: Sat, 12 Sep 2026 07:34:08 +0200 Subject: [PATCH] Keep secrets in Credential Manager on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Secret Service store was keyring::Entry all the way down, and keyring 4's v1 feature set — the one the workspace already asks for — includes the Windows Credential Manager backend. So the Windows store is the same implementation with its cfg widened, and the crate as a target dependency. The one behavioural difference is that the availability probe always succeeds there, which is correct: Credential Manager is always present, so FR-NC-2's degraded mode does not arise. Until now a Windows build compiled, started, and failed at sign-in with the placeholder store's "no secret store is implemented". --- platform/dr-plat/Cargo.toml | 5 +++++ platform/dr-plat/src/secrets.rs | 30 +++++++++++++++++++----------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/platform/dr-plat/Cargo.toml b/platform/dr-plat/Cargo.toml index 17602da..412d801 100644 --- a/platform/dr-plat/Cargo.toml +++ b/platform/dr-plat/Cargo.toml @@ -24,6 +24,11 @@ x11rb.workspace = true wayland-client.workspace = true wayland-protocols.workspace = true +# The same crate on Windows: its `v1` features include the Credential Manager +# backend, and `secrets.rs` is one implementation over both. +[target.'cfg(windows)'.dependencies] +keyring.workspace = true + [target.'cfg(target_os = "android")'.dependencies] android-native-keyring-store.workspace = true keyring-core.workspace = true diff --git a/platform/dr-plat/src/secrets.rs b/platform/dr-plat/src/secrets.rs index dafbe34..4a5c9d6 100644 --- a/platform/dr-plat/src/secrets.rs +++ b/platform/dr-plat/src/secrets.rs @@ -100,14 +100,22 @@ pub trait SecretStore: Send + Sync { /// /// Used by the two stores that have somewhere to file them; the placeholder /// below has nothing to name, and a Windows build otherwise warns here. -#[cfg(any(all(unix, not(target_os = "android")), target_os = "android"))] +#[cfg(any(all(unix, not(target_os = "android")), windows, target_os = "android"))] const SERVICE: &str = "DarkRoom"; -/// Secret Service implementation (GNOME Keyring, KWallet via ksecretd). -#[cfg(all(unix, not(target_os = "android")))] +/// The `keyring`-backed store: Secret Service on Linux (GNOME Keyring, +/// KWallet via ksecretd), Credential Manager on Windows. +/// +/// One implementation for both because `keyring::Entry` is the same API over +/// either, and its `v1` feature set already includes the Windows backend. The +/// difference is in what "unavailable" means: a Linux desktop may have no +/// secrets daemon, which FR-NC-2 treats as a stated degraded mode, while +/// Credential Manager is always present — so on Windows `is_available` is a +/// probe that always succeeds, and that is correct rather than optimistic. +#[cfg(any(all(unix, not(target_os = "android")), windows))] pub struct PlatformSecretStore; -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(any(all(unix, not(target_os = "android")), windows))] impl PlatformSecretStore { pub fn new() -> Self { Self @@ -118,14 +126,14 @@ impl PlatformSecretStore { } } -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(any(all(unix, not(target_os = "android")), windows))] impl Default for PlatformSecretStore { fn default() -> Self { Self::new() } } -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(any(all(unix, not(target_os = "android")), windows))] impl SecretStore for PlatformSecretStore { fn store(&self, secret_ref: &SecretRef, secret: &str) -> Result<(), SecretError> { Self::entry(secret_ref)? @@ -160,7 +168,7 @@ impl SecretStore for PlatformSecretStore { } } -#[cfg(all(unix, not(target_os = "android")))] +#[cfg(any(all(unix, not(target_os = "android")), windows))] fn map_err(e: keyring::Error) -> SecretError { match e { keyring::Error::NoEntry => SecretError::NotFound, @@ -265,24 +273,24 @@ fn map_err(e: keyring_core::Error) -> SecretError { /// /// Failing loudly is deliberate: a silent no-op store would look like it /// worked and then lose the credential. -#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "android")))] +#[cfg(not(any(all(unix, not(target_os = "android")), windows, target_os = "android")))] pub struct PlatformSecretStore; -#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "android")))] +#[cfg(not(any(all(unix, not(target_os = "android")), windows, target_os = "android")))] impl PlatformSecretStore { pub fn new() -> Self { Self } } -#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "android")))] +#[cfg(not(any(all(unix, not(target_os = "android")), windows, target_os = "android")))] impl Default for PlatformSecretStore { fn default() -> Self { Self::new() } } -#[cfg(not(any(all(unix, not(target_os = "android")), target_os = "android")))] +#[cfg(not(any(all(unix, not(target_os = "android")), windows, target_os = "android")))] impl SecretStore for PlatformSecretStore { fn store(&self, _r: &SecretRef, _s: &str) -> Result<(), SecretError> { Err(SecretError::Unavailable(