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.
|
check-rust: ## Run Rust formatting, lint, tests, and documentation checks.
|
||||||
cd $(ROOT) && $(CARGO) fmt --all -- --check
|
cd $(ROOT) && $(CARGO) fmt --all -- --check
|
||||||
cd $(ROOT) && $(CARGO) clippy --workspace --all-targets -- -D warnings
|
cd $(ROOT) && $(CARGO) clippy --workspace --all-targets --features integration-test-store -- -D warnings
|
||||||
cd $(ROOT) && $(CARGO) test --workspace --all-targets
|
cd $(ROOT) && $(CARGO) test --workspace --all-targets --features integration-test-store
|
||||||
cd $(ROOT) && RUSTDOCFLAGS='-D warnings' $(CARGO) doc --workspace --no-deps
|
cd $(ROOT) && RUSTDOCFLAGS='-D warnings' $(CARGO) doc --workspace --no-deps
|
||||||
|
|
||||||
audit-rust: ## Audit Rust dependencies (requires cargo-audit).
|
audit-rust: ## Audit Rust dependencies (requires cargo-audit).
|
||||||
cd $(ROOT) && $(CARGO) audit
|
cd $(ROOT) && $(CARGO) audit
|
||||||
|
|
||||||
test-rust: ## Run the focused Rust core suite.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
check-shared: ## Test and compile the shared Android/JVM module.
|
||||||
cd $(ROOT) && $(GRADLE) :shared:jvmTest :shared:compileKotlinJvm $(GRADLE_FLAGS)
|
cd $(ROOT) && $(GRADLE) :shared:jvmTest :shared:compileKotlinJvm $(GRADLE_FLAGS)
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ license = "Apache-2.0"
|
|||||||
name = "vnidrop"
|
name = "vnidrop"
|
||||||
crate-type = ["cdylib", "staticlib", "rlib"]
|
crate-type = ["cdylib", "staticlib", "rlib"]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
integration-test-store = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
async-channel = "2.5.0"
|
async-channel = "2.5.0"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use iroh::RelayUrl;
|
use iroh::RelayUrl;
|
||||||
use iroh_blobs::Hash;
|
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_CUSTOM_RELAYS: usize = 8;
|
||||||
pub(crate) const MAX_RELAY_URL_BYTES: usize = 2_048;
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)]
|
||||||
pub struct ExperimentalSavedDeviceCapabilities {
|
pub struct ExperimentalSavedDeviceCapabilities {
|
||||||
pub domain_contract_version: u16,
|
pub domain_contract_version: u16,
|
||||||
@@ -18,12 +37,14 @@ pub struct ExperimentalSavedDeviceCapabilities {
|
|||||||
pub targeted_transfer_protocol_version: u16,
|
pub targeted_transfer_protocol_version: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated(note = "use saved_device_capabilities")]
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
pub fn experimental_saved_device_capabilities() -> ExperimentalSavedDeviceCapabilities {
|
pub fn experimental_saved_device_capabilities() -> ExperimentalSavedDeviceCapabilities {
|
||||||
|
let capabilities = saved_device_capabilities();
|
||||||
ExperimentalSavedDeviceCapabilities {
|
ExperimentalSavedDeviceCapabilities {
|
||||||
domain_contract_version: 1,
|
domain_contract_version: capabilities.domain_contract_version,
|
||||||
relationship_protocol_version: 1,
|
relationship_protocol_version: capabilities.relationship_protocol_version,
|
||||||
targeted_transfer_protocol_version: 3,
|
targeted_transfer_protocol_version: capabilities.targeted_transfer_protocol_version,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fn relationship_mac(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod grant_vectors {
|
mod grant_vectors {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::api::experimental_saved_device_capabilities;
|
use crate::api::saved_device_capabilities;
|
||||||
use data_encoding::HEXLOWER;
|
use data_encoding::HEXLOWER;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -103,7 +103,7 @@ mod grant_vectors {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let grant_id = GrantId::decode("0123456789abcdef0123456789abcdef").unwrap();
|
let grant_id = GrantId::decode("0123456789abcdef0123456789abcdef").unwrap();
|
||||||
let challenge = Challenge::from_bytes([9u8; 32]);
|
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(
|
let proof = prove_relationship_grant(
|
||||||
grant_id, &secret, &challenge, "issuer", "holder", 1, protocol,
|
grant_id, &secret, &challenge, "issuer", "holder", 1, protocol,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use tokio::sync::Mutex as TokioMutex;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
experimental_saved_device_capabilities, CoreRelayMode, DeviceRelationship,
|
saved_device_capabilities, CoreRelayMode, DeviceRelationship, DeviceRelationshipState,
|
||||||
DeviceRelationshipState, SavedDevice,
|
SavedDevice,
|
||||||
},
|
},
|
||||||
blocked_devices::BlockStore,
|
blocked_devices::BlockStore,
|
||||||
error::VnidropError,
|
error::VnidropError,
|
||||||
@@ -499,7 +499,7 @@ impl DeviceRelationshipService {
|
|||||||
Ok(capability) => capability,
|
Ok(capability) => capability,
|
||||||
Err(_) => return PairingRequestResponse::Rejected,
|
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
|
// Peers without a compatible saved-device protocol cannot pair; they
|
||||||
// retain ordinary invitation flow outside this ALPN.
|
// retain ordinary invitation flow outside this ALPN.
|
||||||
if request.protocol_version != local_protocol {
|
if request.protocol_version != local_protocol {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ mod logging;
|
|||||||
mod pairing_eligibility;
|
mod pairing_eligibility;
|
||||||
mod persistence;
|
mod persistence;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
#[cfg(test)]
|
||||||
mod secret;
|
mod secret;
|
||||||
#[allow(
|
#[allow(
|
||||||
dead_code,
|
dead_code,
|
||||||
@@ -25,16 +26,17 @@ mod ticket;
|
|||||||
mod transfer_state;
|
mod transfer_state;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
pub use api::{
|
pub use api::{
|
||||||
clear_inactive_transfer_cache, default_core_limits, default_core_network_config,
|
clear_inactive_transfer_cache, default_core_limits, default_core_network_config,
|
||||||
experimental_saved_device_capabilities, CoreEvent, CoreEventSink, CoreLimits,
|
experimental_saved_device_capabilities, saved_device_capabilities, CoreEvent, CoreEventSink,
|
||||||
CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship,
|
CoreLimits, CoreNetworkConfig, CoreRelayMode, CoreStorageUsage, DeviceRelationship,
|
||||||
DeviceRelationshipState, ExperimentalSavedDeviceCapabilities, PairingEligibilitySummary,
|
DeviceRelationshipState, ExperimentalSavedDeviceCapabilities, PairingEligibilitySummary,
|
||||||
PendingTargetedOffer, PublishedOutput, ReceiveOutputSink, ReceiveOutputSinkV2,
|
PendingTargetedOffer, PublishedOutput, ReceiveOutputSink, ReceiveOutputSinkV2,
|
||||||
ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus, SavedDevice,
|
ReceivedArtifact, ReceivedLocatorKind, ReceiverRequest, RuntimeStatus, SavedDevice,
|
||||||
ShareMetadataInput, ShareResult, ShareSource, SourceKind, StoredTransfer,
|
SavedDeviceCapabilities, ShareMetadataInput, ShareResult, ShareSource, SourceKind,
|
||||||
TargetedOfferResponse, TargetedTransfer, TargetedTransferState, TicketInspection,
|
StoredTransfer, TargetedOfferResponse, TargetedTransfer, TargetedTransferState,
|
||||||
TransferAccessMode, TransferMetadata,
|
TicketInspection, TransferAccessMode, TransferMetadata,
|
||||||
};
|
};
|
||||||
pub use error::VnidropError;
|
pub use error::VnidropError;
|
||||||
pub use runtime::VnidropCore;
|
pub use runtime::VnidropCore;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ mod store;
|
|||||||
pub(crate) use store::PairingEligibilityStore;
|
pub(crate) use store::PairingEligibilityStore;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{experimental_saved_device_capabilities, PairingEligibilitySummary},
|
api::{saved_device_capabilities, PairingEligibilitySummary},
|
||||||
device_relationship::DeviceRelationshipStore,
|
device_relationship::DeviceRelationshipStore,
|
||||||
error::VnidropError,
|
error::VnidropError,
|
||||||
event_hub::EventHub,
|
event_hub::EventHub,
|
||||||
@@ -113,8 +113,7 @@ impl PairingEligibilityService {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let protocol_version =
|
let protocol_version = saved_device_capabilities().relationship_protocol_version;
|
||||||
experimental_saved_device_capabilities().relationship_protocol_version;
|
|
||||||
let capability = derive_capability(
|
let capability = derive_capability(
|
||||||
approval_token,
|
approval_token,
|
||||||
&self.local_endpoint_id,
|
&self.local_endpoint_id,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
use std::{future::Future, path::PathBuf, sync::Arc};
|
use std::{future::Future, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
@@ -28,6 +30,30 @@ pub struct VnidropCore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Drive work on this core's multi-thread runtime from a sync API boundary.
|
||||||
///
|
///
|
||||||
/// Uses [`tokio::runtime::Handle::block_on`] rather than exclusive
|
/// Uses [`tokio::runtime::Handle::block_on`] rather than exclusive
|
||||||
@@ -63,11 +89,43 @@ impl VnidropCore {
|
|||||||
relay_urls,
|
relay_urls,
|
||||||
identity_mode,
|
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 }))
|
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)]
|
#[cfg(test)]
|
||||||
impl VnidropCore {
|
impl VnidropCore {
|
||||||
/// Test-only protected identity with an injected secret store.
|
/// Test-only protected identity with an injected secret store.
|
||||||
@@ -259,6 +317,7 @@ impl VnidropCore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
#[uniffi::export]
|
#[uniffi::export]
|
||||||
impl VnidropCore {
|
impl VnidropCore {
|
||||||
#[uniffi::constructor]
|
#[uniffi::constructor]
|
||||||
@@ -309,16 +368,11 @@ impl VnidropCore {
|
|||||||
limits: CoreLimits,
|
limits: CoreLimits,
|
||||||
network_config: CoreNetworkConfig,
|
network_config: CoreNetworkConfig,
|
||||||
) -> Result<Arc<Self>, VnidropError> {
|
) -> Result<Arc<Self>, VnidropError> {
|
||||||
Self::initialize_with_identity_mode(
|
Self::initialize_protected(app_data_dir, event_sink, limits, network_config)
|
||||||
app_data_dir,
|
|
||||||
event_sink,
|
|
||||||
limits,
|
|
||||||
network_config,
|
|
||||||
IdentityMode::Legacy,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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]
|
#[uniffi::constructor]
|
||||||
pub fn initialize_with_experimental_saved_devices(
|
pub fn initialize_with_experimental_saved_devices(
|
||||||
app_data_dir: String,
|
app_data_dir: String,
|
||||||
@@ -326,22 +380,7 @@ impl VnidropCore {
|
|||||||
limits: CoreLimits,
|
limits: CoreLimits,
|
||||||
network_config: CoreNetworkConfig,
|
network_config: CoreNetworkConfig,
|
||||||
) -> Result<Arc<Self>, VnidropError> {
|
) -> Result<Arc<Self>, VnidropError> {
|
||||||
let app_data_path = PathBuf::from(app_data_dir);
|
Self::initialize_protected(app_data_dir, event_sink, limits, network_config)
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> RuntimeStatus {
|
pub fn status(&self) -> RuntimeStatus {
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ use crate::{
|
|||||||
invitation::Repository,
|
invitation::Repository,
|
||||||
logging::init_logging,
|
logging::init_logging,
|
||||||
pairing_eligibility::PairingEligibilityService,
|
pairing_eligibility::PairingEligibilityService,
|
||||||
secret::load_or_create_secret,
|
|
||||||
secure_secret::{start_endpoint_identity, ProfileLock, SecureSecretStore},
|
secure_secret::{start_endpoint_identity, ProfileLock, SecureSecretStore},
|
||||||
targeted_transfer::{TargetedOfferInbox, TargetedTransferProtocol},
|
targeted_transfer::{TargetedOfferInbox, TargetedTransferProtocol},
|
||||||
ticket::ticket_matches_relay_profile,
|
ticket::ticket_matches_relay_profile,
|
||||||
@@ -138,7 +137,6 @@ pub(super) struct ActiveTransfer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) enum IdentityMode {
|
pub(super) enum IdentityMode {
|
||||||
Legacy,
|
|
||||||
Protected {
|
Protected {
|
||||||
store: Arc<dyn SecureSecretStore>,
|
store: Arc<dyn SecureSecretStore>,
|
||||||
profile_lock: ProfileLock,
|
profile_lock: ProfileLock,
|
||||||
@@ -161,7 +159,6 @@ impl CoreInner {
|
|||||||
let targeted_transfers = stores.targeted.clone();
|
let targeted_transfers = stores.targeted.clone();
|
||||||
let blocked_devices = stores.blocked.clone();
|
let blocked_devices = stores.blocked.clone();
|
||||||
let (secret_key, secret_custody, profile_lock) = match identity_mode {
|
let (secret_key, secret_custody, profile_lock) = match identity_mode {
|
||||||
IdentityMode::Legacy => (load_or_create_secret(&app_data_dir).await?, None, None),
|
|
||||||
IdentityMode::Protected {
|
IdentityMode::Protected {
|
||||||
store,
|
store,
|
||||||
profile_lock,
|
profile_lock,
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use uuid::Uuid;
|
|||||||
use super::{receive::ReceiveTarget, targeted_tag_name, CoreInner};
|
use super::{receive::ReceiveTarget, targeted_tag_name, CoreInner};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{
|
||||||
experimental_saved_device_capabilities, PendingTargetedOffer, ShareSource,
|
saved_device_capabilities, PendingTargetedOffer, ShareSource, TargetedOfferResponse,
|
||||||
TargetedOfferResponse, TargetedTransfer, TargetedTransferState, TransferAccessMode,
|
TargetedTransfer, TargetedTransferState, TransferAccessMode,
|
||||||
},
|
},
|
||||||
error::VnidropError,
|
error::VnidropError,
|
||||||
secure_secret::{SecretHandle, SecretKind},
|
secure_secret::{SecretHandle, SecretKind},
|
||||||
@@ -610,8 +610,7 @@ impl CoreInner {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let protocol_version =
|
let protocol_version = saved_device_capabilities().targeted_transfer_protocol_version;
|
||||||
experimental_saved_device_capabilities().targeted_transfer_protocol_version;
|
|
||||||
if let Err(error) = store
|
if let Err(error) = store
|
||||||
.set_state(
|
.set_state(
|
||||||
&transfer_uuid,
|
&transfer_uuid,
|
||||||
@@ -1214,8 +1213,7 @@ impl CoreInner {
|
|||||||
content_hash: row.content_hash.clone(),
|
content_hash: row.content_hash.clone(),
|
||||||
file_count: row.file_count,
|
file_count: row.file_count,
|
||||||
total_size: row.total_size,
|
total_size: row.total_size,
|
||||||
protocol_version: experimental_saved_device_capabilities()
|
protocol_version: saved_device_capabilities().targeted_transfer_protocol_version,
|
||||||
.targeted_transfer_protocol_version,
|
|
||||||
transfer_name: row.transfer_name.clone(),
|
transfer_name: row.transfer_name.clone(),
|
||||||
blob_ticket: blob_ticket.clone(),
|
blob_ticket: blob_ticket.clone(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::{collections::HashSet, fmt, io, path::Path, sync::Arc, time::Duration};
|
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 std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
use data_encoding::HEXLOWER;
|
use data_encoding::HEXLOWER;
|
||||||
@@ -20,6 +20,8 @@ mod platform;
|
|||||||
#[cfg(any(test, target_os = "windows"))]
|
#[cfg(any(test, target_os = "windows"))]
|
||||||
pub(crate) mod 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)]
|
#[cfg(test)]
|
||||||
pub(crate) use platform::scope_store;
|
pub(crate) use platform::scope_store;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -477,9 +479,7 @@ impl SecretCustody {
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
tokio::fs::remove_file(legacy_path)
|
remove_legacy_identity_durably(legacy_path).await?;
|
||||||
.await
|
|
||||||
.map_err(VnidropError::filesystem)?;
|
|
||||||
}
|
}
|
||||||
Err(VnidropError::SecureStorageMissing { .. }) => {}
|
Err(VnidropError::SecureStorageMissing { .. }) => {}
|
||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
@@ -496,9 +496,7 @@ impl SecretCustody {
|
|||||||
Some(endpoint_id.as_str()),
|
Some(endpoint_id.as_str()),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
tokio::fs::remove_file(legacy_path)
|
remove_legacy_identity_durably(legacy_path).await?;
|
||||||
.await
|
|
||||||
.map_err(VnidropError::filesystem)?;
|
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,6 +593,11 @@ impl SecretCustody {
|
|||||||
if validate_material(entry.kind, &material, entry.expected_identity.as_deref())
|
if validate_material(entry.kind, &material, entry.expected_identity.as_deref())
|
||||||
.is_err()
|
.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.metadata.disable(&entry.handle).await?;
|
||||||
self.delete_if_present(&entry.handle).await?;
|
self.delete_if_present(&entry.handle).await?;
|
||||||
summary.disabled += 1;
|
summary.disabled += 1;
|
||||||
@@ -603,7 +606,12 @@ impl SecretCustody {
|
|||||||
summary.staged_activated += 1;
|
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.metadata.disable(&entry.handle).await?;
|
||||||
self.delete_if_present(&entry.handle).await?;
|
self.delete_if_present(&entry.handle).await?;
|
||||||
summary.disabled += 1;
|
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> {
|
async fn read_legacy_endpoint_identity(path: &Path) -> Result<SecretMaterial, VnidropError> {
|
||||||
let encoded = match tokio::fs::read_to_string(path).await {
|
let encoded = match tokio::fs::read_to_string(path).await {
|
||||||
Ok(encoded) => encoded,
|
Ok(encoded) => encoded,
|
||||||
@@ -725,7 +759,7 @@ pub(crate) struct ReconciliationSummary {
|
|||||||
pub(crate) disabled: u64,
|
pub(crate) disabled: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum CustodyCrashPoint {
|
pub(crate) enum CustodyCrashPoint {
|
||||||
StoreWrite,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum ReferenceStoreFailure {
|
pub(crate) enum ReferenceStoreFailure {
|
||||||
Locked,
|
Locked,
|
||||||
Unavailable,
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))]
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct FaultInjectingSecretStore {
|
pub(crate) struct FaultInjectingSecretStore {
|
||||||
values: Mutex<HashMap<SecretHandle, SecretMaterial>>,
|
values: Mutex<HashMap<SecretHandle, SecretMaterial>>,
|
||||||
@@ -777,7 +811,7 @@ pub(crate) struct FaultInjectingSecretStore {
|
|||||||
corrupted: Mutex<Vec<SecretHandle>>,
|
corrupted: Mutex<Vec<SecretHandle>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(any(test, all(feature = "integration-test-store", debug_assertions)))]
|
||||||
impl FaultInjectingSecretStore {
|
impl FaultInjectingSecretStore {
|
||||||
pub(crate) fn fail_with(&self, failure: Option<ReferenceStoreFailure>) {
|
pub(crate) fn fail_with(&self, failure: Option<ReferenceStoreFailure>) {
|
||||||
*self.failure.lock().unwrap() = failure;
|
*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 {
|
impl SecureSecretStore for FaultInjectingSecretStore {
|
||||||
fn put(
|
fn put(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -13,6 +13,34 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::error::VnidropError;
|
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 {
|
struct ScopedSecretStore {
|
||||||
inner: Arc<dyn SecureSecretStore>,
|
inner: Arc<dyn SecureSecretStore>,
|
||||||
physical_prefix: String,
|
physical_prefix: String,
|
||||||
@@ -157,6 +185,10 @@ pub(crate) fn scope_store(
|
|||||||
pub(crate) fn platform_secret_store(
|
pub(crate) fn platform_secret_store(
|
||||||
app_data_dir: &Path,
|
app_data_dir: &Path,
|
||||||
) -> Result<Arc<dyn SecureSecretStore>, VnidropError> {
|
) -> 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(
|
Ok(scope_store(
|
||||||
app_data_dir,
|
app_data_dir,
|
||||||
Arc::new(super::apple::AppleKeychainSecretStore::new()),
|
Arc::new(super::apple::AppleKeychainSecretStore::new()),
|
||||||
@@ -167,6 +199,10 @@ pub(crate) fn platform_secret_store(
|
|||||||
pub(crate) fn platform_secret_store(
|
pub(crate) fn platform_secret_store(
|
||||||
app_data_dir: &Path,
|
app_data_dir: &Path,
|
||||||
) -> Result<Arc<dyn SecureSecretStore>, VnidropError> {
|
) -> 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()
|
super::android::native::create_store_from_android_runtime()
|
||||||
.map(|store| scope_store(app_data_dir, store))
|
.map(|store| scope_store(app_data_dir, store))
|
||||||
.map_err(map_store_error)
|
.map_err(map_store_error)
|
||||||
@@ -176,6 +212,10 @@ pub(crate) fn platform_secret_store(
|
|||||||
pub(crate) fn platform_secret_store(
|
pub(crate) fn platform_secret_store(
|
||||||
app_data_dir: &Path,
|
app_data_dir: &Path,
|
||||||
) -> Result<Arc<dyn SecureSecretStore>, VnidropError> {
|
) -> 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"))
|
super::windows::WindowsDpapiSecretStore::new(app_data_dir.join("protected-secrets-v1"))
|
||||||
.map(|store| scope_store(app_data_dir, Arc::new(store)))
|
.map(|store| scope_store(app_data_dir, Arc::new(store)))
|
||||||
.map_err(map_store_error)
|
.map_err(map_store_error)
|
||||||
@@ -185,6 +225,10 @@ pub(crate) fn platform_secret_store(
|
|||||||
pub(crate) fn platform_secret_store(
|
pub(crate) fn platform_secret_store(
|
||||||
app_data_dir: &Path,
|
app_data_dir: &Path,
|
||||||
) -> Result<Arc<dyn SecureSecretStore>, VnidropError> {
|
) -> 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()
|
super::linux::LinuxSecretServiceStore::connect()
|
||||||
.map(|store| scope_store(app_data_dir, Arc::new(store)))
|
.map(|store| scope_store(app_data_dir, Arc::new(store)))
|
||||||
.map_err(map_store_error)
|
.map_err(map_store_error)
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ use super::{
|
|||||||
state_as_str, TargetedTransferRole, TargetedTransferStore,
|
state_as_str, TargetedTransferRole, TargetedTransferStore,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
api::{saved_device_capabilities, CoreRelayMode, PendingTargetedOffer, TargetedTransferState},
|
||||||
experimental_saved_device_capabilities, CoreRelayMode, PendingTargetedOffer,
|
|
||||||
TargetedTransferState,
|
|
||||||
},
|
|
||||||
device_relationship::{DeviceRelationshipService, WireProof},
|
device_relationship::{DeviceRelationshipService, WireProof},
|
||||||
error::VnidropError,
|
error::VnidropError,
|
||||||
grant::Challenge,
|
grant::Challenge,
|
||||||
@@ -105,7 +102,7 @@ impl TargetedTransferProtocol {
|
|||||||
challenge: &Challenge,
|
challenge: &Challenge,
|
||||||
offer: SubmitTargetedOffer,
|
offer: SubmitTargetedOffer,
|
||||||
) -> WireOfferResponse {
|
) -> 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) {
|
if self.inbox.cooldown().is_cooling(remote_endpoint_id) {
|
||||||
return WireOfferResponse::Refused {
|
return WireOfferResponse::Refused {
|
||||||
reason: "identity-cooldown".to_string(),
|
reason: "identity-cooldown".to_string(),
|
||||||
@@ -275,7 +272,7 @@ impl TargetedTransferProtocol {
|
|||||||
&& row.total_size == auth.total_size
|
&& row.total_size == auth.total_size
|
||||||
&& row.blob_ticket.as_deref() == Some(auth.blob_ticket.as_str())
|
&& row.blob_ticket.as_deref() == Some(auth.blob_ticket.as_str())
|
||||||
&& auth.protocol_version
|
&& auth.protocol_version
|
||||||
== experimental_saved_device_capabilities().targeted_transfer_protocol_version
|
== saved_device_capabilities().targeted_transfer_protocol_version
|
||||||
{
|
{
|
||||||
return DeliverAuthorizationResponse::Stored;
|
return DeliverAuthorizationResponse::Stored;
|
||||||
}
|
}
|
||||||
@@ -421,7 +418,7 @@ impl TargetedTransferProtocol {
|
|||||||
|| auth.total_size != completion.verified_bytes
|
|| auth.total_size != completion.verified_bytes
|
||||||
|| row.blob_ticket.as_deref() != Some(auth.blob_ticket.as_str())
|
|| row.blob_ticket.as_deref() != Some(auth.blob_ticket.as_str())
|
||||||
|| auth.protocol_version
|
|| auth.protocol_version
|
||||||
!= experimental_saved_device_capabilities().targeted_transfer_protocol_version
|
!= saved_device_capabilities().targeted_transfer_protocol_version
|
||||||
{
|
{
|
||||||
return CompletionResponse::Rejected;
|
return CompletionResponse::Rejected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ fn public_api_exposes_saved_device_surface_without_prototype_contact_entry_point
|
|||||||
"fn share_files(",
|
"fn share_files(",
|
||||||
"fn receive(",
|
"fn receive(",
|
||||||
"experimental_saved_device_capabilities",
|
"experimental_saved_device_capabilities",
|
||||||
|
"saved_device_capabilities",
|
||||||
] {
|
] {
|
||||||
assert!(
|
assert!(
|
||||||
facade.contains(required) || api.contains(required) || lib.contains(required),
|
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.domain_contract_version, 1);
|
||||||
assert_eq!(caps.relationship_protocol_version, 1);
|
assert_eq!(caps.relationship_protocol_version, 1);
|
||||||
assert_eq!(caps.targeted_transfer_protocol_version, 3);
|
assert_eq!(caps.targeted_transfer_protocol_version, 3);
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
experimental_saved_device_capabilities, secure_secret::FaultInjectingSecretStore, CoreEvent,
|
saved_device_capabilities, secure_secret::FaultInjectingSecretStore, CoreEvent, CoreEventSink,
|
||||||
CoreEventSink, ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode, VnidropCore,
|
ShareMetadataInput, ShareSource, SourceKind, TransferAccessMode, VnidropCore, VnidropError,
|
||||||
VnidropError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RecordingSink {
|
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(&sender.core, &receiver_id);
|
||||||
wait_for_eligibility(&receiver.core, &sender_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
|
let sender_entry = sender
|
||||||
.core
|
.core
|
||||||
.list_pairing_eligibilities()
|
.list_pairing_eligibilities()
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ use std::{
|
|||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use iroh::SecretKey;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -157,6 +159,36 @@ struct AndroidContractNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
fn new() -> Self {
|
||||||
let no_backup = TempDir::new().unwrap();
|
let no_backup = TempDir::new().unwrap();
|
||||||
let data_dir = 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 {
|
impl Drop for AndroidContractNode {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(core) = self.core.take() {
|
if let Some(core) = self.core.take() {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ use crate::{
|
|||||||
ShareMetadataInput, ShareSource, SourceKind, TargetedTransferState, TransferAccessMode,
|
ShareMetadataInput, ShareSource, SourceKind, TargetedTransferState, TransferAccessMode,
|
||||||
VnidropCore, VnidropError,
|
VnidropCore, VnidropError,
|
||||||
};
|
};
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use iroh::SecretKey;
|
||||||
|
|
||||||
const ERR_SEC_INTERACTION_NOT_ALLOWED: i32 = -25_308;
|
const ERR_SEC_INTERACTION_NOT_ALLOWED: i32 = -25_308;
|
||||||
const ERR_SEC_ITEM_NOT_FOUND: i32 = -25_300;
|
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).
|
/// 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
|
/// `AppleKeychainSecretStore` + profile scoping. CLI unit tests lack the app
|
||||||
/// Keychain entitlement, so the system Keychain returns Unavailable; the
|
/// Keychain entitlement, so the system Keychain returns Unavailable; the
|
||||||
/// injectable API exercises the identical adapter path. Swift XCTest covers
|
/// injectable API exercises the identical adapter path. Swift XCTest covers
|
||||||
@@ -118,6 +120,34 @@ impl AppleKeychainApi for RecordingKeychain {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl KeychainNode {
|
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 {
|
fn new() -> Self {
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
let sink = Arc::new(RecordingSink {
|
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> {
|
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(),
|
app_data_dir.to_string_lossy().into_owned(),
|
||||||
Arc::new(RecordingSink {
|
Arc::new(RecordingSink {
|
||||||
events: Mutex::new(Vec::new()),
|
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 {
|
impl Drop for FaultNode {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(core) = self.core.take() {
|
if let Some(core) = self.core.take() {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ use crate::{
|
|||||||
CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind,
|
CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind,
|
||||||
TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError,
|
TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError,
|
||||||
};
|
};
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use iroh::SecretKey;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use crate::{CoreLimits, CoreNetworkConfig};
|
use crate::{CoreLimits, CoreNetworkConfig};
|
||||||
@@ -54,6 +56,31 @@ struct SecretServiceNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
fn new() -> Self {
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
let sink = Arc::new(RecordingSink {
|
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 {
|
impl Drop for FaultNode {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(core) = self.core.take() {
|
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 {
|
let sink = Arc::new(RecordingSink {
|
||||||
events: Mutex::new(Vec::new()),
|
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(),
|
data_dir.path().to_string_lossy().into_owned(),
|
||||||
sink,
|
sink,
|
||||||
CoreLimits::default(),
|
CoreLimits::default(),
|
||||||
@@ -535,7 +573,7 @@ fn experimental_secret_service_identity_survives_core_restart_on_linux() {
|
|||||||
});
|
});
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let restarted = loop {
|
let restarted = loop {
|
||||||
match VnidropCore::initialize_with_experimental_saved_devices(
|
match VnidropCore::initialize_with_limits_and_network_config(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
sink.clone(),
|
sink.clone(),
|
||||||
CoreLimits::default(),
|
CoreLimits::default(),
|
||||||
@@ -803,7 +841,7 @@ fn linux_public_bindings_omit_raw_secrets_and_generic_mutation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert!(
|
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"
|
"facade must expose experimental saved-device init"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ use crate::{
|
|||||||
CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind,
|
CoreEvent, CoreEventSink, DeviceRelationshipState, ShareMetadataInput, ShareSource, SourceKind,
|
||||||
TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError,
|
TargetedTransferState, TransferAccessMode, VnidropCore, VnidropError,
|
||||||
};
|
};
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use iroh::SecretKey;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use crate::{CoreLimits, CoreNetworkConfig};
|
use crate::{CoreLimits, CoreNetworkConfig};
|
||||||
@@ -48,6 +50,31 @@ struct WindowsContractNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
fn new() -> Self {
|
||||||
let data_dir = tempfile::tempdir().unwrap();
|
let data_dir = tempfile::tempdir().unwrap();
|
||||||
let api = Arc::new(FakeWindowsDpapiApi::new());
|
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 {
|
impl Drop for WindowsContractNode {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(core) = self.core.take() {
|
if let Some(core) = self.core.take() {
|
||||||
@@ -283,7 +321,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
|
|||||||
let sink = Arc::new(RecordingSink {
|
let sink = Arc::new(RecordingSink {
|
||||||
events: Mutex::new(Vec::new()),
|
events: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
let core = VnidropCore::initialize_with_experimental_saved_devices(
|
let core = VnidropCore::initialize_with_limits_and_network_config(
|
||||||
path.clone(),
|
path.clone(),
|
||||||
sink,
|
sink,
|
||||||
CoreLimits::default(),
|
CoreLimits::default(),
|
||||||
@@ -297,7 +335,7 @@ fn real_windows_dpapi_experimental_init_preserves_identity() {
|
|||||||
let sink = Arc::new(RecordingSink {
|
let sink = Arc::new(RecordingSink {
|
||||||
events: Mutex::new(Vec::new()),
|
events: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
let restarted = VnidropCore::initialize_with_experimental_saved_devices(
|
let restarted = VnidropCore::initialize_with_limits_and_network_config(
|
||||||
path,
|
path,
|
||||||
sink,
|
sink,
|
||||||
CoreLimits::default(),
|
CoreLimits::default(),
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use data_encoding::HEXLOWER;
|
||||||
|
use iroh::SecretKey;
|
||||||
use iroh_blobs::{
|
use iroh_blobs::{
|
||||||
provider::{
|
provider::{
|
||||||
events::{RequestUpdate, TransferCompleted},
|
events::{RequestUpdate, TransferCompleted},
|
||||||
@@ -11,7 +13,9 @@ use iroh_blobs::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
invitation::{PendingDeliveryReceiptInsert, Repository, TransferUpsert},
|
invitation::{PendingDeliveryReceiptInsert, Repository, TransferUpsert},
|
||||||
runtime::{consume_request_updates, CoreInner, IdentityMode, RequestStreamOutcome},
|
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},
|
transfer_state::{TransferDirection, TransferStatus},
|
||||||
CoreEvent, CoreEventSink, CoreLimits, CoreRelayMode, VnidropCore, VnidropError,
|
CoreEvent, CoreEventSink, CoreLimits, CoreRelayMode, VnidropCore, VnidropError,
|
||||||
};
|
};
|
||||||
@@ -22,6 +26,14 @@ impl CoreEventSink for TestSink {
|
|||||||
fn on_event(&self, _event: CoreEvent) {}
|
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]
|
#[test]
|
||||||
fn provider_request_stream_distinguishes_success_from_silent_abort() {
|
fn provider_request_stream_distinguishes_success_from_silent_abort() {
|
||||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
@@ -56,6 +68,7 @@ fn provider_request_stream_distinguishes_success_from_silent_abort() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn initializes_and_reports_endpoint() {
|
fn initializes_and_reports_endpoint() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
install_protected_test_store(temp.path());
|
||||||
let core = VnidropCore::initialize(
|
let core = VnidropCore::initialize(
|
||||||
temp.path().to_string_lossy().to_string(),
|
temp.path().to_string_lossy().to_string(),
|
||||||
Arc::new(TestSink),
|
Arc::new(TestSink),
|
||||||
@@ -66,6 +79,55 @@ fn initializes_and_reports_endpoint() {
|
|||||||
core.shutdown();
|
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]
|
#[tokio::test]
|
||||||
async fn protected_runtime_restart_preserves_identity_without_plaintext_fallback() {
|
async fn protected_runtime_restart_preserves_identity_without_plaintext_fallback() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
@@ -109,6 +171,7 @@ async fn protected_runtime_restart_preserves_identity_without_plaintext_fallback
|
|||||||
#[test]
|
#[test]
|
||||||
fn invalid_receive_ticket_is_typed_and_persisted_as_event() {
|
fn invalid_receive_ticket_is_typed_and_persisted_as_event() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
install_protected_test_store(temp.path());
|
||||||
let core = VnidropCore::initialize(
|
let core = VnidropCore::initialize(
|
||||||
temp.path().to_string_lossy().to_string(),
|
temp.path().to_string_lossy().to_string(),
|
||||||
Arc::new(TestSink),
|
Arc::new(TestSink),
|
||||||
@@ -134,6 +197,7 @@ fn invalid_receive_ticket_is_typed_and_persisted_as_event() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn startup_recovers_interrupted_transfer_and_persists_event() {
|
fn startup_recovers_interrupted_transfer_and_persists_event() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
install_protected_test_store(temp.path());
|
||||||
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
preparation_runtime.block_on(async {
|
preparation_runtime.block_on(async {
|
||||||
let repository = Repository::open(temp.path()).await.unwrap();
|
let repository = Repository::open(temp.path()).await.unwrap();
|
||||||
@@ -181,6 +245,7 @@ fn startup_recovers_interrupted_transfer_and_persists_event() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn startup_processes_persisted_delivery_receipts() {
|
fn startup_processes_persisted_delivery_receipts() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
install_protected_test_store(temp.path());
|
||||||
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
preparation_runtime.block_on(async {
|
preparation_runtime.block_on(async {
|
||||||
let repository = Repository::open(temp.path()).await.unwrap();
|
let repository = Repository::open(temp.path()).await.unwrap();
|
||||||
@@ -240,6 +305,7 @@ fn startup_processes_persisted_delivery_receipts() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn startup_fails_persisted_share_when_root_blob_is_missing() {
|
fn startup_fails_persisted_share_when_root_blob_is_missing() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
install_protected_test_store(temp.path());
|
||||||
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
let preparation_runtime = tokio::runtime::Runtime::new().unwrap();
|
||||||
preparation_runtime.block_on(async {
|
preparation_runtime.block_on(async {
|
||||||
let repository = Repository::open(temp.path()).await.unwrap();
|
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);
|
store.remove_for_test(&handle);
|
||||||
drop(custody);
|
drop(custody);
|
||||||
let stores = persistence::open_all(temp.path()).await.unwrap();
|
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!(
|
assert!(matches!(
|
||||||
custody.initialize_endpoint_identity(&legacy_path).await,
|
SecretCustody::start(stores.secrets.clone(), store.clone()).await,
|
||||||
Err(VnidropError::SecureStorageUnavailable { .. })
|
Err(VnidropError::SecureStorageMissing { .. })
|
||||||
));
|
));
|
||||||
assert!(store.list_handles().unwrap().is_empty());
|
assert!(store.list_handles().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ mod support;
|
|||||||
|
|
||||||
use support::TestNode;
|
use support::TestNode;
|
||||||
use vnidrop::{
|
use vnidrop::{
|
||||||
experimental_saved_device_capabilities, DeviceRelationship, DeviceRelationshipState,
|
saved_device_capabilities, DeviceRelationship, DeviceRelationshipState, SavedDevice,
|
||||||
ExperimentalSavedDeviceCapabilities, SavedDevice, ShareMetadataInput, ShareSource, SourceKind,
|
SavedDeviceCapabilities, ShareMetadataInput, ShareSource, SourceKind, TargetedTransfer,
|
||||||
TargetedTransfer, TargetedTransferState, TransferAccessMode, VnidropError,
|
TargetedTransferState, TransferAccessMode, VnidropError,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn saved_device_protocols_are_explicitly_experimental_and_versioned() {
|
fn saved_device_protocols_are_explicitly_experimental_and_versioned() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
experimental_saved_device_capabilities(),
|
saved_device_capabilities(),
|
||||||
ExperimentalSavedDeviceCapabilities {
|
SavedDeviceCapabilities {
|
||||||
domain_contract_version: 1,
|
domain_contract_version: 1,
|
||||||
relationship_protocol_version: 1,
|
relationship_protocol_version: 1,
|
||||||
targeted_transfer_protocol_version: 3,
|
targeted_transfer_protocol_version: 3,
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ pub struct CoreGuard(Arc<VnidropCore>);
|
|||||||
|
|
||||||
impl CoreGuard {
|
impl CoreGuard {
|
||||||
pub fn start(path: &Path, sink: Arc<dyn CoreEventSink>) -> Self {
|
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(
|
Self(
|
||||||
VnidropCore::initialize(path.to_string_lossy().to_string(), sink)
|
VnidropCore::initialize(path.to_string_lossy().to_string(), sink)
|
||||||
.expect("test core should initialize"),
|
.expect("test core should initialize"),
|
||||||
@@ -52,6 +63,17 @@ impl CoreGuard {
|
|||||||
sink: Arc<dyn CoreEventSink>,
|
sink: Arc<dyn CoreEventSink>,
|
||||||
limits: CoreLimits,
|
limits: CoreLimits,
|
||||||
) -> Self {
|
) -> 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(
|
Self(
|
||||||
VnidropCore::initialize_with_limits(path.to_string_lossy().to_string(), sink, limits)
|
VnidropCore::initialize_with_limits(path.to_string_lossy().to_string(), sink, limits)
|
||||||
.expect("test core should initialize with limits"),
|
.expect("test core should initialize with limits"),
|
||||||
@@ -63,6 +85,17 @@ impl CoreGuard {
|
|||||||
sink: Arc<dyn CoreEventSink>,
|
sink: Arc<dyn CoreEventSink>,
|
||||||
network_config: CoreNetworkConfig,
|
network_config: CoreNetworkConfig,
|
||||||
) -> Self {
|
) -> 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(
|
Self(
|
||||||
VnidropCore::initialize_with_network_config(
|
VnidropCore::initialize_with_network_config(
|
||||||
path.to_string_lossy().to_string(),
|
path.to_string_lossy().to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user