mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-13 05:49:57 +02:00
feat(core): promote protected saved device APIs
This commit is contained in:
16
Makefile
16
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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<dyn CoreEventSink>,
|
||||
limits: CoreLimits,
|
||||
network_config: CoreNetworkConfig,
|
||||
) -> Result<Arc<Self>, 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::<VnidropError>() {
|
||||
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<dyn CoreEventSink>,
|
||||
limits: CoreLimits,
|
||||
network_config: CoreNetworkConfig,
|
||||
) -> Result<Arc<Self>, 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<Arc<Self>, 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<Arc<Self>, 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 {
|
||||
|
||||
@@ -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<dyn SecureSecretStore>,
|
||||
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,
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
|
||||
@@ -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<SecretMaterial, VnidropError> {
|
||||
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<HashMap<SecretHandle, SecretMaterial>>,
|
||||
@@ -777,7 +811,7 @@ pub(crate) struct FaultInjectingSecretStore {
|
||||
corrupted: Mutex<Vec<SecretHandle>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))]
|
||||
impl FaultInjectingSecretStore {
|
||||
pub(crate) fn fail_with(&self, failure: Option<ReferenceStoreFailure>) {
|
||||
*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,
|
||||
|
||||
@@ -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::collections::HashMap<std::path::PathBuf, Arc<dyn SecureSecretStore>>>,
|
||||
> = 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<dyn SecureSecretStore>,
|
||||
) {
|
||||
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<Arc<dyn SecureSecretStore>> {
|
||||
TEST_STORES
|
||||
.get_or_init(Default::default)
|
||||
.lock()
|
||||
.expect("test stores")
|
||||
.get(app_data_dir)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
struct ScopedSecretStore {
|
||||
inner: Arc<dyn SecureSecretStore>,
|
||||
physical_prefix: String,
|
||||
@@ -157,6 +185,10 @@ pub(crate) fn scope_store(
|
||||
pub(crate) fn platform_secret_store(
|
||||
app_data_dir: &Path,
|
||||
) -> Result<Arc<dyn SecureSecretStore>, 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<Arc<dyn SecureSecretStore>, 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<Arc<dyn SecureSecretStore>, 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<Arc<dyn SecureSecretStore>, 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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<Arc<VnidropCore>, 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() {
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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<dyn FnOnce() -> Result<Arc<VnidropCore>, VnidropError>>;
|
||||
let constructors: Vec<Constructor> = 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();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -41,6 +41,17 @@ pub struct CoreGuard(Arc<VnidropCore>);
|
||||
|
||||
impl CoreGuard {
|
||||
pub fn start(path: &Path, sink: Arc<dyn CoreEventSink>) -> 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<dyn CoreEventSink>,
|
||||
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<dyn CoreEventSink>,
|
||||
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(),
|
||||
|
||||
Reference in New Issue
Block a user