From 5ffd86afbc54fcfa511181ec11c0cabf52570beb Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Wed, 12 Aug 2026 16:38:21 +0200 Subject: [PATCH] feat(core): promote protected saved device APIs --- Makefile | 16 ++-- crates/vnidrop/Cargo.toml | 3 + crates/vnidrop/src/api.rs | 29 +++++- .../vnidrop/src/device_relationship/crypto.rs | 4 +- .../src/device_relationship/service.rs | 6 +- crates/vnidrop/src/lib.rs | 12 +-- crates/vnidrop/src/pairing_eligibility/mod.rs | 5 +- crates/vnidrop/src/runtime/facade.rs | 89 +++++++++++++------ crates/vnidrop/src/runtime/mod.rs | 3 - crates/vnidrop/src/runtime/targeted.rs | 10 +-- crates/vnidrop/src/secure_secret.rs | 60 ++++++++++--- crates/vnidrop/src/secure_secret/platform.rs | 44 +++++++++ .../vnidrop/src/targeted_transfer/protocol.rs | 11 +-- crates/vnidrop/src/tests/api_surface.rs | 3 +- .../vnidrop/src/tests/pairing_eligibility.rs | 7 +- .../src/tests/platform_contract_android.rs | 43 +++++++++ .../src/tests/platform_contract_apple.rs | 45 +++++++++- .../src/tests/platform_contract_linux.rs | 44 ++++++++- .../src/tests/platform_contract_windows.rs | 42 ++++++++- crates/vnidrop/src/tests/runtime.rs | 68 +++++++++++++- crates/vnidrop/src/tests/secure_secret.rs | 8 +- crates/vnidrop/tests/experimental_domain.rs | 10 +-- crates/vnidrop/tests/support/mod.rs | 33 +++++++ 23 files changed, 492 insertions(+), 103 deletions(-) diff --git a/Makefile b/Makefile index 39d160a..1cc1502 100644 --- a/Makefile +++ b/Makefile @@ -83,30 +83,30 @@ check-release: ## Validate coordinated release scripts and workflow YAML. check-rust: ## Run Rust formatting, lint, tests, and documentation checks. cd $(ROOT) && $(CARGO) fmt --all -- --check - cd $(ROOT) && $(CARGO) clippy --workspace --all-targets -- -D warnings - cd $(ROOT) && $(CARGO) test --workspace --all-targets + cd $(ROOT) && $(CARGO) clippy --workspace --all-targets --features integration-test-store -- -D warnings + cd $(ROOT) && $(CARGO) test --workspace --all-targets --features integration-test-store cd $(ROOT) && RUSTDOCFLAGS='-D warnings' $(CARGO) doc --workspace --no-deps audit-rust: ## Audit Rust dependencies (requires cargo-audit). cd $(ROOT) && $(CARGO) audit test-rust: ## Run the focused Rust core suite. - cd $(ROOT) && $(CARGO) test -p vnidrop + cd $(ROOT) && $(CARGO) test -p vnidrop --features integration-test-store test-rust-all: ## Run every Rust workspace test target. - cd $(ROOT) && $(CARGO) test --workspace --all-targets + cd $(ROOT) && $(CARGO) test --workspace --all-targets --features integration-test-store test-rust-transfer: ## Run Rust transfer integration tests. - cd $(ROOT) && $(CARGO) test -p vnidrop --test transfer + cd $(ROOT) && $(CARGO) test -p vnidrop --features integration-test-store --test transfer test-rust-approval: ## Run Rust approval integration tests. - cd $(ROOT) && $(CARGO) test -p vnidrop --test approval + cd $(ROOT) && $(CARGO) test -p vnidrop --features integration-test-store --test approval test-rust-lifecycle: ## Run Rust lifecycle integration tests. - cd $(ROOT) && $(CARGO) test -p vnidrop --test lifecycle + cd $(ROOT) && $(CARGO) test -p vnidrop --features integration-test-store --test lifecycle test-rust-output-sink: ## Run Rust output-sink integration tests. - cd $(ROOT) && $(CARGO) test -p vnidrop --test output_sink + cd $(ROOT) && $(CARGO) test -p vnidrop --features integration-test-store --test output_sink check-shared: ## Test and compile the shared Android/JVM module. cd $(ROOT) && $(GRADLE) :shared:jvmTest :shared:compileKotlinJvm $(GRADLE_FLAGS) diff --git a/crates/vnidrop/Cargo.toml b/crates/vnidrop/Cargo.toml index 37c11be..554c8f2 100644 --- a/crates/vnidrop/Cargo.toml +++ b/crates/vnidrop/Cargo.toml @@ -8,6 +8,9 @@ license = "Apache-2.0" name = "vnidrop" crate-type = ["cdylib", "staticlib", "rlib"] +[features] +integration-test-store = [] + [dependencies] anyhow = "1.0.102" async-channel = "2.5.0" diff --git a/crates/vnidrop/src/api.rs b/crates/vnidrop/src/api.rs index 4f216c6..c6eeb7e 100644 --- a/crates/vnidrop/src/api.rs +++ b/crates/vnidrop/src/api.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use anyhow::Context; use iroh::RelayUrl; use iroh_blobs::Hash; @@ -10,7 +12,24 @@ use crate::util::{non_empty, now_ms}; pub(crate) const MAX_CUSTOM_RELAYS: usize = 8; pub(crate) const MAX_RELAY_URL_BYTES: usize = 2_048; -/// Versions the additive public domain seam and its two experimental wire protocols. +/// Versions the saved-device domain seam and its wire protocols. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)] +pub struct SavedDeviceCapabilities { + pub domain_contract_version: u16, + pub relationship_protocol_version: u16, + pub targeted_transfer_protocol_version: u16, +} + +#[uniffi::export] +pub fn saved_device_capabilities() -> SavedDeviceCapabilities { + SavedDeviceCapabilities { + domain_contract_version: 1, + relationship_protocol_version: 1, + targeted_transfer_protocol_version: 3, + } +} + +#[deprecated(note = "use SavedDeviceCapabilities")] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)] pub struct ExperimentalSavedDeviceCapabilities { pub domain_contract_version: u16, @@ -18,12 +37,14 @@ pub struct ExperimentalSavedDeviceCapabilities { pub targeted_transfer_protocol_version: u16, } +#[deprecated(note = "use saved_device_capabilities")] #[uniffi::export] pub fn experimental_saved_device_capabilities() -> ExperimentalSavedDeviceCapabilities { + let capabilities = saved_device_capabilities(); ExperimentalSavedDeviceCapabilities { - domain_contract_version: 1, - relationship_protocol_version: 1, - targeted_transfer_protocol_version: 3, + domain_contract_version: capabilities.domain_contract_version, + relationship_protocol_version: capabilities.relationship_protocol_version, + targeted_transfer_protocol_version: capabilities.targeted_transfer_protocol_version, } } diff --git a/crates/vnidrop/src/device_relationship/crypto.rs b/crates/vnidrop/src/device_relationship/crypto.rs index 2980929..152a71f 100644 --- a/crates/vnidrop/src/device_relationship/crypto.rs +++ b/crates/vnidrop/src/device_relationship/crypto.rs @@ -93,7 +93,7 @@ fn relationship_mac( #[cfg(test)] mod grant_vectors { use super::*; - use crate::api::experimental_saved_device_capabilities; + use crate::api::saved_device_capabilities; use data_encoding::HEXLOWER; #[test] @@ -103,7 +103,7 @@ mod grant_vectors { .unwrap(); let grant_id = GrantId::decode("0123456789abcdef0123456789abcdef").unwrap(); let challenge = Challenge::from_bytes([9u8; 32]); - let protocol = experimental_saved_device_capabilities().relationship_protocol_version; + let protocol = saved_device_capabilities().relationship_protocol_version; let proof = prove_relationship_grant( grant_id, &secret, &challenge, "issuer", "holder", 1, protocol, ); diff --git a/crates/vnidrop/src/device_relationship/service.rs b/crates/vnidrop/src/device_relationship/service.rs index 54d84b1..900c048 100644 --- a/crates/vnidrop/src/device_relationship/service.rs +++ b/crates/vnidrop/src/device_relationship/service.rs @@ -12,8 +12,8 @@ use tokio::sync::Mutex as TokioMutex; use crate::{ api::{ - experimental_saved_device_capabilities, CoreRelayMode, DeviceRelationship, - DeviceRelationshipState, SavedDevice, + saved_device_capabilities, CoreRelayMode, DeviceRelationship, DeviceRelationshipState, + SavedDevice, }, blocked_devices::BlockStore, error::VnidropError, @@ -499,7 +499,7 @@ impl DeviceRelationshipService { Ok(capability) => capability, Err(_) => return PairingRequestResponse::Rejected, }; - let local_protocol = experimental_saved_device_capabilities().relationship_protocol_version; + let local_protocol = saved_device_capabilities().relationship_protocol_version; // Peers without a compatible saved-device protocol cannot pair; they // retain ordinary invitation flow outside this ALPN. if request.protocol_version != local_protocol { diff --git a/crates/vnidrop/src/lib.rs b/crates/vnidrop/src/lib.rs index e5f33a5..a1b37fd 100644 --- a/crates/vnidrop/src/lib.rs +++ b/crates/vnidrop/src/lib.rs @@ -14,6 +14,7 @@ mod logging; mod pairing_eligibility; mod persistence; mod runtime; +#[cfg(test)] mod secret; #[allow( dead_code, @@ -25,16 +26,17 @@ mod ticket; mod transfer_state; mod util; +#[allow(deprecated)] pub use api::{ clear_inactive_transfer_cache, default_core_limits, default_core_network_config, - experimental_saved_device_capabilities, CoreEvent, CoreEventSink, CoreLimits, - CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship, + experimental_saved_device_capabilities, saved_device_capabilities, CoreEvent, CoreEventSink, + CoreLimits, CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship, DeviceRelationshipState, ExperimentalSavedDeviceCapabilities, PairingEligibilitySummary, PendingTargetedOffer, PublishedOutput, ReceiveOutputSink, ReceiveOutputSinkV2, ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus, SavedDevice, - ShareMetadataInput, ShareResult, ShareSource, SourceKind, StoredTransfer, - TargetedOfferResponse, TargetedTransfer, TargetedTransferState, TicketInspection, - TransferAccessMode, TransferMetadata, + SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind, + StoredTransfer, TargetedOfferResponse, TargetedTransfer, TargetedTransferState, + TicketInspection, TransferAccessMode, TransferMetadata, }; pub use error::VnidropError; pub use runtime::VnidropCore; diff --git a/crates/vnidrop/src/pairing_eligibility/mod.rs b/crates/vnidrop/src/pairing_eligibility/mod.rs index 1a012d6..1d5a2f8 100644 --- a/crates/vnidrop/src/pairing_eligibility/mod.rs +++ b/crates/vnidrop/src/pairing_eligibility/mod.rs @@ -14,7 +14,7 @@ mod store; pub(crate) use store::PairingEligibilityStore; use crate::{ - api::{experimental_saved_device_capabilities, PairingEligibilitySummary}, + api::{saved_device_capabilities, PairingEligibilitySummary}, device_relationship::DeviceRelationshipStore, error::VnidropError, event_hub::EventHub, @@ -113,8 +113,7 @@ impl PairingEligibilityService { return Ok(()); } - let protocol_version = - experimental_saved_device_capabilities().relationship_protocol_version; + let protocol_version = saved_device_capabilities().relationship_protocol_version; let capability = derive_capability( approval_token, &self.local_endpoint_id, diff --git a/crates/vnidrop/src/runtime/facade.rs b/crates/vnidrop/src/runtime/facade.rs index 77c5398..fbff64d 100644 --- a/crates/vnidrop/src/runtime/facade.rs +++ b/crates/vnidrop/src/runtime/facade.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use std::{future::Future, path::PathBuf, sync::Arc}; use anyhow::Context; @@ -28,6 +30,30 @@ pub struct VnidropCore { } impl VnidropCore { + fn initialize_protected( + app_data_dir: String, + event_sink: Arc, + limits: CoreLimits, + network_config: CoreNetworkConfig, + ) -> Result, VnidropError> { + let app_data_path = PathBuf::from(app_data_dir); + std::fs::create_dir_all(&app_data_path).map_err(VnidropError::filesystem)?; + let app_data_path = + std::fs::canonicalize(app_data_path).map_err(VnidropError::filesystem)?; + let profile_lock = lock_profile(&app_data_path)?; + let store = platform_secret_store(&app_data_path)?; + Self::initialize_with_identity_mode( + app_data_path.to_string_lossy().into_owned(), + event_sink, + limits, + network_config, + IdentityMode::Protected { + store, + profile_lock, + }, + ) + } + /// Drive work on this core's multi-thread runtime from a sync API boundary. /// /// Uses [`tokio::runtime::Handle::block_on`] rather than exclusive @@ -63,11 +89,43 @@ impl VnidropCore { relay_urls, identity_mode, )) - .map_err(VnidropError::initialization)?; + .map_err(|error| match error.downcast::() { + Ok(error) => error, + Err(error) => VnidropError::initialization(error), + })?; Ok(Arc::new(Self { runtime, inner })) } } +#[cfg(all(feature = "integration-test-store", debug_assertions))] +impl VnidropCore { + /// Non-production Rust test harness entry that selects protected in-memory custody. + #[doc(hidden)] + pub fn initialize_for_integration_test( + app_data_dir: String, + event_sink: Arc, + limits: CoreLimits, + network_config: CoreNetworkConfig, + ) -> Result, VnidropError> { + let path = std::fs::canonicalize(&app_data_dir) + .or_else(|_| { + std::fs::create_dir_all(&app_data_dir)?; + std::fs::canonicalize(&app_data_dir) + }) + .map_err(VnidropError::filesystem)?; + crate::secure_secret::install_platform_secret_store_for_test( + &path, + Arc::new(crate::secure_secret::FaultInjectingSecretStore::default()), + ); + Self::initialize_with_limits_and_network_config( + path.to_string_lossy().into_owned(), + event_sink, + limits, + network_config, + ) + } +} + #[cfg(test)] impl VnidropCore { /// Test-only protected identity with an injected secret store. @@ -259,6 +317,7 @@ impl VnidropCore { } } +#[allow(deprecated)] #[uniffi::export] impl VnidropCore { #[uniffi::constructor] @@ -309,16 +368,11 @@ impl VnidropCore { limits: CoreLimits, network_config: CoreNetworkConfig, ) -> Result, VnidropError> { - Self::initialize_with_identity_mode( - app_data_dir, - event_sink, - limits, - network_config, - IdentityMode::Legacy, - ) + Self::initialize_protected(app_data_dir, event_sink, limits, network_config) } - /// Starts the experimental saved-device core with a platform-protected identity. + /// Compatibility constructor retained during the protected-initialization expand phase. + #[deprecated(note = "use initialize_with_limits_and_network_config")] #[uniffi::constructor] pub fn initialize_with_experimental_saved_devices( app_data_dir: String, @@ -326,22 +380,7 @@ impl VnidropCore { limits: CoreLimits, network_config: CoreNetworkConfig, ) -> Result, VnidropError> { - let app_data_path = PathBuf::from(app_data_dir); - std::fs::create_dir_all(&app_data_path).map_err(VnidropError::filesystem)?; - let app_data_path = - std::fs::canonicalize(app_data_path).map_err(VnidropError::filesystem)?; - let profile_lock = lock_profile(&app_data_path)?; - let store = platform_secret_store(&app_data_path)?; - Self::initialize_with_identity_mode( - app_data_path.to_string_lossy().into_owned(), - event_sink, - limits, - network_config, - IdentityMode::Protected { - store, - profile_lock, - }, - ) + Self::initialize_protected(app_data_dir, event_sink, limits, network_config) } pub fn status(&self) -> RuntimeStatus { diff --git a/crates/vnidrop/src/runtime/mod.rs b/crates/vnidrop/src/runtime/mod.rs index d5edb14..d452909 100644 --- a/crates/vnidrop/src/runtime/mod.rs +++ b/crates/vnidrop/src/runtime/mod.rs @@ -62,7 +62,6 @@ use crate::{ invitation::Repository, logging::init_logging, pairing_eligibility::PairingEligibilityService, - secret::load_or_create_secret, secure_secret::{start_endpoint_identity, ProfileLock, SecureSecretStore}, targeted_transfer::{TargetedOfferInbox, TargetedTransferProtocol}, ticket::ticket_matches_relay_profile, @@ -138,7 +137,6 @@ pub(super) struct ActiveTransfer { } pub(super) enum IdentityMode { - Legacy, Protected { store: Arc, profile_lock: ProfileLock, @@ -161,7 +159,6 @@ impl CoreInner { let targeted_transfers = stores.targeted.clone(); let blocked_devices = stores.blocked.clone(); let (secret_key, secret_custody, profile_lock) = match identity_mode { - IdentityMode::Legacy => (load_or_create_secret(&app_data_dir).await?, None, None), IdentityMode::Protected { store, profile_lock, diff --git a/crates/vnidrop/src/runtime/targeted.rs b/crates/vnidrop/src/runtime/targeted.rs index 8fd9065..1b28bf7 100644 --- a/crates/vnidrop/src/runtime/targeted.rs +++ b/crates/vnidrop/src/runtime/targeted.rs @@ -9,8 +9,8 @@ use uuid::Uuid; use super::{receive::ReceiveTarget, targeted_tag_name, CoreInner}; use crate::{ api::{ - experimental_saved_device_capabilities, PendingTargetedOffer, ShareSource, - TargetedOfferResponse, TargetedTransfer, TargetedTransferState, TransferAccessMode, + saved_device_capabilities, PendingTargetedOffer, ShareSource, TargetedOfferResponse, + TargetedTransfer, TargetedTransferState, TransferAccessMode, }, error::VnidropError, secure_secret::{SecretHandle, SecretKind}, @@ -610,8 +610,7 @@ impl CoreInner { } }; - let protocol_version = - experimental_saved_device_capabilities().targeted_transfer_protocol_version; + let protocol_version = saved_device_capabilities().targeted_transfer_protocol_version; if let Err(error) = store .set_state( &transfer_uuid, @@ -1214,8 +1213,7 @@ impl CoreInner { content_hash: row.content_hash.clone(), file_count: row.file_count, total_size: row.total_size, - protocol_version: experimental_saved_device_capabilities() - .targeted_transfer_protocol_version, + protocol_version: saved_device_capabilities().targeted_transfer_protocol_version, transfer_name: row.transfer_name.clone(), blob_ticket: blob_ticket.clone(), }, diff --git a/crates/vnidrop/src/secure_secret.rs b/crates/vnidrop/src/secure_secret.rs index 92c9101..f4ec338 100644 --- a/crates/vnidrop/src/secure_secret.rs +++ b/crates/vnidrop/src/secure_secret.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, fmt, io, path::Path, sync::Arc, time::Duration}; -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] use std::{collections::HashMap, sync::Mutex}; use data_encoding::HEXLOWER; @@ -20,6 +20,8 @@ mod platform; #[cfg(any(test, target_os = "windows"))] pub(crate) mod windows; +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] +pub(crate) use platform::install_platform_secret_store_for_test; #[cfg(test)] pub(crate) use platform::scope_store; #[cfg(test)] @@ -477,9 +479,7 @@ impl SecretCustody { .to_string(), }); } - tokio::fs::remove_file(legacy_path) - .await - .map_err(VnidropError::filesystem)?; + remove_legacy_identity_durably(legacy_path).await?; } Err(VnidropError::SecureStorageMissing { .. }) => {} Err(error) => return Err(error), @@ -496,9 +496,7 @@ impl SecretCustody { Some(endpoint_id.as_str()), ) .await?; - tokio::fs::remove_file(legacy_path) - .await - .map_err(VnidropError::filesystem)?; + remove_legacy_identity_durably(legacy_path).await?; Ok(handle) } @@ -595,6 +593,11 @@ impl SecretCustody { if validate_material(entry.kind, &material, entry.expected_identity.as_deref()) .is_err() { + if entry.kind == SecretKind::EndpointIdentity { + return Err(VnidropError::SecureStorageCorrupted { + reason: "protected endpoint identity is corrupted".to_string(), + }); + } self.metadata.disable(&entry.handle).await?; self.delete_if_present(&entry.handle).await?; summary.disabled += 1; @@ -603,7 +606,12 @@ impl SecretCustody { summary.staged_activated += 1; } } - Err(SecureSecretStoreError::Missing | SecureSecretStoreError::Corrupted) => { + Err( + error @ (SecureSecretStoreError::Missing | SecureSecretStoreError::Corrupted), + ) => { + if entry.kind == SecretKind::EndpointIdentity { + return Err(map_store_error(error)); + } self.metadata.disable(&entry.handle).await?; self.delete_if_present(&entry.handle).await?; summary.disabled += 1; @@ -700,6 +708,32 @@ impl SecretCustody { } } +async fn remove_legacy_identity_durably(path: &Path) -> Result<(), VnidropError> { + let path = path.to_path_buf(); + tokio::task::spawn_blocking(move || { + match std::fs::remove_file(&path) { + Ok(()) => {} + Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()), + Err(error) => return Err(VnidropError::filesystem(error)), + } + #[cfg(unix)] + { + let parent = path.parent().ok_or_else(|| { + VnidropError::filesystem(io::Error::new( + io::ErrorKind::InvalidInput, + "legacy identity path has no parent", + )) + })?; + std::fs::File::open(parent) + .and_then(|directory| directory.sync_all()) + .map_err(VnidropError::filesystem)?; + } + Ok(()) + }) + .await + .map_err(VnidropError::internal)? +} + async fn read_legacy_endpoint_identity(path: &Path) -> Result { let encoded = match tokio::fs::read_to_string(path).await { Ok(encoded) => encoded, @@ -725,7 +759,7 @@ pub(crate) struct ReconciliationSummary { pub(crate) disabled: u64, } -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CustodyCrashPoint { StoreWrite, @@ -762,14 +796,14 @@ fn map_store_error(error: SecureSecretStoreError) -> VnidropError { } } -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ReferenceStoreFailure { Locked, Unavailable, } -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] #[derive(Default)] pub(crate) struct FaultInjectingSecretStore { values: Mutex>, @@ -777,7 +811,7 @@ pub(crate) struct FaultInjectingSecretStore { corrupted: Mutex>, } -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] impl FaultInjectingSecretStore { pub(crate) fn fail_with(&self, failure: Option) { *self.failure.lock().unwrap() = failure; @@ -812,7 +846,7 @@ impl FaultInjectingSecretStore { } } -#[cfg(test)] +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] impl SecureSecretStore for FaultInjectingSecretStore { fn put( &self, diff --git a/crates/vnidrop/src/secure_secret/platform.rs b/crates/vnidrop/src/secure_secret/platform.rs index 9e29ebe..4dfa831 100644 --- a/crates/vnidrop/src/secure_secret/platform.rs +++ b/crates/vnidrop/src/secure_secret/platform.rs @@ -13,6 +13,34 @@ use super::{ }; use crate::error::VnidropError; +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] +static TEST_STORES: std::sync::OnceLock< + std::sync::Mutex>>, +> = std::sync::OnceLock::new(); + +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] +pub(crate) fn install_platform_secret_store_for_test( + app_data_dir: &Path, + store: Arc, +) { + TEST_STORES + .get_or_init(Default::default) + .lock() + .expect("test stores") + .entry(app_data_dir.to_path_buf()) + .or_insert_with(|| scope_store(app_data_dir, store)); +} + +#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] +fn platform_secret_store_for_test(app_data_dir: &Path) -> Option> { + TEST_STORES + .get_or_init(Default::default) + .lock() + .expect("test stores") + .get(app_data_dir) + .cloned() +} + struct ScopedSecretStore { inner: Arc, physical_prefix: String, @@ -157,6 +185,10 @@ pub(crate) fn scope_store( pub(crate) fn platform_secret_store( app_data_dir: &Path, ) -> Result, VnidropError> { + #[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] + if let Some(store) = platform_secret_store_for_test(app_data_dir) { + return Ok(store); + } Ok(scope_store( app_data_dir, Arc::new(super::apple::AppleKeychainSecretStore::new()), @@ -167,6 +199,10 @@ pub(crate) fn platform_secret_store( pub(crate) fn platform_secret_store( app_data_dir: &Path, ) -> Result, VnidropError> { + #[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] + if let Some(store) = platform_secret_store_for_test(app_data_dir) { + return Ok(store); + } super::android::native::create_store_from_android_runtime() .map(|store| scope_store(app_data_dir, store)) .map_err(map_store_error) @@ -176,6 +212,10 @@ pub(crate) fn platform_secret_store( pub(crate) fn platform_secret_store( app_data_dir: &Path, ) -> Result, VnidropError> { + #[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] + if let Some(store) = platform_secret_store_for_test(app_data_dir) { + return Ok(store); + } super::windows::WindowsDpapiSecretStore::new(app_data_dir.join("protected-secrets-v1")) .map(|store| scope_store(app_data_dir, Arc::new(store))) .map_err(map_store_error) @@ -185,6 +225,10 @@ pub(crate) fn platform_secret_store( pub(crate) fn platform_secret_store( app_data_dir: &Path, ) -> Result, VnidropError> { + #[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))] + if let Some(store) = platform_secret_store_for_test(app_data_dir) { + return Ok(store); + } super::linux::LinuxSecretServiceStore::connect() .map(|store| scope_store(app_data_dir, Arc::new(store))) .map_err(map_store_error) diff --git a/crates/vnidrop/src/targeted_transfer/protocol.rs b/crates/vnidrop/src/targeted_transfer/protocol.rs index 96d9a5e..1c9591e 100644 --- a/crates/vnidrop/src/targeted_transfer/protocol.rs +++ b/crates/vnidrop/src/targeted_transfer/protocol.rs @@ -22,10 +22,7 @@ use super::{ state_as_str, TargetedTransferRole, TargetedTransferStore, }; use crate::{ - api::{ - experimental_saved_device_capabilities, CoreRelayMode, PendingTargetedOffer, - TargetedTransferState, - }, + api::{saved_device_capabilities, CoreRelayMode, PendingTargetedOffer, TargetedTransferState}, device_relationship::{DeviceRelationshipService, WireProof}, error::VnidropError, grant::Challenge, @@ -105,7 +102,7 @@ impl TargetedTransferProtocol { challenge: &Challenge, offer: SubmitTargetedOffer, ) -> WireOfferResponse { - let expected = experimental_saved_device_capabilities().targeted_transfer_protocol_version; + let expected = saved_device_capabilities().targeted_transfer_protocol_version; if self.inbox.cooldown().is_cooling(remote_endpoint_id) { return WireOfferResponse::Refused { reason: "identity-cooldown".to_string(), @@ -275,7 +272,7 @@ impl TargetedTransferProtocol { && row.total_size == auth.total_size && row.blob_ticket.as_deref() == Some(auth.blob_ticket.as_str()) && auth.protocol_version - == experimental_saved_device_capabilities().targeted_transfer_protocol_version + == saved_device_capabilities().targeted_transfer_protocol_version { return DeliverAuthorizationResponse::Stored; } @@ -421,7 +418,7 @@ impl TargetedTransferProtocol { || auth.total_size != completion.verified_bytes || row.blob_ticket.as_deref() != Some(auth.blob_ticket.as_str()) || auth.protocol_version - != experimental_saved_device_capabilities().targeted_transfer_protocol_version + != saved_device_capabilities().targeted_transfer_protocol_version { return CompletionResponse::Rejected; } diff --git a/crates/vnidrop/src/tests/api_surface.rs b/crates/vnidrop/src/tests/api_surface.rs index ff59ba9..1867ba8 100644 --- a/crates/vnidrop/src/tests/api_surface.rs +++ b/crates/vnidrop/src/tests/api_surface.rs @@ -59,6 +59,7 @@ fn public_api_exposes_saved_device_surface_without_prototype_contact_entry_point "fn share_files(", "fn receive(", "experimental_saved_device_capabilities", + "saved_device_capabilities", ] { assert!( facade.contains(required) || api.contains(required) || lib.contains(required), @@ -66,7 +67,7 @@ fn public_api_exposes_saved_device_surface_without_prototype_contact_entry_point ); } - let caps = crate::experimental_saved_device_capabilities(); + let caps = crate::saved_device_capabilities(); assert_eq!(caps.domain_contract_version, 1); assert_eq!(caps.relationship_protocol_version, 1); assert_eq!(caps.targeted_transfer_protocol_version, 3); diff --git a/crates/vnidrop/src/tests/pairing_eligibility.rs b/crates/vnidrop/src/tests/pairing_eligibility.rs index 799ab04..f38afba 100644 --- a/crates/vnidrop/src/tests/pairing_eligibility.rs +++ b/crates/vnidrop/src/tests/pairing_eligibility.rs @@ -5,9 +5,8 @@ use std::{ }; use crate::{ - experimental_saved_device_capabilities, secure_secret::FaultInjectingSecretStore, CoreEvent, - CoreEventSink, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode, VnidropCore, - VnidropError, + saved_device_capabilities, secure_secret::FaultInjectingSecretStore, CoreEvent, CoreEventSink, + ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode, VnidropCore, VnidropError, }; struct RecordingSink { @@ -164,7 +163,7 @@ fn completed_authenticated_transfer_creates_pairing_eligibility_on_both_sides() wait_for_eligibility(&sender.core, &receiver_id); wait_for_eligibility(&receiver.core, &sender_id); - let protocol = experimental_saved_device_capabilities().relationship_protocol_version; + let protocol = saved_device_capabilities().relationship_protocol_version; let sender_entry = sender .core .list_pairing_eligibilities() diff --git a/crates/vnidrop/src/tests/platform_contract_android.rs b/crates/vnidrop/src/tests/platform_contract_android.rs index 5778fe8..271e57f 100644 --- a/crates/vnidrop/src/tests/platform_contract_android.rs +++ b/crates/vnidrop/src/tests/platform_contract_android.rs @@ -14,6 +14,8 @@ use std::{ time::{Duration, Instant}, }; +use data_encoding::HEXLOWER; +use iroh::SecretKey; use tempfile::TempDir; use crate::{ @@ -157,6 +159,36 @@ struct AndroidContractNode { } impl AndroidContractNode { + fn new_with_legacy(identity: &SecretKey) -> Self { + let no_backup = TempDir::new().unwrap(); + let data_dir = TempDir::new().unwrap(); + std::fs::write( + data_dir.path().join("iroh.secret"), + HEXLOWER.encode(&identity.to_bytes()), + ) + .unwrap(); + let keystore = Arc::new(FakeAndroidKeystore::default()); + let android_store = + AndroidSecureSecretStore::new(no_backup.path(), keystore.clone()).unwrap(); + let store = Arc::new(RelationshipGatedStore::new(Arc::new(android_store))); + let sink = Arc::new(RecordingSink { + events: Mutex::new(Vec::new()), + }); + let core = VnidropCore::initialize_with_test_secret_store( + data_dir.path().to_string_lossy().into_owned(), + sink.clone(), + store.clone(), + ) + .expect("Android legacy migration"); + Self { + _no_backup: no_backup, + data_dir, + keystore, + store, + sink, + core: Some(core), + } + } fn new() -> Self { let no_backup = TempDir::new().unwrap(); let data_dir = TempDir::new().unwrap(); @@ -230,6 +262,17 @@ impl AndroidContractNode { } } +#[test] +fn android_adapter_protects_identity_before_plaintext_removal() { + let identity = SecretKey::generate(); + let mut node = AndroidContractNode::new_with_legacy(&identity); + assert!(!node.data_dir.path().join("iroh.secret").exists()); + let endpoint = node.core().status().endpoint_id; + assert_eq!(endpoint, identity.public().to_string()); + node.restart(); + assert_eq!(node.core().status().endpoint_id, endpoint); +} + impl Drop for AndroidContractNode { fn drop(&mut self) { if let Some(core) = self.core.take() { diff --git a/crates/vnidrop/src/tests/platform_contract_apple.rs b/crates/vnidrop/src/tests/platform_contract_apple.rs index 0c97950..3f0f32c 100644 --- a/crates/vnidrop/src/tests/platform_contract_apple.rs +++ b/crates/vnidrop/src/tests/platform_contract_apple.rs @@ -21,6 +21,8 @@ use crate::{ ShareMetadataInput, ShareSource, SourceKind, TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError, }; +use data_encoding::HEXLOWER; +use iroh::SecretKey; const ERR_SEC_INTERACTION_NOT_ALLOWED: i32 = -25_308; const ERR_SEC_ITEM_NOT_FOUND: i32 = -25_300; @@ -47,7 +49,7 @@ impl RecordingSink { /// Node backed by the Apple Keychain adapter (injectable API for headless cargo). /// -/// Production `initialize_with_experimental_saved_devices` uses the same +/// Production standard constructors use the same /// `AppleKeychainSecretStore` + profile scoping. CLI unit tests lack the app /// Keychain entitlement, so the system Keychain returns Unavailable; the /// injectable API exercises the identical adapter path. Swift XCTest covers @@ -118,6 +120,34 @@ impl AppleKeychainApi for RecordingKeychain { } impl KeychainNode { + fn new_with_legacy(identity: &SecretKey) -> Self { + let data_dir = tempfile::tempdir().unwrap(); + std::fs::write( + data_dir.path().join("iroh.secret"), + HEXLOWER.encode(&identity.to_bytes()), + ) + .unwrap(); + let sink = Arc::new(RecordingSink { + events: Mutex::new(Vec::new()), + }); + let api = RecordingKeychain::default(); + let store = crate::secure_secret::scope_store( + data_dir.path(), + Arc::new(AppleKeychainSecretStore::with_api(api.clone())), + ); + let core = VnidropCore::initialize_with_test_secret_store( + data_dir.path().to_string_lossy().into_owned(), + sink.clone(), + store, + ) + .expect("Apple legacy migration"); + Self { + data_dir, + api, + sink, + core: Some(core), + } + } fn new() -> Self { let data_dir = tempfile::tempdir().unwrap(); let sink = Arc::new(RecordingSink { @@ -179,7 +209,7 @@ impl Drop for KeychainNode { } fn try_experimental_keychain_init(app_data_dir: &Path) -> Result, VnidropError> { - VnidropCore::initialize_with_experimental_saved_devices( + VnidropCore::initialize_with_limits_and_network_config( app_data_dir.to_string_lossy().into_owned(), Arc::new(RecordingSink { events: Mutex::new(Vec::new()), @@ -264,6 +294,17 @@ impl FaultNode { } } +#[test] +fn apple_adapter_protects_identity_before_plaintext_removal() { + let identity = SecretKey::generate(); + let node = KeychainNode::new_with_legacy(&identity); + assert!(!node.data_dir.path().join("iroh.secret").exists()); + let endpoint = node.core().status().endpoint_id; + assert_eq!(endpoint, identity.public().to_string()); + let node = node.restart(); + assert_eq!(node.core().status().endpoint_id, endpoint); +} + impl Drop for FaultNode { fn drop(&mut self) { if let Some(core) = self.core.take() { diff --git a/crates/vnidrop/src/tests/platform_contract_linux.rs b/crates/vnidrop/src/tests/platform_contract_linux.rs index 56ebdf8..ed9c719 100644 --- a/crates/vnidrop/src/tests/platform_contract_linux.rs +++ b/crates/vnidrop/src/tests/platform_contract_linux.rs @@ -21,6 +21,8 @@ use crate::{ CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind, TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError, }; +use data_encoding::HEXLOWER; +use iroh::SecretKey; #[cfg(target_os = "linux")] use crate::{CoreLimits, CoreNetworkConfig}; @@ -54,6 +56,31 @@ struct SecretServiceNode { } impl SecretServiceNode { + fn new_with_legacy(identity: &SecretKey) -> Self { + let data_dir = tempfile::tempdir().unwrap(); + std::fs::write( + data_dir.path().join("iroh.secret"), + HEXLOWER.encode(&identity.to_bytes()), + ) + .unwrap(); + let sink = Arc::new(RecordingSink { + events: Mutex::new(Vec::new()), + }); + let api = Arc::new(ControllableSecretService::default()); + let store = Arc::new(LinuxSecretServiceStore::with_api(api.clone())); + let core = VnidropCore::initialize_with_test_secret_store( + data_dir.path().to_string_lossy().into_owned(), + sink.clone(), + store, + ) + .expect("Linux legacy migration"); + Self { + data_dir, + api, + sink, + core: Some(core), + } + } fn new() -> Self { let data_dir = tempfile::tempdir().unwrap(); let sink = Arc::new(RecordingSink { @@ -207,6 +234,17 @@ impl FaultNode { } } +#[test] +fn linux_adapter_protects_identity_before_plaintext_removal() { + let identity = SecretKey::generate(); + let node = SecretServiceNode::new_with_legacy(&identity); + assert!(!node.data_dir.path().join("iroh.secret").exists()); + let endpoint = node.core().status().endpoint_id; + assert_eq!(endpoint, identity.public().to_string()); + let node = node.restart(); + assert_eq!(node.core().status().endpoint_id, endpoint); +} + impl Drop for FaultNode { fn drop(&mut self) { if let Some(core) = self.core.take() { @@ -516,7 +554,7 @@ fn experimental_secret_service_identity_survives_core_restart_on_linux() { let sink = Arc::new(RecordingSink { events: Mutex::new(Vec::new()), }); - let core = VnidropCore::initialize_with_experimental_saved_devices( + let core = VnidropCore::initialize_with_limits_and_network_config( data_dir.path().to_string_lossy().into_owned(), sink, CoreLimits::default(), @@ -535,7 +573,7 @@ fn experimental_secret_service_identity_survives_core_restart_on_linux() { }); let started = Instant::now(); let restarted = loop { - match VnidropCore::initialize_with_experimental_saved_devices( + match VnidropCore::initialize_with_limits_and_network_config( path.clone(), sink.clone(), CoreLimits::default(), @@ -803,7 +841,7 @@ fn linux_public_bindings_omit_raw_secrets_and_generic_mutation() { } } assert!( - public_facade.contains("initialize_with_experimental_saved_devices"), + public_facade.contains("initialize_with_limits_and_network_config"), "facade must expose experimental saved-device init" ); assert!( diff --git a/crates/vnidrop/src/tests/platform_contract_windows.rs b/crates/vnidrop/src/tests/platform_contract_windows.rs index fb2be96..d2a9b1a 100644 --- a/crates/vnidrop/src/tests/platform_contract_windows.rs +++ b/crates/vnidrop/src/tests/platform_contract_windows.rs @@ -20,6 +20,8 @@ use crate::{ CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind, TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError, }; +use data_encoding::HEXLOWER; +use iroh::SecretKey; #[cfg(target_os = "windows")] use crate::{CoreLimits, CoreNetworkConfig}; @@ -48,6 +50,31 @@ struct WindowsContractNode { } impl WindowsContractNode { + fn new_with_legacy(identity: &SecretKey) -> Self { + let data_dir = tempfile::tempdir().unwrap(); + std::fs::write( + data_dir.path().join("iroh.secret"), + HEXLOWER.encode(&identity.to_bytes()), + ) + .unwrap(); + let api = Arc::new(FakeWindowsDpapiApi::new()); + let sink = Arc::new(RecordingSink { + events: Mutex::new(Vec::new()), + }); + let store = windows_scoped_store(data_dir.path(), api.clone()); + let core = VnidropCore::initialize_with_test_secret_store( + data_dir.path().to_string_lossy().into_owned(), + sink.clone(), + store, + ) + .expect("Windows legacy migration"); + Self { + data_dir, + api, + sink, + core: Some(core), + } + } fn new() -> Self { let data_dir = tempfile::tempdir().unwrap(); let api = Arc::new(FakeWindowsDpapiApi::new()); @@ -93,6 +120,17 @@ impl WindowsContractNode { } } +#[test] +fn windows_adapter_protects_identity_before_plaintext_removal() { + let identity = SecretKey::generate(); + let mut node = WindowsContractNode::new_with_legacy(&identity); + assert!(!node.data_dir.path().join("iroh.secret").exists()); + let endpoint = node.core().status().endpoint_id; + assert_eq!(endpoint, identity.public().to_string()); + let restarted = node.restart(); + assert_eq!(restarted.status().endpoint_id, endpoint); +} + impl Drop for WindowsContractNode { fn drop(&mut self) { if let Some(core) = self.core.take() { @@ -283,7 +321,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() { let sink = Arc::new(RecordingSink { events: Mutex::new(Vec::new()), }); - let core = VnidropCore::initialize_with_experimental_saved_devices( + let core = VnidropCore::initialize_with_limits_and_network_config( path.clone(), sink, CoreLimits::default(), @@ -297,7 +335,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() { let sink = Arc::new(RecordingSink { events: Mutex::new(Vec::new()), }); - let restarted = VnidropCore::initialize_with_experimental_saved_devices( + let restarted = VnidropCore::initialize_with_limits_and_network_config( path, sink, CoreLimits::default(), diff --git a/crates/vnidrop/src/tests/runtime.rs b/crates/vnidrop/src/tests/runtime.rs index 8bd6d09..5600c95 100644 --- a/crates/vnidrop/src/tests/runtime.rs +++ b/crates/vnidrop/src/tests/runtime.rs @@ -1,5 +1,7 @@ use std::{sync::Arc, time::Duration}; +use data_encoding::HEXLOWER; +use iroh::SecretKey; use iroh_blobs::{ provider::{ events::{RequestUpdate, TransferCompleted}, @@ -11,7 +13,9 @@ use iroh_blobs::{ use crate::{ invitation::{PendingDeliveryReceiptInsert, Repository, TransferUpsert}, runtime::{consume_request_updates, CoreInner, IdentityMode, RequestStreamOutcome}, - secure_secret::{lock_profile, FaultInjectingSecretStore}, + secure_secret::{ + install_platform_secret_store_for_test, lock_profile, FaultInjectingSecretStore, + }, transfer_state::{TransferDirection, TransferStatus}, CoreEvent, CoreEventSink, CoreLimits, CoreRelayMode, VnidropCore, VnidropError, }; @@ -22,6 +26,14 @@ impl CoreEventSink for TestSink { fn on_event(&self, _event: CoreEvent) {} } +fn install_protected_test_store(path: &std::path::Path) { + let canonical = std::fs::canonicalize(path).unwrap(); + install_platform_secret_store_for_test( + &canonical, + Arc::new(FaultInjectingSecretStore::default()), + ); +} + #[test] fn provider_request_stream_distinguishes_success_from_silent_abort() { let runtime = tokio::runtime::Runtime::new().unwrap(); @@ -56,6 +68,7 @@ fn provider_request_stream_distinguishes_success_from_silent_abort() { #[test] fn initializes_and_reports_endpoint() { let temp = tempfile::tempdir().unwrap(); + install_protected_test_store(temp.path()); let core = VnidropCore::initialize( temp.path().to_string_lossy().to_string(), Arc::new(TestSink), @@ -66,6 +79,55 @@ fn initializes_and_reports_endpoint() { core.shutdown(); } +#[test] +fn all_standard_constructors_migrate_and_restart_one_protected_identity() { + let temp = tempfile::tempdir().unwrap(); + let canonical = std::fs::canonicalize(temp.path()).unwrap(); + let store = Arc::new(FaultInjectingSecretStore::default()); + install_platform_secret_store_for_test(&canonical, store); + let original = SecretKey::generate(); + let legacy = temp.path().join("iroh.secret"); + std::fs::write(&legacy, HEXLOWER.encode(&original.to_bytes())).unwrap(); + let path = temp.path().to_string_lossy().into_owned(); + let network = || crate::CoreNetworkConfig { + mode: CoreRelayMode::LocalOnly, + relay_urls: Vec::new(), + }; + + type Constructor = Box Result, VnidropError>>; + let constructors: Vec = vec![ + Box::new({ + let path = path.clone(); + move || VnidropCore::initialize(path, Arc::new(TestSink)) + }), + Box::new({ + let path = path.clone(); + move || { + VnidropCore::initialize_with_limits(path, Arc::new(TestSink), CoreLimits::default()) + } + }), + Box::new({ + let path = path.clone(); + move || VnidropCore::initialize_with_network_config(path, Arc::new(TestSink), network()) + }), + Box::new(move || { + VnidropCore::initialize_with_limits_and_network_config( + path.clone(), + Arc::new(TestSink), + CoreLimits::default(), + network(), + ) + }), + ]; + for constructor in constructors { + let core = constructor().unwrap(); + assert_eq!(core.status().endpoint_id, original.public().to_string()); + assert!(!legacy.exists()); + core.shutdown(); + drop(core); + } +} + #[tokio::test] async fn protected_runtime_restart_preserves_identity_without_plaintext_fallback() { let temp = tempfile::tempdir().unwrap(); @@ -109,6 +171,7 @@ async fn protected_runtime_restart_preserves_identity_without_plaintext_fallback #[test] fn invalid_receive_ticket_is_typed_and_persisted_as_event() { let temp = tempfile::tempdir().unwrap(); + install_protected_test_store(temp.path()); let core = VnidropCore::initialize( temp.path().to_string_lossy().to_string(), Arc::new(TestSink), @@ -134,6 +197,7 @@ fn invalid_receive_ticket_is_typed_and_persisted_as_event() { #[test] fn startup_recovers_interrupted_transfer_and_persists_event() { let temp = tempfile::tempdir().unwrap(); + install_protected_test_store(temp.path()); let preparation_runtime = tokio::runtime::Runtime::new().unwrap(); preparation_runtime.block_on(async { let repository = Repository::open(temp.path()).await.unwrap(); @@ -181,6 +245,7 @@ fn startup_recovers_interrupted_transfer_and_persists_event() { #[test] fn startup_processes_persisted_delivery_receipts() { let temp = tempfile::tempdir().unwrap(); + install_protected_test_store(temp.path()); let preparation_runtime = tokio::runtime::Runtime::new().unwrap(); preparation_runtime.block_on(async { let repository = Repository::open(temp.path()).await.unwrap(); @@ -240,6 +305,7 @@ fn startup_processes_persisted_delivery_receipts() { #[test] fn startup_fails_persisted_share_when_root_blob_is_missing() { let temp = tempfile::tempdir().unwrap(); + install_protected_test_store(temp.path()); let preparation_runtime = tokio::runtime::Runtime::new().unwrap(); preparation_runtime.block_on(async { let repository = Repository::open(temp.path()).await.unwrap(); diff --git a/crates/vnidrop/src/tests/secure_secret.rs b/crates/vnidrop/src/tests/secure_secret.rs index e15422b..2c0dfaf 100644 --- a/crates/vnidrop/src/tests/secure_secret.rs +++ b/crates/vnidrop/src/tests/secure_secret.rs @@ -332,13 +332,9 @@ async fn first_install_identity_is_protected_once_and_never_silently_replaced() store.remove_for_test(&handle); drop(custody); let stores = persistence::open_all(temp.path()).await.unwrap(); - let (custody, summary) = SecretCustody::start(stores.secrets.clone(), store.clone()) - .await - .unwrap(); - assert_eq!(summary.disabled, 1); assert!(matches!( - custody.initialize_endpoint_identity(&legacy_path).await, - Err(VnidropError::SecureStorageUnavailable { .. }) + SecretCustody::start(stores.secrets.clone(), store.clone()).await, + Err(VnidropError::SecureStorageMissing { .. }) )); assert!(store.list_handles().unwrap().is_empty()); } diff --git a/crates/vnidrop/tests/experimental_domain.rs b/crates/vnidrop/tests/experimental_domain.rs index 56d7b81..058cfff 100644 --- a/crates/vnidrop/tests/experimental_domain.rs +++ b/crates/vnidrop/tests/experimental_domain.rs @@ -2,16 +2,16 @@ mod support; use support::TestNode; use vnidrop::{ - experimental_saved_device_capabilities, DeviceRelationship, DeviceRelationshipState, - ExperimentalSavedDeviceCapabilities, SavedDevice, ShareMetadataInput, ShareSource, SourceKind, - TargetedTransfer, TargetedTransferState, TransferAccessMode, VnidropError, + saved_device_capabilities, DeviceRelationship, DeviceRelationshipState, SavedDevice, + SavedDeviceCapabilities, ShareMetadataInput, ShareSource, SourceKind, TargetedTransfer, + TargetedTransferState, TransferAccessMode, VnidropError, }; #[test] fn saved_device_protocols_are_explicitly_experimental_and_versioned() { assert_eq!( - experimental_saved_device_capabilities(), - ExperimentalSavedDeviceCapabilities { + saved_device_capabilities(), + SavedDeviceCapabilities { domain_contract_version: 1, relationship_protocol_version: 1, targeted_transfer_protocol_version: 3, diff --git a/crates/vnidrop/tests/support/mod.rs b/crates/vnidrop/tests/support/mod.rs index f1acfa3..03b19f6 100644 --- a/crates/vnidrop/tests/support/mod.rs +++ b/crates/vnidrop/tests/support/mod.rs @@ -41,6 +41,17 @@ pub struct CoreGuard(Arc); impl CoreGuard { pub fn start(path: &Path, sink: Arc) -> Self { + #[cfg(feature = "integration-test-store")] + return Self( + VnidropCore::initialize_for_integration_test( + path.to_string_lossy().to_string(), + sink, + CoreLimits::default(), + CoreNetworkConfig::default(), + ) + .expect("test core should initialize"), + ); + #[cfg(not(feature = "integration-test-store"))] Self( VnidropCore::initialize(path.to_string_lossy().to_string(), sink) .expect("test core should initialize"), @@ -52,6 +63,17 @@ impl CoreGuard { sink: Arc, limits: CoreLimits, ) -> Self { + #[cfg(feature = "integration-test-store")] + return Self( + VnidropCore::initialize_for_integration_test( + path.to_string_lossy().to_string(), + sink, + limits, + CoreNetworkConfig::default(), + ) + .expect("test core should initialize with limits"), + ); + #[cfg(not(feature = "integration-test-store"))] Self( VnidropCore::initialize_with_limits(path.to_string_lossy().to_string(), sink, limits) .expect("test core should initialize with limits"), @@ -63,6 +85,17 @@ impl CoreGuard { sink: Arc, network_config: CoreNetworkConfig, ) -> Self { + #[cfg(feature = "integration-test-store")] + return Self( + VnidropCore::initialize_for_integration_test( + path.to_string_lossy().to_string(), + sink, + CoreLimits::default(), + network_config, + ) + .expect("test core should initialize with network config"), + ); + #[cfg(not(feature = "integration-test-store"))] Self( VnidropCore::initialize_with_network_config( path.to_string_lossy().to_string(),