Implement secure OS-backed secret storage

This commit is contained in:
Hermes Agent
2026-08-10 00:23:40 +00:00
parent 75ce19da00
commit b685a4864c
14 changed files with 1838 additions and 9 deletions

View File

@@ -0,0 +1,236 @@
//! Safe adapters around native credential-store crates.
//!
//! This is the only module that selects operating-system implementations.
//! IronStorage performs no direct FFI and contains no unsafe code: the Apple,
//! Windows, and Linux crates own their respective Security Framework,
//! Credential Manager, and Secret Service boundaries. Operations are serialized
//! by the parent `SecretStore`, as required by the Windows adapter contract.
use std::sync::Arc;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use std::collections::HashMap;
use keyring_core::{CredentialStore, Entry};
use super::{SecretLocator, SecretProtection, SecretStoreBackend, SecretStoreError};
use crate::repository::SecretBytes;
pub struct NativeSecretBackend {
#[cfg(any(target_os = "linux", target_os = "windows"))]
store: Arc<CredentialStore>,
#[cfg(target_os = "ios")]
protected: Arc<CredentialStore>,
#[cfg(target_os = "macos")]
keychain: Arc<CredentialStore>,
#[cfg(target_os = "macos")]
protected: Arc<CredentialStore>,
}
impl NativeSecretBackend {
pub fn new() -> Result<Self, SecretStoreError> {
#[cfg(target_os = "linux")]
{
let store: Arc<CredentialStore> =
zbus_secret_service_keyring_store::Store::new().map_err(map_error)?;
Ok(Self { store })
}
#[cfg(target_os = "windows")]
{
let store: Arc<CredentialStore> =
windows_native_keyring_store::Store::new().map_err(map_error)?;
Ok(Self { store })
}
#[cfg(target_os = "ios")]
{
let protected: Arc<CredentialStore> =
apple_native_keyring_store::protected::Store::new().map_err(map_error)?;
Ok(Self { protected })
}
#[cfg(target_os = "macos")]
{
let keychain: Arc<CredentialStore> =
apple_native_keyring_store::keychain::Store::new().map_err(map_error)?;
let protected: Arc<CredentialStore> =
apple_native_keyring_store::protected::Store::new().map_err(map_error)?;
Ok(Self {
keychain,
protected,
})
}
#[cfg(not(any(
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "windows"
)))]
{
Err(SecretStoreError::Unavailable)
}
}
fn entry(
&self,
locator: &SecretLocator,
protection: SecretProtection,
) -> Result<Entry, SecretStoreError> {
let (service, user) = locator.service_and_user();
#[cfg(any(target_os = "linux", target_os = "windows"))]
{
if protection == SecretProtection::RequireUserPresence {
return Err(SecretStoreError::UnsupportedProtection);
}
self.store.build(service, &user, None).map_err(map_error)
}
#[cfg(target_os = "ios")]
{
let modifiers = presence_modifiers(protection);
self.protected
.build(service, &user, modifiers.as_ref())
.map_err(map_error)
}
#[cfg(target_os = "macos")]
{
match protection {
SecretProtection::DeviceUnlocked => {
self.keychain.build(service, &user, None).map_err(map_error)
}
SecretProtection::RequireUserPresence => {
let modifiers = presence_modifiers(protection);
self.protected
.build(service, &user, modifiers.as_ref())
.map_err(map_error)
}
}
}
#[cfg(not(any(
target_os = "ios",
target_os = "linux",
target_os = "macos",
target_os = "windows"
)))]
{
let _ = (service, user, protection);
Err(SecretStoreError::Unavailable)
}
}
}
impl SecretStoreBackend for NativeSecretBackend {
fn create(
&self,
locator: &SecretLocator,
protection: SecretProtection,
value: &[u8],
) -> Result<(), SecretStoreError> {
let entry = self.entry(locator, protection)?;
match entry.get_secret() {
Ok(existing) => {
drop(SecretBytes::new(existing));
Err(SecretStoreError::AlreadyExists)
}
Err(keyring_core::Error::NoEntry) => entry.set_secret(value).map_err(map_error),
Err(error) => Err(map_error(error)),
}
}
fn retrieve(
&self,
locator: &SecretLocator,
protection: SecretProtection,
) -> Result<SecretBytes, SecretStoreError> {
self.entry(locator, protection)?
.get_secret()
.map(SecretBytes::new)
.map_err(map_error)
}
fn replace(
&self,
locator: &SecretLocator,
protection: SecretProtection,
value: &[u8],
) -> Result<(), SecretStoreError> {
let entry = self.entry(locator, protection)?;
let existing = entry.get_secret().map_err(map_error)?;
drop(SecretBytes::new(existing));
entry.set_secret(value).map_err(map_error)
}
fn delete(
&self,
locator: &SecretLocator,
protection: SecretProtection,
) -> Result<(), SecretStoreError> {
self.entry(locator, protection)?
.delete_credential()
.map_err(map_error)
}
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn presence_modifiers(protection: SecretProtection) -> Option<HashMap<&'static str, &'static str>> {
match protection {
SecretProtection::DeviceUnlocked => None,
SecretProtection::RequireUserPresence => {
Some(HashMap::from([("access-policy", "require-user-presence")]))
}
}
}
fn map_error(error: keyring_core::Error) -> SecretStoreError {
match error {
keyring_core::Error::NoEntry => SecretStoreError::Missing,
keyring_core::Error::NoStorageAccess(error) => platform_access_error(&error),
keyring_core::Error::BadEncoding(bytes) => {
drop(SecretBytes::new(bytes));
SecretStoreError::Corrupted
}
keyring_core::Error::BadDataFormat(bytes, _) => {
drop(SecretBytes::new(bytes));
SecretStoreError::Corrupted
}
keyring_core::Error::BadStoreFormat(_) | keyring_core::Error::Ambiguous(_) => {
SecretStoreError::Corrupted
}
keyring_core::Error::TooLong(_, _) | keyring_core::Error::Invalid(_, _) => {
SecretStoreError::InvalidReference
}
keyring_core::Error::NotSupportedByStore(_) | keyring_core::Error::NoDefaultStore => {
SecretStoreError::Unavailable
}
keyring_core::Error::PlatformFailure(error) => {
if platform_cancelled(&error) {
SecretStoreError::Cancelled
} else {
SecretStoreError::Unavailable
}
}
_ => SecretStoreError::Unavailable,
}
}
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn platform_cancelled(error: &keyring_core::error::PlatformError) -> bool {
error
.downcast_ref::<security_framework::base::Error>()
.is_some_and(|error| error.code() == -128)
}
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
fn platform_cancelled(_error: &keyring_core::error::PlatformError) -> bool {
false
}
#[cfg(target_os = "linux")]
fn platform_access_error(error: &keyring_core::error::PlatformError) -> SecretStoreError {
match error.downcast_ref::<secret_service::Error>() {
Some(secret_service::Error::Prompt) => SecretStoreError::Cancelled,
_ => SecretStoreError::Denied,
}
}
#[cfg(not(target_os = "linux"))]
fn platform_access_error(_error: &keyring_core::error::PlatformError) -> SecretStoreError {
SecretStoreError::Denied
}