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:
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())
}