Implement secure OS-backed secret storage
This commit is contained in:
236
crates/storage/src/secret_store/platform.rs
Normal file
236
crates/storage/src/secret_store/platform.rs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user