diff --git a/crates/onionwire-sdk/Cargo.lock b/crates/onionwire-sdk/Cargo.lock index 6835896..0422aac 100644 --- a/crates/onionwire-sdk/Cargo.lock +++ b/crates/onionwire-sdk/Cargo.lock @@ -3588,6 +3588,7 @@ checksum = "6725596c3f2c3a0aef021139e145d4eafe314a6623e4680ca83852b2c67ab2ba" dependencies = [ "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", diff --git a/crates/onionwire-sdk/Cargo.toml b/crates/onionwire-sdk/Cargo.toml index c4bb52e..e3847bd 100644 --- a/crates/onionwire-sdk/Cargo.toml +++ b/crates/onionwire-sdk/Cargo.toml @@ -35,11 +35,14 @@ tokio = { version = "1", features = ["rt-multi-thread", "time"] } # rustls 0.23 resolves its provider from its OWN `ring` / `aws-lc-rs` features, # never from whichever crypto crates happen to be linked in the graph. Arti # pulls rustls in through `tor-rtcompat` with `default-features = false`, so -# unless this crate turns a provider feature on, rustls compiles with zero -# providers and the first TLS config built from the process default fails at -# runtime. (Currently: no provider feature — see the regression test in -# `tests/provider_resolution.rs`.) -rustls = { version = "0.23", default-features = false } +# without this line rustls compiles with zero providers and the first TLS config +# built from the process default fails at runtime — the "Failed to start / +# Could not automatically determine the process-level CryptoProvider" screen. +# +# `ring`, not `aws-lc-rs`: aws-lc-rs needs CMake and a C toolchain for the NDK +# and fights the Android cross-compile. The `ring` crate was already in the +# graph (via `snow`, for Noise) but that is a different thing entirely. +rustls = { version = "0.23", default-features = false, features = ["ring"] } uniffi = { version = "0.32", features = ["cli", "tokio"] } thiserror = "2" diff --git a/crates/onionwire-sdk/src/lib.rs b/crates/onionwire-sdk/src/lib.rs index cd08457..e90c7fa 100644 --- a/crates/onionwire-sdk/src/lib.rs +++ b/crates/onionwire-sdk/src/lib.rs @@ -17,13 +17,38 @@ //! no TUI/ratatui type leaks into the AAR. use std::path::PathBuf; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use onionwire::node::Node; use onionwire::qr; uniffi::setup_scaffolding!(); +/// Install rustls's process-level default `CryptoProvider`, exactly once. +/// +/// Belt and braces. `Cargo.toml` pins `rustls` with the `ring` feature, which +/// is what actually fixes the missing-provider failure: with it, rustls +/// resolves a provider from crate features on its own. This function exists +/// because the cdylib is loaded into a process we do not own — an explicitly +/// installed provider makes the SDK independent of how the surrounding app's +/// feature graph happens to resolve rustls. +/// +/// Idempotent and cheap after the first call (`OnceLock` short-circuits it). +/// An `Err` means some provider is already installed process-wide, which is the +/// outcome we want, so it is deliberately ignored — including the case of a +/// consumer that installed `aws-lc-rs` itself. A genuine *conflict* — both +/// provider features compiled in — is a build-graph bug, not something to +/// paper over here; `Cargo.toml` enables `ring` only. +/// +/// Not exported over UniFFI: it is Rust-side plumbing, not part of the Kotlin +/// surface. +pub fn install_crypto_provider() { + static INSTALLED: OnceLock<()> = OnceLock::new(); + INSTALLED.get_or_init(|| { + let _ = rustls::crypto::ring::default_provider().install_default(); + }); +} + #[derive(Debug, thiserror::Error, uniffi::Error)] #[uniffi(flat_error)] pub enum WireError { @@ -88,6 +113,10 @@ pub struct Wire { /// that is minutes, not seconds. Call it off the main thread. #[uniffi::export(async_runtime = "tokio")] pub async fn open_wire(home: String, passphrase: String) -> WResult> { + // Before anything that can build a TLS config: Arti's rustls backend dies + // with "Could not automatically determine the process-level CryptoProvider" + // if no provider is resolvable. See `install_crypto_provider`. + install_crypto_provider(); let node = Node::start_with_passphrase(PathBuf::from(home), &passphrase) .await .map_err(WireError::new)?; diff --git a/crates/onionwire-sdk/tests/init_provider.rs b/crates/onionwire-sdk/tests/init_provider.rs new file mode 100644 index 0000000..85d8a79 --- /dev/null +++ b/crates/onionwire-sdk/tests/init_provider.rs @@ -0,0 +1,30 @@ +//! `open_wire` must not depend on how the consumer's feature graph resolves a +//! rustls provider — it installs the process default itself, first. +//! +//! This is the belt-and-braces half of the fix for the on-device "Failed to +//! start / Could not automatically determine the process-level CryptoProvider" +//! screen; the primary fix is the `rustls` `ring` feature in `Cargo.toml`, +//! covered by `tests/provider_resolution.rs`. Kept in its own test binary on +//! purpose: installing a provider here would mask that test if they shared a +//! process. + +#[test] +fn sdk_installs_the_process_default_provider() { + assert!( + rustls::crypto::CryptoProvider::get_default().is_none(), + "this test must start with no provider installed" + ); + + onionwire_sdk::install_crypto_provider(); + + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "install_crypto_provider() left the process without a default provider" + ); + + // Second call: idempotent, not a panic and not an error. + onionwire_sdk::install_crypto_provider(); + + // The call Arti makes that blew up on device. + let _ = rustls::ClientConfig::builder(); +}