diff --git a/crates/onionwire-sdk/Cargo.lock b/crates/onionwire-sdk/Cargo.lock index b132956..0422aac 100644 --- a/crates/onionwire-sdk/Cargo.lock +++ b/crates/onionwire-sdk/Cargo.lock @@ -2507,6 +2507,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.8.3" @@ -2750,18 +2760,21 @@ dependencies = [ [[package]] name = "onionwire" -version = "0.2.0" +version = "0.2.1" dependencies = [ "argon2", "arti-client", "chacha20poly1305", "ed25519-dalek", "futures", + "md-5", "rand 0.8.8", "ratatui", + "rpassword", "rusqlite", "safelog", "serde_json", + "sha3 0.10.9", "snow", "tokio", "tor-cell", @@ -2776,6 +2789,7 @@ version = "0.1.0" dependencies = [ "arti-client", "onionwire", + "rustls", "thiserror 2.0.20", "tokio", "uniffi", @@ -3472,6 +3486,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rpassword" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.61.2", +] + [[package]] name = "rsa" version = "0.9.10" @@ -3493,6 +3518,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtoolbox" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a1efe12a1469752d0e6ff5ebec0b6ef4924cc5c4c71046b0ec730040535819d" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "rusqlite" version = "0.36.0" @@ -3553,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 27c9cb8..e3847bd 100644 --- a/crates/onionwire-sdk/Cargo.toml +++ b/crates/onionwire-sdk/Cargo.toml @@ -32,6 +32,17 @@ arti-client = { version = "0.46", default-features = false, features = [ "static-sqlite", ] } 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 +# 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(); +} diff --git a/crates/onionwire-sdk/tests/provider_resolution.rs b/crates/onionwire-sdk/tests/provider_resolution.rs new file mode 100644 index 0000000..0b490cb --- /dev/null +++ b/crates/onionwire-sdk/tests/provider_resolution.rs @@ -0,0 +1,45 @@ +//! Regression test for the on-device failure: the APK built, installed and +//! opened, but pressing **Open** on the unlock screen died one call deep inside +//! Arti's rustls backend with +//! +//! ```text +//! Could not automatically determine the process-level CryptoProvider from +//! Rustls crate features. +//! Call CryptoProvider::install_default() before this point to select a +//! provider manually, or make sure exactly one of the 'aws-lc-rs' and 'ring' +//! features is enabled. +//! ``` +//! +//! rustls 0.23 chooses its provider from its own `ring` / `aws-lc-rs` **crate +//! features**, not from which crypto crates happen to be linked. Arti reaches +//! rustls through `tor-rtcompat` with `default-features = false`, so neither +//! provider feature is on and rustls is compiled with *no* provider at all: +//! `ring` showing up in `cargo tree` (pulled in by `snow`, for Noise) proves +//! nothing. Everything compiles, the APK ships, and the process-default lookup +//! fails at runtime. +//! +//! This test is the invariant Arti relies on: the process default must be +//! resolvable **without** anyone calling `install_default()` first. +//! +//! Deliberately the only test in this file — a second test that installs a +//! provider would race with it inside the same test binary and could mask the +//! regression. + +/// Building a `rustls` config the way Arti does, straight from the process +/// default, must not panic. +#[test] +fn rustls_default_provider_resolves_without_manual_install() { + assert!( + rustls::crypto::CryptoProvider::get_default().is_none(), + "this test must start with no provider installed" + ); + + // Pre-fix this panics with the device's exact message. + let _ = rustls::ClientConfig::builder(); + + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "rustls resolved no CryptoProvider — the crate feature that selects a \ + provider is not enabled in this workspace" + ); +}