//! `SecretStorageRepository` trait + Linux Secret Service adapter. //! //! Maps to SDD-078 (`SecretStorageRepository shall persist secrets only //! through platform secure storage service interfaces`) and ADR-006 //! (`SecureStore trait and per-platform adapters`). use std::fmt; use zeroize::Zeroize; /// Trait every platform adapter implements. /// /// The API is intentionally narrow: a secret is identified by a stable /// `name` (e.g. `"identity.primary"`, `"server..password"`) /// and treated as an opaque UTF-8 byte sequence. The trait does **not** /// expose handles, paths, or backend-specific types — the /// `chanora_storage` crate must not leak those upward (SAD-067). pub trait SecretStorageRepository: Send + Sync { /// Store (or replace) a secret under `name`. The implementation /// must not write `secret` to plaintext disk, logs, or diagnostic /// exports (SS-AUD-001..004). fn set(&self, name: &str, secret: &Secret) -> Result<(), SecureStoreError>; /// Retrieve a secret previously stored under `name`. /// /// Returns `Err(SecureStoreError::NotFound)` if no entry exists. fn get(&self, name: &str) -> Result; /// Remove a secret. Returns `Err(SecureStoreError::NotFound)` if /// it was already absent. Idempotent variants are intentionally /// not provided here; callers must handle `NotFound`. fn delete(&self, name: &str) -> Result<(), SecureStoreError>; } /// Typed error DTO. Production code (`chanora_bridge`) will widen this /// when more failure modes are observed; the PoC only needs to prove /// SS-AUD-005 (safe error mapping). #[derive(Debug, thiserror::Error)] pub enum SecureStoreError { #[error("secret not found")] NotFound, #[error("platform secure storage unavailable: {0}")] Unavailable(String), #[error("platform secure storage backend rejected the operation: {0}")] Backend(String), } /// Owned, zeroed-on-drop secret material. /// /// `Debug` and `Display` deliberately do **not** print the inner bytes /// — this is part of the SS-AUD-003 (no-secrets-in-logs) defense in /// depth. Tests in this crate rely on this behavior. #[derive(Clone, Zeroize)] #[zeroize(drop)] pub struct Secret(Vec); impl Secret { pub fn from_utf8(s: impl Into) -> Self { Self(s.into().into_bytes()) } pub fn from_bytes(b: impl Into>) -> Self { Self(b.into()) } pub fn as_bytes(&self) -> &[u8] { &self.0 } pub fn as_str(&self) -> Result<&str, std::str::Utf8Error> { std::str::from_utf8(&self.0) } pub fn len(&self) -> usize { self.0.len() } pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl fmt::Debug for Secret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Secret(<{} bytes redacted>)", self.0.len()) } } impl fmt::Display for Secret { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // Same redaction shape as Debug. write!(f, "") } } impl PartialEq for Secret { fn eq(&self, other: &Self) -> bool { // Constant-time-ish compare via fixed length-first then byte compare. // The PoC does not promise true constant-time semantics. self.0 == other.0 } } // ---------- Linux adapter ---------- #[cfg(target_os = "linux")] pub mod linux { use super::*; use keyring::Entry; /// Which Linux backend to use. /// /// Both are acceptable under SysRS-053 / SysRS-162 ("Secret Service, /// libsecret, or equivalent"): /// /// * `SecretService`: D-Bus Secret Service (gnome-keyring, kwallet, /// KeePassXC, etc.). Preferred on interactive desktop sessions. /// * `Keyutils`: kernel session keyring (`add_key(2)` / /// `request_key(2)`). Always available on Linux, no D-Bus /// dependency, but secrets live only for the session lifetime. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum LinuxBackend { /// Default user keyring backend selected by the `keyring` crate /// at compile time. With the features this PoC enables, this /// is Secret Service. Default, /// Force the kernel keyutils backend. Useful for headless /// environments and for production code paths where no /// graphical session is available. Keyutils, } /// Linux adapter using the `keyring` crate. The choice of backend /// is made at construction so the rest of the application can /// remain backend-agnostic. pub struct LinuxSecureStore { service: String, backend: LinuxBackend, } impl LinuxSecureStore { /// Construct an adapter backed by the default `keyring` /// credential store (Secret Service with the features compiled /// in this PoC). pub fn new(service: impl Into) -> Self { Self { service: service.into(), backend: LinuxBackend::Default } } /// Construct an adapter backed by the kernel keyutils session /// keyring. Production code may select this when no D-Bus /// session is available. pub fn new_keyutils(service: impl Into) -> Self { Self { service: service.into(), backend: LinuxBackend::Keyutils } } fn entry(&self, name: &str) -> Result { let result = match self.backend { LinuxBackend::Default => Entry::new(&self.service, name), LinuxBackend::Keyutils => { let cred = keyring::keyutils::KeyutilsCredential::new_with_target( None, &self.service, name, ) .map_err(|e| SecureStoreError::Unavailable(format!("{e}")))?; Ok(Entry::new_with_credential(Box::new(cred))) } }; result.map_err(|e| SecureStoreError::Unavailable(format!("{e}"))) } } impl SecretStorageRepository for LinuxSecureStore { fn set(&self, name: &str, secret: &Secret) -> Result<(), SecureStoreError> { let entry = self.entry(name)?; let s = secret.as_str().map_err(|e| { SecureStoreError::Backend(format!("secret is not valid utf-8: {e}")) })?; entry .set_password(s) .map_err(|e| SecureStoreError::Backend(format!("{e}"))) } fn get(&self, name: &str) -> Result { let entry = self.entry(name)?; match entry.get_password() { Ok(s) => Ok(Secret::from_utf8(s)), Err(keyring::Error::NoEntry) => Err(SecureStoreError::NotFound), Err(e) => Err(SecureStoreError::Backend(format!("{e}"))), } } fn delete(&self, name: &str) -> Result<(), SecureStoreError> { let entry = self.entry(name)?; match entry.delete_credential() { Ok(()) => Ok(()), Err(keyring::Error::NoEntry) => Err(SecureStoreError::NotFound), Err(e) => Err(SecureStoreError::Backend(format!("{e}"))), } } } /// Back-compat type alias — older code in this PoC referred to /// `SecretServiceAdapter` before the backend choice existed. pub type SecretServiceAdapter = LinuxSecureStore; }