app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 9701a78ac02d186d736cbe5414120a2192298fd7
parent e1f4d689803b32d9a48363344cbf9c18c7dbe69f
Author: triesap <tyson@radroots.org>
Date:   Wed,  8 Apr 2026 16:39:34 +0000

security: align native host vault posture

Diffstat:
Mcrates/apple/security/src/vault.rs | 69++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mcrates/desktop/src/main.rs | 5+++--
Mcrates/desktop/src/remote_signer.rs | 4++--
Mcrates/ios/src/remote_signer.rs | 4++--
Mcrates/ios/src/storage.rs | 4+++-
5 files changed, 62 insertions(+), 24 deletions(-)

diff --git a/crates/apple/security/src/vault.rs b/crates/apple/security/src/vault.rs @@ -3,9 +3,8 @@ use crate::security::{ remove_secret, remove_secret_namespace, store_secret, }; use radroots_secret_vault::{ - RadrootsHostVaultCapabilities, RadrootsHostVaultHardwarePolicy, RadrootsHostVaultPolicy, - RadrootsHostVaultResidency, RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault, - RadrootsSecretVaultAccessError, + RadrootsHostVaultCapabilities, RadrootsHostVaultPolicy, RadrootsHostVaultResidency, + RadrootsHostVaultUserPresencePolicy, RadrootsSecretVault, RadrootsSecretVaultAccessError, }; use zeroize::Zeroizing; @@ -13,32 +12,56 @@ use zeroize::Zeroizing; pub struct RadrootsAppleKeychainVault { service_name: String, namespace: String, + default_policy: RadrootsHostVaultPolicy, } impl RadrootsAppleKeychainVault { #[must_use] - pub fn new(service_name: impl Into<String>) -> Self { - Self::new_with_namespace(service_name, APPLE_NOSTR_NAMESPACE) + pub fn new_desktop(service_name: impl Into<String>) -> Self { + Self::new_with_namespace_desktop(service_name, APPLE_NOSTR_NAMESPACE) } #[must_use] - pub fn new_with_namespace( + pub fn new_device_local(service_name: impl Into<String>) -> Self { + Self::new_with_namespace_device_local(service_name, APPLE_NOSTR_NAMESPACE) + } + + #[must_use] + pub fn new_with_namespace_desktop( + service_name: impl Into<String>, + namespace: impl Into<String>, + ) -> Self { + Self::new_with_namespace_and_policy(service_name, namespace, Self::desktop_policy()) + } + + #[must_use] + pub fn new_with_namespace_device_local( + service_name: impl Into<String>, + namespace: impl Into<String>, + ) -> Self { + Self::new_with_namespace_and_policy(service_name, namespace, Self::device_local_policy()) + } + + fn new_with_namespace_and_policy( service_name: impl Into<String>, namespace: impl Into<String>, + default_policy: RadrootsHostVaultPolicy, ) -> Self { Self { service_name: service_name.into(), namespace: namespace.into(), + default_policy, } } #[must_use] - pub const fn secure_local_policy() -> RadrootsHostVaultPolicy { - RadrootsHostVaultPolicy { - residency: RadrootsHostVaultResidency::DeviceLocalOnly, - user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, - hardware: RadrootsHostVaultHardwarePolicy::Any, - } + pub const fn desktop_policy() -> RadrootsHostVaultPolicy { + RadrootsHostVaultPolicy::desktop() + } + + #[must_use] + pub const fn device_local_policy() -> RadrootsHostVaultPolicy { + RadrootsHostVaultPolicy::device_local() } fn capabilities() -> RadrootsHostVaultCapabilities { @@ -106,7 +129,7 @@ impl RadrootsAppleKeychainVault { impl RadrootsSecretVault for RadrootsAppleKeychainVault { fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> { - self.store_secret_with_policy(slot, secret, Self::secure_local_policy()) + self.store_secret_with_policy(slot, secret, self.default_policy) } fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> { @@ -141,9 +164,21 @@ mod tests { }; #[test] - fn secure_local_policy_is_device_local_without_user_presence() { + fn desktop_policy_matches_shared_desktop_contract() { + assert_eq!( + RadrootsAppleKeychainVault::desktop_policy(), + RadrootsHostVaultPolicy { + residency: RadrootsHostVaultResidency::UserProfile, + user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, + hardware: RadrootsHostVaultHardwarePolicy::Any, + } + ); + } + + #[test] + fn device_local_policy_matches_shared_mobile_contract() { assert_eq!( - RadrootsAppleKeychainVault::secure_local_policy(), + RadrootsAppleKeychainVault::device_local_policy(), RadrootsHostVaultPolicy { residency: RadrootsHostVaultResidency::DeviceLocalOnly, user_presence: RadrootsHostVaultUserPresencePolicy::NotRequired, @@ -155,7 +190,7 @@ mod tests { #[cfg(not(any(target_os = "ios", target_os = "macos")))] #[test] fn vault_operations_report_unavailable_off_apple() { - let vault = RadrootsAppleKeychainVault::new(crate::APPLE_NOSTR_SERVICE); + let vault = RadrootsAppleKeychainVault::new_desktop(crate::APPLE_NOSTR_SERVICE); let load = vault.load_secret("alice").expect_err("load off apple"); assert!(load.to_string().starts_with("secret vault access error:")); @@ -172,7 +207,7 @@ mod tests { #[cfg(any(target_os = "ios", target_os = "macos"))] #[test] fn hardware_backed_requirement_reports_unsupported() { - let vault = RadrootsAppleKeychainVault::new(crate::APPLE_NOSTR_SERVICE); + let vault = RadrootsAppleKeychainVault::new_device_local(crate::APPLE_NOSTR_SERVICE); let error = vault .store_secret_with_policy( "alice", diff --git a/crates/desktop/src/main.rs b/crates/desktop/src/main.rs @@ -184,7 +184,7 @@ impl DesktopBackend { } let store = Arc::new(RadrootsNostrFileAccountStore::new(accounts_path)); - let vault = Arc::new(RadrootsAppleKeychainVault::new(APPLE_NOSTR_SERVICE)); + let vault = Arc::new(RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE)); RadrootsNostrAccountsManager::new(store, vault).map_err(|source| source.to_string()) } @@ -1115,7 +1115,8 @@ mod tests { #[test] fn apple_keychain_vault_round_trips_secret_hex() { - let vault = RadrootsAppleKeychainVault::new("org.radroots.app.tests.desktop.roundtrip"); + let vault = + RadrootsAppleKeychainVault::new_desktop("org.radroots.app.tests.desktop.roundtrip"); let account_id = RadrootsIdentityId::parse( "3bf0c63f0f4478a288f6b67f0429dbf7f5119d4fa7218a4c40ef1378f80f7606", ) diff --git a/crates/desktop/src/remote_signer.rs b/crates/desktop/src/remote_signer.rs @@ -438,14 +438,14 @@ fn sessions_path() -> Result<PathBuf, String> { } fn client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_with_namespace( + RadrootsAppleKeychainVault::new_with_namespace_desktop( APPLE_NOSTR_SERVICE, RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, ) } fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new(APPLE_NOSTR_SERVICE) + RadrootsAppleKeychainVault::new_desktop(APPLE_NOSTR_SERVICE) } fn client_secret_slot(client_account_id: &str) -> Result<String, String> { diff --git a/crates/ios/src/remote_signer.rs b/crates/ios/src/remote_signer.rs @@ -411,14 +411,14 @@ fn sessions_path() -> Result<PathBuf, String> { } fn client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new_with_namespace( + RadrootsAppleKeychainVault::new_with_namespace_device_local( APPLE_NOSTR_SERVICE, RADROOTS_APP_REMOTE_SIGNER_SECRET_NAMESPACE, ) } fn legacy_client_secret_vault() -> RadrootsAppleKeychainVault { - RadrootsAppleKeychainVault::new(APPLE_NOSTR_SERVICE) + RadrootsAppleKeychainVault::new_device_local(APPLE_NOSTR_SERVICE) } fn client_secret_slot(client_account_id: &str) -> Result<String, String> { diff --git a/crates/ios/src/storage.rs b/crates/ios/src/storage.rs @@ -63,7 +63,9 @@ pub(crate) fn app_data_root() -> Result<PathBuf, String> { #[cfg(target_os = "ios")] pub(crate) fn accounts_manager() -> Result<RadrootsNostrAccountsManager, String> { let store = Arc::new(RadrootsNostrFileAccountStore::new(accounts_path()?)); - let vault = Arc::new(RadrootsAppleKeychainVault::new(APPLE_NOSTR_SERVICE)); + let vault = Arc::new(RadrootsAppleKeychainVault::new_device_local( + APPLE_NOSTR_SERVICE, + )); RadrootsNostrAccountsManager::new(store, vault).map_err(|source| source.to_string()) }