diff --git a/Cargo.lock b/Cargo.lock index a30129a9..0cb5f477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2090,6 +2090,33 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half 2.7.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -8125,10 +8152,13 @@ dependencies = [ "bon", "chacha20poly1305", "chrono", + "ciborium", + "clap", "color-eyre", "dashmap", "derive_more 2.1.0", "dogstatsd", + "escargot", "flume", "futures", "hex 0.4.3", @@ -8138,6 +8168,7 @@ dependencies = [ "num-derive", "num-traits", "orb-backend-status-dbus", + "orb-build-info", "orb-connd-dbus", "orb-info", "orb-secure-storage-ca", @@ -8160,6 +8191,8 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "uuid 1.19.0", + "uzers", "zbus", ] @@ -8519,7 +8552,7 @@ dependencies = [ [[package]] name = "orb-secure-storage-proto" version = "0.0.0" -source = "git+https://github.com/worldcoin/orb-rustzone.git?branch=main#d5e8fbd6a21313d046193b458ea2f1948b466d5f" +source = "git+https://github.com/worldcoin/orb-rustzone.git?branch=main#5dd3be3ff1d1981d4b4823b0d72ce9928be0f0ec" dependencies = [ "num_enum", "serde", @@ -13778,6 +13811,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "uzers" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d" +dependencies = [ + "libc", + "log", +] + [[package]] name = "v_frame" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index a3a661ab..e0c5c397 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ camino = "1.1.6" cc = "1.2.16" chacha20poly1305 = "0.10.1" chrono = "0.4.40" +ciborium = "0.2.2" clap = { version = "4.5", features = ["derive", "env"] } clap-num = "1.2.0" clap-stdin = { version = "0.6.0", default-features = false } @@ -111,6 +112,7 @@ derive_more = { version = "2.1.0", default-features = false, features = [ "from", ] } ed25519-dalek = { version = "2.1.1", default-features = false, features = ["std"] } +escargot = "0.5.15" eyre = "0.6.12" flate2 = "1.0" flume = "0.11.1" diff --git a/orb-connd/Cargo.toml b/orb-connd/Cargo.toml index 44c6092a..31d80fa9 100644 --- a/orb-connd/Cargo.toml +++ b/orb-connd/Cargo.toml @@ -17,6 +17,8 @@ base64.workspace = true bon.workspace = true chacha20poly1305.workspace = true chrono.workspace = true +ciborium.workspace = true +clap = { workspace = true, features = ["derive"] } color-eyre.workspace = true dashmap.workspace = true derive_more = { workspace = true, default-features = false, features = ["display"] } @@ -28,10 +30,12 @@ nom.workspace = true num-derive.workspace = true num-traits.workspace = true orb-backend-status-dbus.workspace = true +orb-build-info.workspace = true orb-connd-dbus.workspace = true orb-info = { workspace = true, features = ["orb-os-release", "async"] } orb-secure-storage-ca = { workspace = true, default-features = false, features = [ "backend-in-memory", + "backend-optee", ] } orb-telemetry.workspace = true p256.workspace = true @@ -49,16 +53,25 @@ tokio-serde = { workspace = true, features = ["cbor"] } tokio-util = { workspace = true, features = ["codec"] } tracing-subscriber.workspace = true tracing.workspace = true +uuid = { workspace = true, features = ["v4"] } +uzers = "0.12.0" zbus.workspace = true [dev-dependencies] async-tempfile.workspace = true +escargot.workspace = true test-utils.workspace = true nix = { workspace = true, features = ["socket"] } test-with.workspace = true mockall.workspace = true tokio-stream = { workspace = true, features = ["fs"] } +[build-dependencies] +orb-build-info = { workspace = true, features = ["build-script"] } + +[package.metadata.orb] +unsupported_targets = ["aarch64-apple-darwin", "x86_64-apple-darwin"] + [package.metadata.deb] maintainer-scripts = "debian/" assets = [ diff --git a/orb-connd/build.rs b/orb-connd/build.rs new file mode 100644 index 00000000..0710e0f2 --- /dev/null +++ b/orb-connd/build.rs @@ -0,0 +1,3 @@ +fn main() { + orb_build_info::initialize().unwrap(); +} diff --git a/orb-connd/src/main_daemon.rs b/orb-connd/src/connectivity_daemon.rs similarity index 81% rename from orb-connd/src/main_daemon.rs rename to orb-connd/src/connectivity_daemon.rs index 98e546b6..413dd7e2 100644 --- a/orb-connd/src/main_daemon.rs +++ b/orb-connd/src/connectivity_daemon.rs @@ -1,6 +1,6 @@ use crate::modem_manager::ModemManager; use crate::network_manager::NetworkManager; -use crate::service::ConndService; +use crate::service::{ConndService, ProfileStorage}; use crate::statsd::StatsdClient; use crate::{telemetry, OrbCapabilities, Tasks}; use color_eyre::eyre::{OptionExt, Result}; @@ -8,8 +8,8 @@ use orb_info::orb_os_release::OrbOsRelease; use std::time::Duration; use std::{path::Path, sync::Arc}; use tokio::{task, time}; -use tracing::error; -use tracing::{info, warn}; +use tracing::info; +use tracing::{error, warn}; #[bon::builder(finish_fn = run)] pub async fn program( @@ -21,6 +21,7 @@ pub async fn program( statsd_client: impl StatsdClient, modem_manager: impl ModemManager, connect_timeout: Duration, + profile_storage: ProfileStorage, ) -> Result { let sysfs = sysfs.as_ref().to_path_buf(); let modem_manager: Arc = Arc::new(modem_manager); @@ -38,21 +39,10 @@ pub async fn program( os_release.release_type, cap, connect_timeout, - ); - - connd.setup_default_profiles().await?; - - if let Err(e) = connd.import_wpa_conf(&usr_persistent).await { - warn!("failed to import legacy wpa config {e}"); - } - - if let Err(e) = connd.ensure_networking_enabled().await { - warn!("failed to ensure networking is enabled {e}"); - } - - if let Err(e) = connd.ensure_nm_state_below_max_size(usr_persistent).await { - warn!("failed to ensure nm state below max size: {e}"); - } + &usr_persistent, + profile_storage, + ) + .await?; let mut tasks = vec![connd.spawn()]; @@ -121,14 +111,14 @@ fn setup_modem_bands_and_modes(mm: &Arc) { mm.set_current_bands(&modem.id, &bands).await?; info!("modem bands set up successfully"); - if let Err(e) = mm + + match mm .set_allowed_and_preferred_modes(&modem.id, &["3g", "4g"], "4g") .await { - warn!("allowed and preferred could not be set up: {e}"); - } else { - info!("allowed and preferred modes set up successfully"); - } + Err(e) => warn!("allowed and preferred could not be set up: {e}"), + Ok(_) => info!("allowed and preferred modes set up successfully"), + }; Ok(()) }; diff --git a/orb-connd/src/key_material/mod.rs b/orb-connd/src/key_material/mod.rs deleted file mode 100644 index a6ebd53e..00000000 --- a/orb-connd/src/key_material/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -#![allow(dead_code)] - -use async_trait::async_trait; -use color_eyre::Result; -use secrecy::SecretVec; - -pub mod static_key; -pub mod trustzone; - -#[async_trait] -pub trait KeyMaterial: Send + Sync + 'static { - async fn fetch(&self) -> Result>; -} diff --git a/orb-connd/src/key_material/static_key.rs b/orb-connd/src/key_material/static_key.rs deleted file mode 100644 index d1c452a4..00000000 --- a/orb-connd/src/key_material/static_key.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![allow(dead_code)] - -use super::KeyMaterial; -use async_trait::async_trait; -use color_eyre::Result; -use secrecy::SecretVec; - -pub struct StaticKey(pub Vec); - -#[async_trait] -impl KeyMaterial for StaticKey { - async fn fetch(&self) -> Result> { - Ok(self.0.clone().into()) - } -} diff --git a/orb-connd/src/key_material/trustzone.rs b/orb-connd/src/key_material/trustzone.rs deleted file mode 100644 index 73ea5a79..00000000 --- a/orb-connd/src/key_material/trustzone.rs +++ /dev/null @@ -1,15 +0,0 @@ -#![allow(dead_code)] - -use super::KeyMaterial; -use async_trait::async_trait; -use color_eyre::Result; -use secrecy::SecretVec; - -pub struct TrustZone; - -#[async_trait] -impl KeyMaterial for TrustZone { - async fn fetch(&self) -> Result> { - todo!("ryan, plz impl here") - } -} diff --git a/orb-connd/src/lib.rs b/orb-connd/src/lib.rs index 67d35260..a5e054b8 100644 --- a/orb-connd/src/lib.rs +++ b/orb-connd/src/lib.rs @@ -1,27 +1,17 @@ -use color_eyre::{ - eyre::{self, Context, OptionExt}, - Result, -}; +use color_eyre::Result; use derive_more::Display; -use num_derive::{FromPrimitive, ToPrimitive}; -use num_traits::FromPrimitive as _; -use orb_secure_storage_ca::in_memory::InMemoryBackend; -use std::env::VarError; use std::path::Path; -use std::str::FromStr; use tokio::{fs, task::JoinHandle}; -pub mod key_material; -pub mod main_daemon; +pub mod connectivity_daemon; pub mod modem_manager; pub mod network_manager; +pub mod secure_storage; pub mod service; pub mod statsd; pub mod telemetry; pub mod wpa_ctrl; -mod profile_store; -mod secure_storage; mod utils; pub(crate) type Tasks = Vec>>; @@ -42,48 +32,3 @@ impl OrbCapabilities { } } } - -pub const ENV_FORK_MARKER: &str = "ORB_CONND_FORK_MARKER"; - -// TODO: Instead of toplevel enum, use inventory crate to register entry points and an -// init() hook at entry point of program. -/// The complete set of worker entrypoints that could be executed instead of the regular `main`. -#[derive(Debug, FromPrimitive, ToPrimitive)] -#[repr(u8)] -pub enum EntryPoint { - SecureStorage = 1, -} - -impl EntryPoint { - pub fn run(self) -> Result<()> { - let rt = tokio::runtime::Builder::new_current_thread().build()?; - // TODO(@vmenge): Have a way to control whether we use in-memory or actual - // optee via runtime configuration (for testing and portability) - let mut in_memory_ctx = - orb_secure_storage_ca::in_memory::InMemoryContext::default(); - rt.block_on(match self { - EntryPoint::SecureStorage => { - crate::secure_storage::subprocess::entry::( - tokio::io::join(tokio::io::stdin(), tokio::io::stdout()), - &mut in_memory_ctx, - ) - } - }) - } -} - -impl FromStr for EntryPoint { - type Err = eyre::Report; - - fn from_str(s: &str) -> Result { - Self::from_u8(u8::from_str(s).wrap_err("not a u8")?).ok_or_eyre("unknown id") - } -} - -pub fn maybe_fork() -> Result<()> { - match std::env::var(ENV_FORK_MARKER) { - Err(VarError::NotUnicode(_)) => panic!("expected unicode env var value"), - Err(VarError::NotPresent) => Ok(()), - Ok(s) => EntryPoint::from_str(&s).expect("unknown entrypoint").run(), - } -} diff --git a/orb-connd/src/main.rs b/orb-connd/src/main.rs index 4af13d3a..caa1453e 100644 --- a/orb-connd/src/main.rs +++ b/orb-connd/src/main.rs @@ -1,30 +1,96 @@ -use color_eyre::eyre::Result; +use clap::{Parser, Subcommand}; +use color_eyre::eyre::{Context, Result}; +use orb_build_info::{make_build_info, BuildInfo}; use orb_connd::{ - modem_manager::cli::ModemManagerCli, network_manager::NetworkManager, - statsd::dd::DogstatsdClient, wpa_ctrl::cli::WpaCli, + connectivity_daemon, + modem_manager::cli::ModemManagerCli, + network_manager::NetworkManager, + secure_storage::{self, ConndStorageScopes, SecureStorage}, + service::ProfileStorage, + statsd::dd::DogstatsdClient, + wpa_ctrl::cli::WpaCli, }; -use orb_info::orb_os_release::OrbOsRelease; +use orb_info::orb_os_release::{OrbOsPlatform, OrbOsRelease}; +use orb_secure_storage_ca::{in_memory::InMemoryBackend, optee::OpteeBackend}; use std::time::Duration; -use tokio::signal::unix::{self, SignalKind}; +use tokio::{ + io, + signal::unix::{self, SignalKind}, +}; +use tokio_util::sync::CancellationToken; use tracing::{info, warn}; +const BUILD_INFO: BuildInfo = make_build_info!(); const SYSLOG_IDENTIFIER: &str = "worldcoin-connd"; -#[tokio::main] -async fn main() -> Result<()> { +#[derive(Parser, Debug)] +#[clap(version = BUILD_INFO.version, about)] +pub struct Args { + #[command(subcommand)] + pub command: Option, +} + +#[derive(Subcommand, Debug, Default)] +pub enum Command { + #[default] + ConnectivityDaemon, + SecureStorageWorker { + #[arg(long)] + in_memory: bool, + #[arg(long)] + scope: ConndStorageScopes, + }, +} + +fn main() -> Result<()> { color_eyre::install()?; let tel_flusher = orb_telemetry::TelemetryConfig::new() .with_journald(SYSLOG_IDENTIFIER) .init(); - let result = async { + let args = Args::parse(); + + use Command::*; + let result = match args.command.unwrap_or_default() { + ConnectivityDaemon => connectivity_daemon(), + SecureStorageWorker { in_memory, scope } => { + secure_storage_worker(in_memory, scope) + } + }; + + tel_flusher.flush_blocking(); + + result +} + +fn connectivity_daemon() -> Result<()> { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build()?; + + rt.block_on(async { let os_release = OrbOsRelease::read().await?; let nm = NetworkManager::new( zbus::Connection::system().await?, WpaCli::new(os_release.orb_os_platform_type), ); - let tasks = orb_connd::main_daemon::program() + let cancel_token = CancellationToken::new(); + let profile_storage = match os_release.orb_os_platform_type { + OrbOsPlatform::Pearl => ProfileStorage::NetworkManager, + OrbOsPlatform::Diamond => { + let secure_storage = SecureStorage::new( + std::env::current_exe()?, + false, + cancel_token.clone(), + ConndStorageScopes::NmProfiles, + ); + + ProfileStorage::SecureStorage(secure_storage) + } + }; + + let tasks = connectivity_daemon::program() .sysfs("/sys") .usr_persistent("/usr/persistent") .network_manager(nm) @@ -33,6 +99,7 @@ async fn main() -> Result<()> { .statsd_client(DogstatsdClient::new()) .modem_manager(ModemManagerCli) .connect_timeout(Duration::from_secs(15)) + .profile_storage(profile_storage) .run() .await?; @@ -46,15 +113,42 @@ async fn main() -> Result<()> { info!("aborting tasks and exiting gracefully"); + cancel_token.cancel(); for handle in tasks { handle.abort(); } Ok(()) - } - .await; + }) +} - tel_flusher.flush().await; +fn secure_storage_worker(in_memory: bool, scope: ConndStorageScopes) -> Result<()> { + let rt = tokio::runtime::Builder::new_current_thread().build()?; - result + let io = io::join(io::stdin(), io::stdout()); + + // in_memory is only used for integration tests + if in_memory { + let mut ctx = orb_secure_storage_ca::in_memory::InMemoryContext::default(); + rt.block_on(secure_storage::subprocess::entry::( + io, &mut ctx, scope, + )) + } else { + info!("about to open optee context"); + info!( + "subprocess info: {}", + String::from_utf8_lossy( + &std::process::Command::new("id") + .output() + .expect("failed to execute `id` command") + .stdout + ) + ); + let mut ctx = + orb_secure_storage_ca::reexported_crates::optee_teec::Context::new() + .wrap_err("failed to initialize optee context")?; + rt.block_on(secure_storage::subprocess::entry::( + io, &mut ctx, scope, + )) + } } diff --git a/orb-connd/src/network_manager/mod.rs b/orb-connd/src/network_manager/mod.rs index 49bc3745..4af9e39c 100644 --- a/orb-connd/src/network_manager/mod.rs +++ b/orb-connd/src/network_manager/mod.rs @@ -1,3 +1,4 @@ +use crate::wpa_ctrl::WpaCtrl; use bon::bon; use chrono::{DateTime, Utc}; use color_eyre::{ @@ -18,10 +19,9 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::{fs, time}; use tracing::warn; +use uuid::Uuid; use zbus::zvariant::{Array, ObjectPath, OwnedObjectPath, OwnedValue, Value}; -use crate::wpa_ctrl::WpaCtrl; - #[derive(Clone)] pub struct NetworkManager { conn: zbus::Connection, @@ -37,6 +37,24 @@ impl NetworkManager { } } + pub async fn wait_for_nm_ready(&self) -> Result<()> { + let proxy = NetworkManagerProxy::new(&self.conn).await?; + let mut changes = proxy.receive_startup_changed().await; + + if !proxy.startup().await? { + return Ok(()); + } + + while let Some(change) = changes.next().await { + let startup = change.get().await?; + if !startup { + break; + } + } + + Ok(()) + } + pub async fn primary_connection(&self) -> Result> { let nm = NetworkManagerProxy::new(&self.conn).await?; let ac_path = nm.primary_connection().await?; @@ -290,15 +308,19 @@ impl NetworkManager { ssid: &str, sec: WifiSec, psk: &str, + #[builder(default = false)] persist: bool, #[builder(default = true)] autoconnect: bool, #[builder(default = 0)] priority: i32, #[builder(default = false)] hidden: bool, #[builder(default = -1)] max_autoconnect_retries: i64, // -1 here means apply gobal default - ) -> Result { + ) -> Result { self.remove_profile(id).await?; + let uuid = Uuid::new_v4().to_string(); + let connection = HashMap::from_iter([ kv("id", id), + kv("uuid", uuid.as_str()), kv("type", "802-11-wireless"), kv("autoconnect", autoconnect), kv("autoconnect-priority", priority), @@ -311,22 +333,39 @@ impl NetworkManager { kv("hidden", hidden), ]); - let sec = HashMap::from_iter([kv("key-mgmt", sec.as_nm_str()), kv("psk", psk)]); + let secv = + HashMap::from_iter([kv("key-mgmt", sec.as_nm_str()), kv("psk", psk)]); let ipv4 = HashMap::from_iter([kv("method", "auto")]); let ipv6 = HashMap::from_iter([kv("method", "ignore")]); let settings = HashMap::from_iter([ ("connection", connection), ("802-11-wireless", wifi), - ("802-11-wireless-security", sec), + ("802-11-wireless-security", secv), ("ipv4", ipv4), ("ipv6", ipv6), ]); let sp = SettingsProxy::new(&self.conn).await?; - let path = sp.add_connection(settings).await?; + let path = if persist { + sp.add_connection(settings).await? + } else { + sp.add_connection_unsaved(settings).await? + }; + + let profile = WifiProfile { + id: id.into(), + uuid, + ssid: ssid.into(), + sec, + psk: psk.into(), + autoconnect, + priority, + hidden, + path: path.to_string(), + }; - Ok(path) + Ok(profile) } /// Adds a cellular profile ensuring id uniqueness @@ -362,7 +401,7 @@ impl NetworkManager { ]); let sp = SettingsProxy::new(&self.conn).await?; - sp.add_connection(settings).await?; + sp.add_connection_unsaved(settings).await?; Ok(()) } diff --git a/orb-connd/src/profile_store/mod.rs b/orb-connd/src/profile_store/mod.rs deleted file mode 100644 index 68a4f515..00000000 --- a/orb-connd/src/profile_store/mod.rs +++ /dev/null @@ -1,242 +0,0 @@ -#![allow(dead_code)] - -use crate::{key_material::KeyMaterial, network_manager::WifiProfile}; -use chacha20poly1305::{aead::Aead, AeadCore, Key, KeyInit, XChaCha20Poly1305, XNonce}; -use color_eyre::{ - eyre::{bail, ensure, eyre, Context}, - Result, -}; -use dashmap::{mapref::one::Ref, DashMap}; -use rand::rngs::OsRng; -use secrecy::{ExposeSecret, SecretVec}; -use std::{collections::HashMap, path::PathBuf, sync::Arc}; -use tokio::fs; - -pub struct ProfileStore { - key: Result>, - store_path: PathBuf, - profiles: Arc>, -} - -impl ProfileStore { - const FILENAME: &str = "nmprofiles"; - - pub async fn from_store( - store_path: impl Into, - key_material: &impl KeyMaterial, - ) -> Self { - Self { - key: key_material.fetch().await, - store_path: store_path.into(), - profiles: Arc::new(DashMap::new()), - } - } - - pub async fn import(&self) -> Result<()> { - let secret = match &self.key { - Err(e) => bail!("failed to retrieve key material: {e}"), - Ok(v) => v, - }; - - let path = self.store_path.join(Self::FILENAME); - let mut bytes = fs::read(&path) - .await - .wrap_err_with(|| format!("failed to read profile store at {path:?}"))?; - - ensure!( - bytes.len() >= 24, - "profile store is too short ({} bytes), should be at least 24 bytes", - bytes.len() - ); - - let contents = bytes.split_off(24); - let nonce = bytes; - - let json = decrypt(contents, nonce, secret.expose_secret())?; - let profiles: HashMap = serde_json::from_slice(&json)?; - - for (key, value) in profiles { - self.profiles.insert(key, value); - } - - Ok(()) - } - - pub async fn commit(&self) -> Result<()> { - let secret = match &self.key { - Err(e) => bail!("failed to retrieve key material: {e}"), - Ok(v) => v, - }; - - let profiles: HashMap<_, _> = self - .profiles - .iter() - .map(|entry| (entry.key().clone(), entry.value().clone())) - .collect(); - - let json = serde_json::to_vec(&profiles)?; - let bytes = encrypt(json, secret.expose_secret())?; - - let path = self.store_path.join(Self::FILENAME); - fs::write(path, bytes).await?; - - Ok(()) - } - - pub fn insert(&self, profile: WifiProfile) { - self.profiles.insert(profile.ssid.clone(), profile); - } - - pub fn remove(&self, ssid: &str) -> Option { - self.profiles.remove(ssid).map(|(_, value)| value) - } - - pub fn get(&self, ssid: &str) -> Option> { - self.profiles.get(ssid) - } - - pub fn values(&self) -> Vec { - self.profiles.iter().map(|x| x.value().clone()).collect() - } -} - -fn decrypt(bytes: Vec, mut nonce: Vec, secret: &[u8]) -> Result> { - ensure!( - nonce.len() == 24, - "none len must be 24 bytes, instead is {}", - nonce.len() - ); - - ensure!( - secret.len() == 32, - "secret len must be 32 bytes, instead is {}", - secret.len() - ); - - let nonce = XNonce::from_mut_slice(&mut nonce); - - let secret = Key::from_slice(secret); - let cipher = XChaCha20Poly1305::new(secret); - - let plaintext = cipher - .decrypt(nonce, bytes.as_slice()) - .map_err(|e| eyre!("failed to decrypt profiles: {e:?}"))?; - - Ok(plaintext) -} - -fn encrypt(bytes: Vec, secret: &[u8]) -> Result> { - ensure!( - secret.len() == 32, - "secret len must be 32 bytes, instead is {}", - secret.len() - ); - - let mut rng = OsRng; - let nonce = XChaCha20Poly1305::generate_nonce(&mut rng); - let secret = Key::from_slice(secret); - - let cipher = XChaCha20Poly1305::new(secret); - let ciphertext = cipher - .encrypt(&nonce, bytes.as_slice()) - .map_err(|e| eyre!("failed to encrypt profiles: {e:?}"))?; - - let mut out = Vec::new(); - out.extend_from_slice(nonce.as_slice()); - out.extend(ciphertext); - - Ok(out) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::network_manager::WifiSec; - use async_tempfile::TempDir; - use secrecy::SecretVec; - use std::path::Path; - - const TEST_KEY: [u8; 32] = [0x42; 32]; - - // Helper to build a ProfileStore with a static key, no KeyMaterial needed. - fn make_store(dir: &Path) -> ProfileStore { - ProfileStore { - key: Ok(SecretVec::new(TEST_KEY.to_vec())), - store_path: dir.to_path_buf(), - profiles: Arc::new(DashMap::new()), - } - } - - #[tokio::test] - async fn it_imports_and_commits_wifi_profiles() { - // Arrange - let tmpdir = TempDir::new().await.unwrap(); - let tmpdirpath = tmpdir.to_path_buf(); - - let expected = [ - WifiProfile { - id: "test-ssid1".into(), - ssid: "test-ssid1".into(), - uuid: String::new(), - sec: WifiSec::Wpa2Psk, - psk: "12345678".into(), - autoconnect: true, - priority: 0, - hidden: false, - path: String::new(), - }, - WifiProfile { - id: "test-ssid2".into(), - ssid: "test-ssid2".into(), - uuid: String::new(), - sec: WifiSec::Wpa3Sae, - psk: "12345678".into(), - autoconnect: true, - priority: 1, - hidden: false, - path: String::new(), - }, - WifiProfile { - id: "test-ssid3".into(), - ssid: "test-ssid3".into(), - uuid: String::new(), - sec: WifiSec::Wpa2Psk, - psk: "12345678".into(), - autoconnect: true, - priority: 2, - hidden: false, - path: String::new(), - }, - ]; - - let store = make_store(&tmpdirpath); - - for profile in &expected { - store.insert(profile.clone()); - } - - // Act - store.commit().await.unwrap(); - - // Second store: load from disk and import - let store = make_store(&tmpdirpath); - store.import().await.unwrap(); - - // Assert - let mut actual: Vec<_> = store.values(); - actual.sort_by_key(|p| p.priority); - assert_eq!(actual, expected); - - // Act: remove some profiles - store.remove("test-ssid2"); - store.remove("test-ssid3"); - store.commit().await.unwrap(); - - // Assert - let store = make_store(&tmpdirpath); - store.import().await.unwrap(); - - let actual: Vec<_> = store.values(); - assert_eq!(actual, vec![expected[0].clone()]); - } -} diff --git a/orb-connd/src/secure_storage/mod.rs b/orb-connd/src/secure_storage/mod.rs index 13f0f3d7..15d015f0 100644 --- a/orb-connd/src/secure_storage/mod.rs +++ b/orb-connd/src/secure_storage/mod.rs @@ -1,17 +1,54 @@ mod messages; -pub(crate) mod subprocess; -use std::sync::Arc; +pub mod subprocess; use self::messages::{Request, Response}; use color_eyre::eyre::{Context, Result}; +use orb_secure_storage_ca::StorageDomain; +use std::{fmt::Display, path::PathBuf, sync::Arc}; use tokio::sync::{mpsc, oneshot}; use tokio_util::sync::{CancellationToken, DropGuard}; type RequestChannelPayload = (Request, oneshot::Sender); -/// The effective user id for the CA. -const CA_EUID: u32 = 1000; // TODO: Figure this out +/// The complete list of all "use cases" that connd has for storage. Each one gets +/// mapped to a different UID and/or TA. +#[derive(Debug, Eq, PartialEq, Clone, Copy, clap::ValueEnum)] +pub enum ConndStorageScopes { + /// NetworkManager Wifi profiles + NmProfiles, +} + +impl Display for ConndStorageScopes { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + ConndStorageScopes::NmProfiles => "nm-profiles", + }; + + f.write_str(s) + } +} + +impl ConndStorageScopes { + /// The linux username that should be used for this scope + const fn as_username(&self) -> &'static str { + match self { + Self::NmProfiles => "orb-ss-connd-nmprofiles", + } + } + + /// The linux group that should be used for this scope" + const fn as_groupname(&self) -> &'static str { + "worldcoin" + } + + /// The TA storage domain that should be used when interacting with this scope. + const fn as_domain(&self) -> StorageDomain { + match self { + ConndStorageScopes::NmProfiles => StorageDomain::WifiProfiles, + } + } +} /// Async-friendly handle through which the secure storage can be talked to. /// @@ -22,10 +59,14 @@ pub struct SecureStorage { _drop_guard: Arc, } -#[expect(dead_code)] impl SecureStorage { - pub fn new(cancel: CancellationToken) -> Self { - self::subprocess::spawn(1, cancel) + pub fn new( + exe_path: PathBuf, + in_memory: bool, + cancel: CancellationToken, + scope: ConndStorageScopes, + ) -> Self { + self::subprocess::spawn(exe_path, in_memory, 1, cancel, scope) } pub async fn get(&self, key: String) -> Result>> { diff --git a/orb-connd/src/secure_storage/subprocess.rs b/orb-connd/src/secure_storage/subprocess.rs index 6cc59482..c34c1154 100644 --- a/orb-connd/src/secure_storage/subprocess.rs +++ b/orb-connd/src/secure_storage/subprocess.rs @@ -1,31 +1,28 @@ //! Implementation of secure storage backend using a fork/exec subprocess. -use std::io::Result as IoResult; -use std::{process::Stdio, sync::Arc}; - -use color_eyre::eyre::Result; +use crate::secure_storage::messages::{GetErr, PutErr, Request, Response}; +use crate::secure_storage::{ConndStorageScopes, RequestChannelPayload, SecureStorage}; +use color_eyre::eyre::{eyre, Result}; use futures::{Sink, SinkExt as _, Stream, TryStreamExt as _}; -use orb_secure_storage_ca::reexported_crates::orb_secure_storage_proto::StorageDomain; use orb_secure_storage_ca::BackendT; +use std::io::Result as IoResult; +use std::path::PathBuf; +use std::{process::Stdio, sync::Arc}; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use tracing::{info, warn}; - -use crate::secure_storage::{RequestChannelPayload, SecureStorage, CA_EUID}; -use crate::{ - secure_storage::messages::{GetErr, PutErr, Request, Response}, - EntryPoint, -}; +use tracing::{debug, info, warn}; type SsClient = Arc>>; pub(super) fn spawn( + exe_path: PathBuf, + in_memory: bool, request_queue_size: usize, cancel: CancellationToken, + scope: ConndStorageScopes, ) -> SecureStorage { - let mut framed_pipes = make_framed_subprocess(); - // TODO: perhaps this should always be 1 or 0? + let mut framed_pipes = make_framed_subprocess(exe_path, in_memory, scope); let (request_tx, mut request_rx) = mpsc::channel::(request_queue_size); let cancel_clone = cancel.clone(); @@ -61,24 +58,41 @@ pub(super) fn spawn( } fn make_framed_subprocess( + exe_path: PathBuf, + in_memory: bool, + scope: ConndStorageScopes, ) -> impl Stream> + Sink { let current_euid = rustix::process::geteuid(); - let child_euid = if current_euid.is_root() { - CA_EUID + let current_egid = rustix::process::getegid(); + let (child_euid, child_egid) = if current_euid.is_root() { + let child_username = scope.as_username(); + let child_groupname = scope.as_groupname(); + let child_euid = uzers::get_user_by_name(child_username) + .ok_or_else(|| eyre!("username {child_username} doesn't exist")) + .unwrap() + .uid(); + let child_egid = uzers::get_group_by_name(child_groupname) + .ok_or_else(|| eyre!("username {child_groupname} doesn't exist")) + .unwrap() + .gid(); + + (child_euid, child_egid) } else { warn!("current EUID in parent connd process is not root! For this reason we will spawn the subprocess as the same EUID, since we don't have perms to change it. This probably only should be done in integration tests." ); - current_euid.as_raw() + (current_euid.as_raw(), current_egid.as_raw()) }; - let current_exe = std::env::current_exe().expect("infallible"); - let mut child = tokio::process::Command::new(current_exe) - .env( - crate::ENV_FORK_MARKER, - (EntryPoint::SecureStorage as u8).to_string(), - ) + let mut cmd = tokio::process::Command::new(exe_path); + cmd.arg("secure-storage-worker") + .args(["--scope", &scope.to_string()]) .uid(child_euid) + .gid(child_egid) .stdin(Stdio::piped()) - .stdout(Stdio::piped()) + .stdout(Stdio::piped()); + if in_memory { + cmd.arg("--in-memory"); + } + let mut child = cmd .spawn() .expect("failed to spawn secure storage subprocess"); let stdin = child.stdin.take().unwrap(); @@ -101,6 +115,7 @@ fn make_framed_subprocess( pub async fn entry( io: impl AsyncRead + AsyncWrite + Unpin + Send + Sync + 'static, secure_storage_context: &mut B::Context, + scope: ConndStorageScopes, ) -> Result<()> where B: BackendT + 'static, @@ -124,19 +139,17 @@ where ); // A bit lame to use a mutex just for `spawn_blocking()` but /shrug - let client: SsClient = - Arc::new(std::sync::Mutex::new(orb_secure_storage_ca::Client::new( - secure_storage_context, - StorageDomain::WifiProfiles, - )?)); + let client: SsClient = Arc::new(std::sync::Mutex::new( + orb_secure_storage_ca::Client::new(secure_storage_context, scope.as_domain())?, + )); while let Some(input) = framed.try_next().await? { - info!("request: {input:?}"); + debug!("request: {input:?}"); let response = match input { Request::Put { key, val } => handle_put(client.clone(), key, val).await, Request::Get { key } => handle_get(client.clone(), key).await, }; - info!("response: {response:?}"); + debug!("response: {response:?}"); framed.send(response).await?; } @@ -152,7 +165,7 @@ where tokio::task::spawn_blocking(move || client.lock().unwrap().put(&key, &val)) .await .expect("task panicked") - .map_err(|err| PutErr::Generic(err.to_string())); + .map_err(|err| PutErr::Generic(format!("{err:?}"))); Response::Put(result) } @@ -165,7 +178,7 @@ where let result = tokio::task::spawn_blocking(move || client.lock().unwrap().get(&key)) .await .expect("task panicked") - .map_err(|err| GetErr::Generic(err.to_string())); + .map_err(|err| GetErr::Generic(format!("{err:?}"))); Response::Get(result) } diff --git a/orb-connd/src/service/dbus.rs b/orb-connd/src/service/dbus.rs new file mode 100644 index 00000000..c81c9f5c --- /dev/null +++ b/orb-connd/src/service/dbus.rs @@ -0,0 +1,534 @@ +use crate::{ + network_manager::{AccessPoint, ActiveConnState, WifiProfile, WifiSec}, + service::{netconfig::NetConfig, wifi, ConndService}, + utils::IntoZResult, + OrbCapabilities, +}; +use async_trait::async_trait; +use chrono::Utc; +use color_eyre::eyre::{eyre, ContextCompat}; +use orb_connd_dbus::{ConndT, ConnectionState}; +use orb_info::orb_os_release::OrbRelease; +use rusty_network_manager::dbus_interface_types::{ + NM80211Mode, NMConnectivityState, NMState, +}; +use tracing::{error, info, warn}; +use zbus::fdo::{Error as ZErr, Result as ZResult}; + +#[async_trait] +impl ConndT for ConndService { + async fn create_softap(&self, ssid: String, _pwd: String) -> ZResult<()> { + info!("received request to create softap with ssid {ssid}"); + Err(e("not yet implemented!")) + } + + async fn remove_softap(&self, ssid: String) -> ZResult<()> { + info!("received request to remove softap with ssid {ssid}"); + Err(e("not yet implemented!")) + } + + /// d-bus impl + async fn add_wifi_profile( + &self, + ssid: String, + sec: String, + pwd: String, + hidden: bool, + ) -> ZResult<()> { + info!("adding wifi profile with ssid {ssid}"); + + let sec = match WifiSec::parse(&sec) { + Some(sec @ (WifiSec::Wpa2Psk | WifiSec::Wpa3Sae)) => sec, + _ => return Err(e("invalid sec. supported values are Wpa2Psk or Wpa3Sae")), + }; + + self.wifi_profile_add(&ssid, sec, &pwd, hidden) + .await + .into_z()?; + + if let Err(e) = self.commit_profiles_to_storage().await { + error!("failed to commit profile store when adding wifi profile. err: {e}"); + } + + info!("profile for ssid: {ssid}, saved successfully"); + + Ok(()) + } + + /// d-bus impl + async fn remove_wifi_profile(&self, ssid: String) -> ZResult<()> { + info!("removing wifi profile with ssid {ssid}"); + if ssid == Self::DEFAULT_CELLULAR_PROFILE || ssid == Self::DEFAULT_WIFI_SSID { + return Err(e(&format!("{ssid} is not an allowed SSID name",))); + } + + self.nm.remove_profile(&ssid).await.into_z()?; + + if let Err(e) = self.commit_profiles_to_storage().await { + error!( + "failed to commit profile store when removing wifi profile. err: {e}" + ); + } + + Ok(()) + } + + /// d-bus impl + async fn list_wifi_profiles(&self) -> ZResult> { + info!("listing wifi profiles"); + + let active_conns = self + .nm + .active_connections() + .await + .inspect_err(|e| warn!("issue retrieving active connections: {e}")) + .unwrap_or_default(); + + let profiles = self + .nm + .list_wifi_profiles() + .await + .into_z()? + .into_iter() + .map(|p| { + let is_active = active_conns.iter().any(|conn| { + conn.id == p.ssid && conn.state == ActiveConnState::Activated + }); + + p.into_dbus_wifi_profile(is_active) + }) + .collect(); + + Ok(profiles) + } + + /// d-bus impl + async fn scan_wifi(&self) -> ZResult> { + let aps = self.nm.wifi_scan().await.into_z()?; + let profiles = self.nm.list_wifi_profiles().await.into_z()?; + let active_conns = self + .nm + .active_connections() + .await + .inspect_err(|e| warn!("issue retrieving active connections: {e}")) + .unwrap_or_default(); + + let aps = aps + .into_iter() + .map(|ap| { + let is_saved = profiles.iter().any(|profile| ap.eq_profile(profile)); + + let is_active = active_conns.iter().any(|conn| { + conn.id == ap.ssid && conn.state == ActiveConnState::Activated + }); + + ap.into_dbus_ap(is_saved, is_active) + }) + .collect(); + + Ok(aps) + } + + /// d-bus impl + async fn netconfig_set( + &self, + set_wifi: bool, + set_smart_switching: bool, + set_airplane_mode: bool, + ) -> ZResult { + if let OrbCapabilities::WifiOnly = self.cap { + return Err(eyre!( + "cannot apply netconfig on orbs that do not have cellular" + )) + .into_z(); + } + + info!( + set_wifi, + set_smart_switching, set_airplane_mode, "setting netconfig" + ); + + let wifi_enabled = self.nm.wifi_enabled().await.into_z()?; + let smart_switching_enabled = + self.nm.smart_switching_enabled().await.into_z()?; + + info!( + wifi_enabled, + smart_switching_enabled, + airplane_mode_enabled = false, + "current netconfig" + ); + + if wifi_enabled != set_wifi { + self.nm.set_wifi(set_wifi).await.into_z()?; + } + + if smart_switching_enabled != set_smart_switching { + self.nm + .set_smart_switching(set_smart_switching) + .await + .into_z()?; + } + + if set_airplane_mode { + warn!("tried applying airplane mode on the orb, but it is not implemented yet!"); + } + + info!("sending netconfig after set"); + + Ok(orb_connd_dbus::NetConfig { + wifi: set_wifi, + smart_switching: set_smart_switching, + airplane_mode: false, + }) + } + + /// d-bus impl + async fn netconfig_get(&self) -> ZResult { + if let OrbCapabilities::WifiOnly = self.cap { + return Err(eyre!( + "cannot apply netconfig on orbs that do not have cellular" + )) + .into_z(); + } + + info!("getting netconfig"); + let wifi = self.nm.wifi_enabled().await.into_z()?; + let smart_switching = self.nm.smart_switching_enabled().await.into_z()?; + + info!("sending netconfig after get"); + Ok(orb_connd_dbus::NetConfig { + wifi, + smart_switching, + airplane_mode: false, + }) + } + + /// d-bus impl + async fn connect_to_wifi( + &self, + ssid: String, + ) -> ZResult { + info!("connecting to wifi with ssid {ssid}"); + let profiles = self.nm.list_wifi_profiles().await.into_z()?; + let max_prio = profiles + .iter() + .map(|p| p.priority) + .max() + .unwrap_or_default(); + + let profile = profiles + .into_iter() + .find(|p| p.ssid == ssid) + .wrap_err_with(|| format!("ssid {ssid} is not a saved profile")) + .into_z()?; + + let aps = self + .nm + .wifi_scan() + .await + .inspect_err(|e| error!("failed to scan for wifi networks due to err {e}")) + .into_z()?; + + let active_conns = self.nm.active_connections().await.unwrap_or_default(); + for conn in active_conns { + if conn.id == profile.id + && (ActiveConnState::Activated == conn.state + || ActiveConnState::Activating == conn.state) + { + info!("{:?}, no need to attempt connetion: {conn:?}", conn.state); + + return aps.into_iter().find(|ap| ap.ssid == profile.ssid).map(|ap|ap.into_dbus_ap(true, true)).with_context(|| format!("already connected, but could not find an ap for the connection with ssid {}. should be unreachable state.", profile.ssid)).into_z(); + } + } + + // We re-add the profile as that will overwrite the old one + // and is easier than re-using shitty NM d-bus api. + // We do this to elevate the profile's priority and make sure + // latest connected profile is always the highest priority one. + let next_priority = if profile.priority != max_prio { + self.get_next_priority().await.into_z()? + } else { + profile.priority + }; + + let profile = self + .nm + .wifi_profile(&profile.id) + .ssid(&profile.ssid) + .sec(profile.sec) + .psk(&profile.psk) + .priority(next_priority) + .hidden(profile.hidden) + .add() + .await + .into_z()?; + + let path = profile.path.clone(); + + if let Err(e) = self.commit_profiles_to_storage().await { + error!( + "failed to commit profile store when removing wifi profile. err: {e}" + ); + } + + for ap in aps { + if ap.ssid == ssid { + info!("connecting to ap {ap:?}"); + + self.nm + .connect_to_wifi( + &path, + Self::DEFAULT_WIFI_IFACE, + self.connect_timeout, + ) + .await + .map_err(|e| { + eyre!("failed to connect to wifi ssid {ssid} due to err {e}") + }) + .into_z()?; + + info!("successfully connected to ap {ap:?}"); + + return Ok(ap.into_dbus_ap(true, true)); + } + } + + Err(eyre!("could not find ssid {ssid}")).into_z() + } + + /// d-bus impl + async fn apply_wifi_qr(&self, contents: String) -> ZResult<()> { + async { + info!("applying wifi qr code"); + let skip_wifi_qr_restrictions = self.release == OrbRelease::Dev; + + if !skip_wifi_qr_restrictions { + let state = self.nm.check_connectivity().await.into_z()?; + let has_no_connectivity = NMConnectivityState::FULL != state; + + let magic_qr_applied_at = self + .magic_qr_applied_at + .read(|x| *x) + .map_err(|_| e("magic qr mtx err"))?; + + let within_magic_qr_timespan = (Utc::now() - magic_qr_applied_at) + .num_minutes() + < Self::MAGIC_QR_TIMESPAN_MIN; + + let can_apply_wifi_qr = has_no_connectivity || within_magic_qr_timespan; + + if !can_apply_wifi_qr { + let msg = + "we already have internet connectivity, use signed qr instead"; + + error!(msg); + + return Err(e(msg)); + } + } + + let creds = wifi::Credentials::parse(&contents).into_z()?; + let sec: WifiSec = creds.auth.try_into().into_z()?; + + if let Some(psk) = creds.psk { + self.wifi_profile_add(&creds.ssid, sec, &psk.0, creds.hidden) + .await + .into_z()?; + + if let Err(e) = self.commit_profiles_to_storage().await { + error!("failed to commit saved profiles: {e}"); + } + } + + self.connect_to_wifi(creds.ssid.clone()).await?; + + info!("applied wifi qr successfully"); + + Ok(()) + } + .await + .inspect_err(|e| error!("failed to apply wifi qr with {e}")) + } + + /// d-bus impl + async fn apply_netconfig_qr( + &self, + contents: String, + check_ts: bool, + ) -> ZResult<()> { + async { + info!("trying to apply netconfig qr code"); + NetConfig::verify_signature(&contents, self.release).into_z()?; + let netconf = NetConfig::parse(&contents).into_z()?; + + if check_ts { + let now = Utc::now(); + let delta = now - netconf.created_at; + if delta.num_minutes() > 10 { + return Err(e("qr code was created more than 10min ago")); + } + } + + let connect_result = if let Some(wifi_creds) = netconf.wifi_credentials { + let sec: WifiSec = wifi_creds.auth.try_into().into_z()?; + info!(ssid = wifi_creds.ssid, "adding wifi network from netconfig"); + + if let Some(psk) = wifi_creds.psk { + self.wifi_profile_add( + &wifi_creds.ssid, + sec, + &psk.0, + wifi_creds.hidden, + ) + .await + .into_z()?; + + if let Err(e) = self.commit_profiles_to_storage().await { + error!("failed to commit saved profiles: {e}"); + } + } + + self.connect_to_wifi(wifi_creds.ssid.clone()) + .await + .map(|_| ()) + } else { + Ok(()) + }; + + // Orbs without cellular do not support extra NetConfig fields + if self.cap == OrbCapabilities::WifiOnly { + return connect_result; + } + + if let Some(_airplane_mode) = netconf.airplane_mode { + warn!("airplane mode is not supported yet!"); + } + + if let Some(wifi_enabled) = netconf.wifi_enabled { + self.nm.set_wifi(wifi_enabled).await.into_z()?; + } + + if let Some(smart_switching) = netconf.smart_switching { + self.nm + .set_smart_switching(smart_switching) + .await + .into_z()?; + } + + info!( + airplane_mode = netconf.airplane_mode, + wifi_enabled = netconf.wifi_enabled, + smart_switching = netconf.smart_switching, + "applied netconfig qr successfully!" + ); + + connect_result + } + .await + .inspect_err(|e| error!("failed to apply netconfig qr with {e}")) + } + + /// d-bus impl + async fn apply_magic_reset_qr(&self) -> ZResult<()> { + info!("trying to apply magic reset qr"); + + let wifi_profiles = self.nm.list_wifi_profiles().await.into_z()?; + for profile in wifi_profiles { + if profile.ssid == Self::DEFAULT_WIFI_SSID { + continue; + } + + self.nm.remove_profile(&profile.id).await.into_z()?; + } + + self.magic_qr_applied_at + .write(|val| *val = Utc::now()) + .map_err(|_| e("magic qr mtx err"))?; + + info!("successfuly applied magic reset qr"); + + Ok(()) + } + + /// d-bus impl + async fn connection_state(&self) -> ZResult { + // let uri = self.nm.connectivity_check_uri().await.into_z()?; + + // info!("checking connectivity against {uri}"); + + self.nm.check_connectivity().await.into_z()?; + let value = self.nm.state().await.into_z()?; + + use ConnectionState::*; + let state = match value { + NMState::UNKNOWN | NMState::ASLEEP | NMState::DISCONNECTED => Disconnected, + + NMState::DISCONNECTING => Disconnecting, + + NMState::CONNECTING => Connecting, + + NMState::CONNECTED_LOCAL | NMState::CONNECTED_SITE => PartiallyConnected, + + NMState::CONNECTED_GLOBAL => Connected, + }; + + // info!("connection state: {state:?}"); + + Ok(state) + } +} + +fn e(str: &str) -> ZErr { + ZErr::Failed(str.to_string()) +} + +impl WifiProfile { + fn into_dbus_wifi_profile(self, is_active: bool) -> orb_connd_dbus::WifiProfile { + orb_connd_dbus::WifiProfile { + ssid: self.ssid, + sec: self.sec.to_string(), + psk: self.psk, + is_active, + } + } +} + +impl AccessPoint { + fn into_dbus_ap( + self, + is_saved: bool, + is_active: bool, + ) -> orb_connd_dbus::AccessPoint { + use NM80211Mode::*; + let mode = match self.mode { + UNKNOWN => "Unknown", + ADHOC => "Adhoc", + INFRA => "Infra", + AP => "Ap", + MESH => "Mesh", + } + .to_string(); + + let capabiltiies = orb_connd_dbus::AccessPointCapabilities { + privacy: self.capabilities.privacy, + wps: self.capabilities.wps, + wps_pbc: self.capabilities.wps_pbc, + wps_pin: self.capabilities.wps_pin, + }; + + orb_connd_dbus::AccessPoint { + ssid: self.ssid, + bssid: self.bssid, + is_saved, + is_active, + freq_mhz: self.freq_mhz, + max_bitrate_kbps: self.max_bitrate_kbps, + strength_pct: self.strength_pct, + last_seen: self.last_seen.to_rfc3339(), + mode, + capabilities: capabiltiies, + sec: self.sec.to_string(), + } + } +} diff --git a/orb-connd/src/service/mod.rs b/orb-connd/src/service/mod.rs index 1824ac89..9c089342 100644 --- a/orb-connd/src/service/mod.rs +++ b/orb-connd/src/service/mod.rs @@ -1,22 +1,16 @@ -use crate::network_manager::{ - AccessPoint, ActiveConnState, NetworkManager, WifiProfile, WifiSec, -}; +use crate::network_manager::{NetworkManager, WifiProfile, WifiSec}; +use crate::secure_storage::SecureStorage; use crate::utils::{IntoZResult, State}; use crate::OrbCapabilities; -use async_trait::async_trait; use chrono::{DateTime, Utc}; -use color_eyre::eyre::eyre; use color_eyre::{ - eyre::{bail, ContextCompat}, + eyre::{bail, eyre, Context}, Result, }; -use netconfig::NetConfig; -use orb_connd_dbus::{Connd, ConndT, ConnectionState, OBJ_PATH, SERVICE}; +use orb_connd_dbus::{Connd, OBJ_PATH, SERVICE}; use orb_info::orb_os_release::OrbRelease; -use rusty_network_manager::dbus_interface_types::{ - NM80211Mode, NMConnectivityState, NMState, -}; use std::cmp; +use std::collections::HashSet; use std::path::Path; use std::time::Duration; use tokio::fs::{self, File}; @@ -25,8 +19,8 @@ use tokio::task::{self, JoinHandle}; use tracing::{error, info, warn}; use wifi::Auth; use wpa_conf::LegacyWpaConfig; -use zbus::fdo::{Error as ZErr, Result as ZResult}; +mod dbus; mod mecard; mod netconfig; mod wifi; @@ -39,9 +33,23 @@ pub struct ConndService { cap: OrbCapabilities, magic_qr_applied_at: State>, connect_timeout: Duration, + profile_storage: ProfileStorage, +} + +#[derive(Debug)] +pub enum ProfileStorage { + SecureStorage(SecureStorage), + NetworkManager, +} + +impl ProfileStorage { + pub fn should_persist(&self) -> bool { + matches!(self, Self::NetworkManager) + } } impl ConndService { + const NM_FOLDER: &str = "network-manager"; const DEFAULT_CELLULAR_PROFILE: &str = "cellular"; const DEFAULT_CELLULAR_APN: &str = "em"; const DEFAULT_CELLULAR_IFACE: &str = "cdc-wdm0"; @@ -50,22 +58,69 @@ impl ConndService { const DEFAULT_WIFI_IFACE: &str = "wlan0"; const MAGIC_QR_TIMESPAN_MIN: i64 = 10; const NM_STATE_MAX_SIZE_KB: u64 = 1024; + const SECURE_STORAGE_KEY: &str = "nmprofiles"; - pub fn new( + pub async fn new( session_dbus: zbus::Connection, nm: NetworkManager, release: OrbRelease, cap: OrbCapabilities, connect_timeout: Duration, - ) -> Self { - Self { + usr_persistent: impl AsRef, + profile_storage: ProfileStorage, + ) -> Result { + let usr_persistent = usr_persistent.as_ref(); + + let connd = Self { session_dbus, nm, release, cap, magic_qr_applied_at: State::new(DateTime::default()), connect_timeout, + profile_storage, + }; + + // we start after NM, but NM slow (c++ haha), we also slow, but they slower + // so we need to be sure NM is available on dbus + info!("waiting for NetworkManager to be ready"); + connd.nm.wait_for_nm_ready().await?; + info!("NetworkManager is now ready"); + + let startup_errors = [ + connd + .setup_default_profiles() + .await + .wrap_err("failed to setup default profiles"), + connd + .import_stored_profiles() + .await + .wrap_err("failed to import stored profiles"), + connd + .import_legacy_wpa_conf(&usr_persistent) + .await + .wrap_err("failed to import legacy wpa config"), + connd + .ensure_networking_enabled() + .await + .wrap_err("failed to ensure networking is enabled"), + connd + .ensure_nm_state_below_max_size(usr_persistent) + .await + .wrap_err("failed to ensure nm state below max size"), + connd + .commit_profiles_to_storage() + .await + .wrap_err("failed to commit profiles to storage"), + ] + .into_iter() + .flat_map(|r| r.err()); + + for error in startup_errors { + warn!(?error, "non fatal startup failure") } + + Ok(connd) } pub fn spawn(self) -> JoinHandle> { @@ -89,7 +144,54 @@ impl ConndService { }) } - pub async fn ensure_networking_enabled(&self) -> Result<()> { + async fn wifi_profile_add( + &self, + ssid: &str, + sec: WifiSec, + pwd: &str, + hidden: bool, + ) -> Result<()> { + if ssid == Self::DEFAULT_CELLULAR_PROFILE || ssid == Self::DEFAULT_WIFI_SSID { + bail!("{ssid} is not an allowed SSID name"); + } + + let already_saved = self + .nm + .list_wifi_profiles() + .await + .into_z()? + .into_iter() + .any(|profile| { + profile.ssid == ssid + && profile.sec == sec + && profile.psk == pwd + && profile.hidden == hidden + }); + + if already_saved { + info!("profile for ssid: {ssid}, already saved, exiting early"); + + return Ok(()); + } + + let prio = self.get_next_priority().await?; + + self.nm + .wifi_profile(ssid) + .ssid(ssid) + .sec(sec) + .psk(pwd) + .autoconnect(true) + .priority(prio) + .hidden(hidden) + .persist(self.profile_storage.should_persist()) + .add() + .await?; + + Ok(()) + } + + async fn ensure_networking_enabled(&self) -> Result<()> { if !self.nm.networking_enabled().await? { self.nm.set_networking(true).await?; } @@ -108,7 +210,7 @@ impl ConndService { Ok(()) } - pub async fn setup_default_profiles(&self) -> Result<()> { + async fn setup_default_profiles(&self) -> Result<()> { let cel_profiles = self.nm.list_cellular_profiles().await?; let default_cel_profile_exists = cel_profiles .iter() @@ -138,6 +240,7 @@ impl ConndService { .autoconnect(true) .hidden(false) .priority(-998) + .persist(self.profile_storage.should_persist()) .add() .await?; } @@ -145,17 +248,72 @@ impl ConndService { Ok(()) } - pub async fn import_wpa_conf(&self, wpa_conf_dir: impl AsRef) -> Result<()> { + async fn import_stored_profiles(&self) -> Result<()> { + let ProfileStorage::SecureStorage(ss) = &self.profile_storage else { + return Ok(()); + }; + + let ss_profiles = ss + .get(Self::SECURE_STORAGE_KEY.into()) + .await + .wrap_err("failed trying to import from secure storage")?; + + let ss_profiles: Vec = ss_profiles + .map(|p| ciborium::de::from_reader(p.as_slice())) + .transpose()? + .unwrap_or_default(); + + let nm_profiles = self.nm.list_wifi_profiles().await?; + let nm_ssids: HashSet<_> = nm_profiles.iter().map(|p| &p.ssid).collect(); + + let to_import = ss_profiles + .into_iter() + .filter(|p| !nm_ssids.contains(&p.ssid)); + + for profile in to_import { + self.wifi_profile_add( + &profile.ssid, + profile.sec, + &profile.psk, + profile.hidden, + ) + .await?; + } + + Ok(()) + } + + async fn commit_profiles_to_storage(&self) -> Result<()> { + let ProfileStorage::SecureStorage(ss) = &self.profile_storage else { + return Ok(()); + }; + + let profiles = self.nm.list_wifi_profiles().await?; + + let mut bytes = Vec::new(); + ciborium::ser::into_writer(&profiles, &mut bytes)?; + + ss.put(Self::SECURE_STORAGE_KEY.into(), bytes) + .await + .wrap_err("failed trying to commit to secure storage")?; + + Ok(()) + } + + pub async fn import_legacy_wpa_conf( + &self, + wpa_conf_dir: impl AsRef, + ) -> Result<()> { let wpa_conf_path = wpa_conf_dir.as_ref().join("wpa_supplicant-wlan0.conf"); match File::open(&wpa_conf_path).await { Ok(file) => { let wpa_conf = LegacyWpaConfig::from_file(file).await?; if wpa_conf.ssid != Self::DEFAULT_WIFI_SSID { - self.add_wifi_profile( - wpa_conf.ssid, - "wpa2".into(), - wpa_conf.psk, + self.wifi_profile_add( + &wpa_conf.ssid, + WifiSec::Wpa2Psk, + &wpa_conf.psk, false, ) .await?; @@ -176,11 +334,12 @@ impl ConndService { Ok(()) } + /// returns true if anything was deleted because state was too big pub async fn ensure_nm_state_below_max_size( &self, usr_persistent: impl AsRef, ) -> Result<()> { - let nm_dir = usr_persistent.as_ref().join("network-manager"); + let nm_dir = usr_persistent.as_ref().join(Self::NM_FOLDER); let dir_size_kb = async || -> Result { let mut total_bytes = 0u64; let mut stack = vec![nm_dir.clone()]; @@ -202,13 +361,30 @@ impl ConndService { Ok(total_bytes / 1024) }; - let dir_size = dir_size_kb().await?; - if dir_size < Self::NM_STATE_MAX_SIZE_KB { - info!("/usr/persistent/network-manager is below 1024kB. current size {dir_size}kB"); + let get_state_size = async || -> Result { + let dir_size = dir_size_kb().await?; + let ss_size = match &self.profile_storage { + ProfileStorage::NetworkManager => 0, + ProfileStorage::SecureStorage(ss) => ss + .get(Self::SECURE_STORAGE_KEY.to_owned()) + .await + .inspect_err(|e| error!("failed to read from secure storage when trying to calculate size: {e}")) + .ok() + .flatten() + .map(|bytes|bytes.len()) + .unwrap_or_default() as u64, + }; + + Ok(dir_size + ss_size) + }; + + let state_size = get_state_size().await?; + if state_size < Self::NM_STATE_MAX_SIZE_KB { + info!("{nm_dir:?} plus SecureStorage-{} is below 1024kB. current size {state_size}kB", Self::SECURE_STORAGE_KEY); return Ok(()); } - warn!("/usr/persistent/network-manager is above 1024kB. current size {dir_size}. attempting to reduce size"); + warn!("{nm_dir:?} plus SecureStorage-{} is above 1024kB. current size {state_size}kB. attempting to reduce size", Self::SECURE_STORAGE_KEY); // remove excess wifi profiles let mut wifi_profiles = self.nm.list_wifi_profiles().await?; @@ -225,11 +401,10 @@ impl ConndService { self.nm.remove_profile(&profile.id).await?; } + self.commit_profiles_to_storage().await?; + // remove dhcp leases and seen-bssids - let varlib = usr_persistent - .as_ref() - .join("network-manager") - .join("varlib"); + let varlib = usr_persistent.as_ref().join(Self::NM_FOLDER).join("varlib"); let seen_bssids = varlib.join("seen-bssids"); let mut to_delete = vec![seen_bssids]; @@ -251,7 +426,7 @@ impl ConndService { fs::remove_file(filepath).await?; } - let dir_size = dir_size_kb().await?; + let dir_size = get_state_size().await?; if dir_size < Self::NM_STATE_MAX_SIZE_KB { info!("successfully reduced nm state size to {dir_size}kB"); Ok(()) @@ -280,470 +455,6 @@ impl ConndService { } } -#[async_trait] -impl ConndT for ConndService { - async fn create_softap(&self, ssid: String, _pwd: String) -> ZResult<()> { - info!("received request to create softap with ssid {ssid}"); - Err(e("not yet implemented!")) - } - - async fn remove_softap(&self, ssid: String) -> ZResult<()> { - info!("received request to remove softap with ssid {ssid}"); - Err(e("not yet implemented!")) - } - - async fn add_wifi_profile( - &self, - ssid: String, - sec: String, - pwd: String, - hidden: bool, - ) -> ZResult<()> { - info!("adding wifi profile with ssid {ssid}"); - if ssid == Self::DEFAULT_CELLULAR_PROFILE || ssid == Self::DEFAULT_WIFI_SSID { - return Err(e(&format!("{ssid} is not an allowed SSID name"))); - } - - let sec = match WifiSec::parse(&sec) { - Some(sec @ (WifiSec::Wpa2Psk | WifiSec::Wpa3Sae)) => sec, - _ => return Err(e("invalid sec. supported values are Wpa2Psk or Wpa3Sae")), - }; - - let already_saved = self - .nm - .list_wifi_profiles() - .await - .into_z()? - .into_iter() - .any(|profile| { - profile.ssid == ssid - && profile.sec == sec - && profile.psk == pwd - && profile.hidden == hidden - }); - - if already_saved { - info!("profile for ssid: {ssid}, already saved, exiting early"); - - return Ok(()); - } - - let prio = self.get_next_priority().await.into_z()?; - - self.nm - .wifi_profile(&ssid) - .ssid(&ssid) - .sec(sec) - .psk(&pwd) - .autoconnect(true) - .priority(prio) - .hidden(hidden) - .add() - .await - .into_z()?; - - info!("profile for ssid: {ssid}, saved successfully"); - - Ok(()) - } - - async fn remove_wifi_profile(&self, ssid: String) -> ZResult<()> { - info!("removing wifi profile with ssid {ssid}"); - if ssid == Self::DEFAULT_CELLULAR_PROFILE || ssid == Self::DEFAULT_WIFI_SSID { - return Err(e(&format!("{ssid} is not an allowed SSID name",))); - } - - self.nm.remove_profile(&ssid).await.into_z()?; - - Ok(()) - } - - async fn list_wifi_profiles(&self) -> ZResult> { - let active_conns = self - .nm - .active_connections() - .await - .inspect_err(|e| warn!("issue retrieving active connections: {e}")) - .unwrap_or_default(); - - let profiles = self - .nm - .list_wifi_profiles() - .await - .into_z()? - .into_iter() - .map(|p| { - let is_active = active_conns.iter().any(|conn| { - conn.id == p.ssid && conn.state == ActiveConnState::Activated - }); - - p.into_dbus_wifi_profile(is_active) - }) - .collect(); - - Ok(profiles) - } - - async fn scan_wifi(&self) -> ZResult> { - let aps = self.nm.wifi_scan().await.into_z()?; - let profiles = self.nm.list_wifi_profiles().await.into_z()?; - let active_conns = self - .nm - .active_connections() - .await - .inspect_err(|e| warn!("issue retrieving active connections: {e}")) - .unwrap_or_default(); - - let aps = aps - .into_iter() - .map(|ap| { - let is_saved = profiles.iter().any(|profile| ap.eq_profile(profile)); - - let is_active = active_conns.iter().any(|conn| { - conn.id == ap.ssid && conn.state == ActiveConnState::Activated - }); - - ap.into_dbus_ap(is_saved, is_active) - }) - .collect(); - - Ok(aps) - } - - async fn netconfig_set( - &self, - set_wifi: bool, - set_smart_switching: bool, - set_airplane_mode: bool, - ) -> ZResult { - if let OrbCapabilities::WifiOnly = self.cap { - return Err(eyre!( - "cannot apply netconfig on orbs that do not have cellular" - )) - .into_z(); - } - - info!( - set_wifi, - set_smart_switching, set_airplane_mode, "setting netconfig" - ); - - let wifi_enabled = self.nm.wifi_enabled().await.into_z()?; - let smart_switching_enabled = - self.nm.smart_switching_enabled().await.into_z()?; - - info!( - wifi_enabled, - smart_switching_enabled, - airplane_mode_enabled = false, - "current netconfig" - ); - - if wifi_enabled != set_wifi { - self.nm.set_wifi(set_wifi).await.into_z()?; - } - - if smart_switching_enabled != set_smart_switching { - self.nm - .set_smart_switching(set_smart_switching) - .await - .into_z()?; - } - - if set_airplane_mode { - warn!("tried applying airplane mode on the orb, but it is not implemented yet!"); - } - - info!("sending netconfig after set"); - - Ok(orb_connd_dbus::NetConfig { - wifi: set_wifi, - smart_switching: set_smart_switching, - airplane_mode: false, - }) - } - - async fn netconfig_get(&self) -> ZResult { - if let OrbCapabilities::WifiOnly = self.cap { - return Err(eyre!( - "cannot apply netconfig on orbs that do not have cellular" - )) - .into_z(); - } - - info!("getting netconfig"); - let wifi = self.nm.wifi_enabled().await.into_z()?; - let smart_switching = self.nm.smart_switching_enabled().await.into_z()?; - - info!("sending netconfig after get"); - Ok(orb_connd_dbus::NetConfig { - wifi, - smart_switching, - airplane_mode: false, - }) - } - - async fn connect_to_wifi( - &self, - ssid: String, - ) -> ZResult { - info!("connecting to wifi with ssid {ssid}"); - let profiles = self.nm.list_wifi_profiles().await.into_z()?; - let max_prio = profiles - .iter() - .map(|p| p.priority) - .max() - .unwrap_or_default(); - - let profile = profiles - .into_iter() - .find(|p| p.ssid == ssid) - .wrap_err_with(|| format!("ssid {ssid} is not a saved profile")) - .into_z()?; - - let aps = self - .nm - .wifi_scan() - .await - .inspect_err(|e| error!("failed to scan for wifi networks due to err {e}")) - .into_z()?; - - let active_conns = self.nm.active_connections().await.unwrap_or_default(); - for conn in active_conns { - if conn.id == profile.id - && (ActiveConnState::Activated == conn.state - || ActiveConnState::Activating == conn.state) - { - info!("{:?}, no need to attempt connetion: {conn:?}", conn.state); - - return aps.into_iter().find(|ap| ap.ssid == profile.ssid).map(|ap|ap.into_dbus_ap(true, true)).with_context(|| format!("already connected, but could not find an ap for the connection with ssid {}. should be unreachable state.", profile.ssid)).into_z(); - } - } - - // We re-add the profile as that will overwrite the old one - // and is easier than re-using shitty NM d-bus api. - // We do this to elevate the profile's priority and make sure - // latest connected profile is always the highest priority one. - let next_priority = if profile.priority != max_prio { - self.get_next_priority().await.into_z()? - } else { - profile.priority - }; - - let profile = self - .nm - .wifi_profile(&profile.id) - .ssid(&profile.ssid) - .sec(profile.sec) - .psk(&profile.psk) - .priority(next_priority) - .hidden(profile.hidden) - .add() - .await - .into_z()?; - - for ap in aps { - if ap.ssid == ssid { - info!("connecting to ap {ap:?}"); - - self.nm - .connect_to_wifi( - &profile, - Self::DEFAULT_WIFI_IFACE, - self.connect_timeout, - ) - .await - .map_err(|e| { - eyre!("failed to connect to wifi ssid {ssid} due to err {e}") - }) - .into_z()?; - - info!("successfully connected to ap {ap:?}"); - - return Ok(ap.into_dbus_ap(true, true)); - } - } - - Err(eyre!("could not find ssid {ssid}")).into_z() - } - - async fn apply_wifi_qr(&self, contents: String) -> ZResult<()> { - async { - info!("applying wifi qr code"); - let skip_wifi_qr_restrictions = self.release == OrbRelease::Dev; - - if !skip_wifi_qr_restrictions { - let state = self.nm.check_connectivity().await.into_z()?; - let has_no_connectivity = NMConnectivityState::FULL != state; - - let magic_qr_applied_at = self - .magic_qr_applied_at - .read(|x| *x) - .map_err(|_| e("magic qr mtx err"))?; - - let within_magic_qr_timespan = (Utc::now() - magic_qr_applied_at) - .num_minutes() - < Self::MAGIC_QR_TIMESPAN_MIN; - - let can_apply_wifi_qr = has_no_connectivity || within_magic_qr_timespan; - - if !can_apply_wifi_qr { - let msg = - "we already have internet connectivity, use signed qr instead"; - - error!(msg); - - return Err(e(msg)); - } - } - - let creds = wifi::Credentials::parse(&contents).into_z()?; - let sec: WifiSec = creds.auth.try_into().into_z()?; - - if let Some(psk) = creds.psk { - self.add_wifi_profile( - creds.ssid.clone(), - sec.to_string(), - psk.0, - creds.hidden, - ) - .await?; - } - - self.connect_to_wifi(creds.ssid.clone()).await?; - - info!("applied wifi qr successfully"); - - Ok(()) - } - .await - .inspect_err(|e| error!("failed to apply wifi qr with {e}")) - } - - async fn apply_netconfig_qr( - &self, - contents: String, - check_ts: bool, - ) -> ZResult<()> { - async { - info!("trying to apply netconfig qr code"); - NetConfig::verify_signature(&contents, self.release).into_z()?; - let netconf = NetConfig::parse(&contents).into_z()?; - - if check_ts { - let now = Utc::now(); - let delta = now - netconf.created_at; - if delta.num_minutes() > 10 { - return Err(e("qr code was created more than 10min ago")); - } - } - - let connect_result = if let Some(wifi_creds) = netconf.wifi_credentials { - let sec: WifiSec = wifi_creds.auth.try_into().into_z()?; - info!(ssid = wifi_creds.ssid, "adding wifi network from netconfig"); - - if let Some(psk) = wifi_creds.psk { - self.add_wifi_profile( - wifi_creds.ssid.clone(), - sec.to_string(), - psk.0, - wifi_creds.hidden, - ) - .await?; - } - - self.connect_to_wifi(wifi_creds.ssid.clone()) - .await - .map(|_| ()) - } else { - Ok(()) - }; - - // Orbs without cellular do not support extra NetConfig fields - if self.cap == OrbCapabilities::WifiOnly { - return connect_result; - } - - if let Some(_airplane_mode) = netconf.airplane_mode { - warn!("airplane mode is not supported yet!"); - } - - if let Some(wifi_enabled) = netconf.wifi_enabled { - self.nm.set_wifi(wifi_enabled).await.into_z()?; - } - - if let Some(smart_switching) = netconf.smart_switching { - self.nm - .set_smart_switching(smart_switching) - .await - .into_z()?; - } - - info!( - airplane_mode = netconf.airplane_mode, - wifi_enabled = netconf.wifi_enabled, - smart_switching = netconf.smart_switching, - "applied netconfig qr successfully!" - ); - - connect_result - } - .await - .inspect_err(|e| error!("failed to apply netconfig qr with {e}")) - } - - async fn apply_magic_reset_qr(&self) -> ZResult<()> { - info!("trying to apply magic reset qr"); - - let wifi_profiles = self.nm.list_wifi_profiles().await.into_z()?; - for profile in wifi_profiles { - if profile.ssid == Self::DEFAULT_WIFI_SSID { - continue; - } - - self.nm.remove_profile(&profile.id).await.into_z()?; - } - - self.magic_qr_applied_at - .write(|val| *val = Utc::now()) - .map_err(|_| e("magic qr mtx err"))?; - - info!("successfuly applied magic reset qr"); - - Ok(()) - } - - async fn connection_state(&self) -> ZResult { - let uri = self.nm.connectivity_check_uri().await.into_z()?; - - info!("checking connectivity against {uri}"); - - self.nm.check_connectivity().await.into_z()?; - let value = self.nm.state().await.into_z()?; - - use ConnectionState::*; - let state = match value { - NMState::UNKNOWN | NMState::ASLEEP | NMState::DISCONNECTED => Disconnected, - - NMState::DISCONNECTING => Disconnecting, - - NMState::CONNECTING => Connecting, - - NMState::CONNECTED_LOCAL | NMState::CONNECTED_SITE => PartiallyConnected, - - NMState::CONNECTED_GLOBAL => Connected, - }; - - info!("connection state: {state:?}"); - - Ok(state) - } -} - -fn e(str: &str) -> ZErr { - ZErr::Failed(str.to_string()) -} - impl TryInto for Auth { type Error = color_eyre::Report; @@ -774,53 +485,3 @@ impl TryFrom for Auth { Ok(auth) } } - -impl WifiProfile { - fn into_dbus_wifi_profile(self, is_active: bool) -> orb_connd_dbus::WifiProfile { - orb_connd_dbus::WifiProfile { - ssid: self.ssid, - sec: self.sec.to_string(), - psk: self.psk, - is_active, - } - } -} - -impl AccessPoint { - fn into_dbus_ap( - self, - is_saved: bool, - is_active: bool, - ) -> orb_connd_dbus::AccessPoint { - use NM80211Mode::*; - let mode = match self.mode { - UNKNOWN => "Unknown", - ADHOC => "Adhoc", - INFRA => "Infra", - AP => "Ap", - MESH => "Mesh", - } - .to_string(); - - let capabiltiies = orb_connd_dbus::AccessPointCapabilities { - privacy: self.capabilities.privacy, - wps: self.capabilities.wps, - wps_pbc: self.capabilities.wps_pbc, - wps_pin: self.capabilities.wps_pin, - }; - - orb_connd_dbus::AccessPoint { - ssid: self.ssid, - bssid: self.bssid, - is_saved, - is_active, - freq_mhz: self.freq_mhz, - max_bitrate_kbps: self.max_bitrate_kbps, - strength_pct: self.strength_pct, - last_seen: self.last_seen.to_rfc3339(), - mode, - capabilities: capabiltiies, - sec: self.sec.to_string(), - } - } -} diff --git a/orb-connd/tests/connd_service.rs b/orb-connd/tests/connd_service.rs index 887ceecd..25f63be8 100644 --- a/orb-connd/tests/connd_service.rs +++ b/orb-connd/tests/connd_service.rs @@ -4,7 +4,7 @@ use orb_connd::{network_manager::WifiSec, OrbCapabilities}; use orb_connd_dbus::{ConnectionState, WifiProfile}; use orb_info::orb_os_release::{OrbOsPlatform, OrbRelease}; use prelude::future::Callback; -use std::{path::PathBuf, time::Duration}; +use std::time::Duration; use tokio::{fs, time}; use tokio_stream::wrappers::ReadDirStream; @@ -355,8 +355,8 @@ async fn it_wipes_dhcp_leases_and_seen_bssids_if_too_big() { // Arrange let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Prod) - .arrange(Callback::new(async |usr_persistent: PathBuf| { - let varlib = usr_persistent.join("network-manager").join("varlib"); + .arrange(Callback::new(async |ctx: fixture::Ctx| { + let varlib = ctx.usr_persistent.join("network-manager").join("varlib"); fs::create_dir_all(&varlib).await.unwrap(); // we create a file thats 2mb in size, which puts us @@ -550,7 +550,7 @@ async fn it_imports_wpa_conf_with_hex_encoded_ssid() { // Arrange let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Dev) - .arrange(Callback::new(async |usr_persistent: PathBuf| { + .arrange(Callback::new(async |ctx: fixture::Ctx| { // Create wpa_supplicant config with hex-encoded SSID // SSID "546573744e6574776f726b" is hex for "TestNetwork" let wpa_conf_content = r#"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev @@ -562,7 +562,7 @@ network={ } "#; fs::write( - usr_persistent.join("wpa_supplicant-wlan0.conf"), + ctx.usr_persistent.join("wpa_supplicant-wlan0.conf"), wpa_conf_content, ) .await @@ -596,7 +596,7 @@ async fn it_imports_wpa_conf_with_quoted_ssid() { // Arrange let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Dev) - .arrange(Callback::new(async |usr_persistent: PathBuf| { + .arrange(Callback::new(async |ctx: fixture::Ctx| { // Create wpa_supplicant config with quoted SSID let wpa_conf_content = r#"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev @@ -607,7 +607,7 @@ network={ } "#; fs::write( - usr_persistent.join("wpa_supplicant-wlan0.conf"), + ctx.usr_persistent.join("wpa_supplicant-wlan0.conf"), wpa_conf_content, ) .await @@ -642,7 +642,7 @@ async fn it_handles_invalid_wpa_conf_gracefully() { { let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Dev) - .arrange(Callback::new(async |usr_persistent: PathBuf| { + .arrange(Callback::new(async |ctx: fixture::Ctx| { let wpa_conf_content = r#"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev network={ key_mgmt=WPA-PSK @@ -651,7 +651,7 @@ network={ } "#; fs::write( - usr_persistent.join("wpa_supplicant-wlan0.conf"), + ctx.usr_persistent.join("wpa_supplicant-wlan0.conf"), wpa_conf_content, ) .await @@ -671,7 +671,7 @@ network={ { let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Dev) - .arrange(Callback::new(async |usr_persistent: PathBuf| { + .arrange(Callback::new(async |ctx: fixture::Ctx| { let wpa_conf_content = r#"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev network={ key_mgmt=WPA-PSK @@ -680,7 +680,7 @@ network={ } "#; fs::write( - usr_persistent.join("wpa_supplicant-wlan0.conf"), + ctx.usr_persistent.join("wpa_supplicant-wlan0.conf"), wpa_conf_content, ) .await @@ -698,7 +698,7 @@ network={ { let fx = Fixture::platform(OrbOsPlatform::Pearl) .release(OrbRelease::Dev) - .arrange(Callback::new(async |usr_persistent: PathBuf| { + .arrange(Callback::new(async |ctx: fixture::Ctx| { let long_ssid = "a".repeat(33); let wpa_conf_content = format!( r#"ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev @@ -710,7 +710,7 @@ network={{ "# ); fs::write( - usr_persistent.join("wpa_supplicant-wlan0.conf"), + ctx.usr_persistent.join("wpa_supplicant-wlan0.conf"), wpa_conf_content, ) .await diff --git a/orb-connd/tests/fixture.rs b/orb-connd/tests/fixture.rs index 8be7c465..f6b54d0f 100644 --- a/orb-connd/tests/fixture.rs +++ b/orb-connd/tests/fixture.rs @@ -1,15 +1,18 @@ use async_trait::async_trait; use bon::bon; use color_eyre::Result; +use escargot::CargoBuild; use mockall::mock; use nix::libc; use orb_connd::{ - main_daemon::program, + connectivity_daemon::program, modem_manager::{ connection_state::ConnectionState, Location, Modem, ModemId, ModemInfo, ModemManager, Signal, SimId, SimInfo, }, network_manager::NetworkManager, + secure_storage::{ConndStorageScopes, SecureStorage}, + service::ProfileStorage, statsd::StatsdClient, wpa_ctrl::WpaCtrl, OrbCapabilities, @@ -20,6 +23,7 @@ use prelude::future::Callback; use std::{env, path::PathBuf, time::Duration}; use test_utils::docker::{self, Container}; use tokio::{fs, task::JoinHandle, time}; +use tokio_util::sync::CancellationToken; use zbus::Address; #[allow(dead_code)] @@ -30,16 +34,27 @@ pub struct Fixture { program_handles: Vec>>, pub sysfs: PathBuf, pub usr_persistent: PathBuf, + pub secure_storage: SecureStorage, + pub secure_storage_cancel_token: CancellationToken, } impl Drop for Fixture { fn drop(&mut self) { + self.secure_storage_cancel_token.cancel(); + for handle in &self.program_handles { handle.abort(); } } } +#[allow(dead_code)] +pub struct Ctx { + pub usr_persistent: PathBuf, + pub nm: NetworkManager, + pub secure_storage: SecureStorage, +} + #[bon] impl Fixture { #[builder(start_fn = platform, finish_fn = run)] @@ -50,9 +65,11 @@ impl Fixture { modem_manager: Option, statsd: Option, wpa_ctrl: Option, - arrange: Option>, + arrange: Option>, #[builder(default = false)] log: bool, ) -> Self { + let _ = color_eyre::install(); + if log { let _ = orb_telemetry::TelemetryConfig::new().init(); } @@ -60,8 +77,10 @@ impl Fixture { let container = setup_container().await; let sysfs = container.tempdir.path().join("sysfs"); let usr_persistent = container.tempdir.path().join("usr_persistent"); + let network_manager_folder = usr_persistent.join("network-manager"); fs::create_dir_all(&sysfs).await.unwrap(); fs::create_dir_all(&usr_persistent).await.unwrap(); + fs::create_dir_all(&network_manager_folder).await.unwrap(); if cap == OrbCapabilities::CellularAndWifi { let stats = sysfs @@ -80,10 +99,6 @@ impl Fixture { time::sleep(Duration::from_secs(1)).await; - if let Some(arrange_cb) = arrange { - arrange_cb.call(usr_persistent.clone()).await; - } - let dbus_socket = container.tempdir.path().join("socket"); let dbus_socket = format!("unix:path={}", dbus_socket.display()); let addr: Address = dbus_socket.parse().unwrap(); @@ -99,6 +114,39 @@ impl Fixture { wpa_ctrl.unwrap_or_else(default_mock_wpa_cli), ); + let built_connd = CargoBuild::new() + .bin("orb-connd") + .current_target() + .current_release() + .manifest_path(env!("CARGO_MANIFEST_PATH")) + .run() + .unwrap(); + + let cancel_token = CancellationToken::new(); + let secure_storage = SecureStorage::new( + built_connd.path().into(), + true, + cancel_token.clone(), + ConndStorageScopes::NmProfiles, + ); + + let profile_storage = match platform { + OrbOsPlatform::Pearl => ProfileStorage::NetworkManager, + OrbOsPlatform::Diamond => { + ProfileStorage::SecureStorage(secure_storage.clone()) + } + }; + + if let Some(arrange_cb) = arrange { + let ctx = Ctx { + usr_persistent: usr_persistent.clone(), + nm: nm.clone(), + secure_storage: secure_storage.clone(), + }; + + arrange_cb.call(ctx).await; + } + let program_handles = program() .os_release(OrbOsRelease { release_type: release, @@ -113,6 +161,7 @@ impl Fixture { .usr_persistent(usr_persistent.clone()) .session_bus(conn.clone()) .connect_timeout(Duration::from_secs(1)) + .profile_storage(profile_storage) .run() .await .unwrap(); @@ -132,6 +181,8 @@ impl Fixture { container, sysfs, usr_persistent, + secure_storage, + secure_storage_cancel_token: cancel_token, } } diff --git a/orb-connd/tests/profile_store.rs b/orb-connd/tests/profile_store.rs new file mode 100644 index 00000000..93836267 --- /dev/null +++ b/orb-connd/tests/profile_store.rs @@ -0,0 +1,81 @@ +use fixture::Fixture; +use orb_connd::{ + network_manager::{WifiProfile, WifiSec}, + OrbCapabilities, +}; +use orb_info::orb_os_release::{OrbOsPlatform, OrbRelease}; +use prelude::future::Callback; +use uuid::Uuid; + +mod fixture; + +#[cfg_attr(target_os = "macos", test_with::no_env(GITHUB_ACTIONS))] +#[tokio::test] +async fn it_adds_removes_and_imports_encrypted_profiles() { + // Arrange + let fx = Fixture::platform(OrbOsPlatform::Diamond) + .cap(OrbCapabilities::WifiOnly) + .release(OrbRelease::Prod) + .arrange(Callback::new(async |ctx: fixture::Ctx| { + // prepopulate with encrypted profiles + let profiles = vec![WifiProfile { + id: "imported".into(), + uuid: Uuid::new_v4().to_string(), + ssid: "imported".into(), + sec: WifiSec::Wpa2Psk, + psk: "1234567890".into(), + autoconnect: false, + priority: 0, + hidden: false, + path: String::new(), + }]; + + let mut bytes = Vec::new(); + ciborium::ser::into_writer(&profiles, &mut bytes).unwrap(); + + ctx.secure_storage + .put("nmprofiles".into(), bytes) + .await + .unwrap(); + })) + .run() + .await; + + let connd = fx.connd().await; + + // Assert profile was imported + let imported_profile = connd + .list_wifi_profiles() + .await + .unwrap() + .into_iter() + .find(|p| p.ssid == "imported"); + + assert!(imported_profile.is_some()); + + // Act: remove imported, add new profile + connd.remove_wifi_profile("imported".into()).await.unwrap(); + connd + .add_wifi_profile( + "new_profile".into(), + "Wpa2Psk".into(), + "1234567890".into(), + false, + ) + .await + .unwrap(); + + // Assert: store reflects changes + let profiles = fx + .secure_storage + .get("nmprofiles".into()) + .await + .unwrap() + .unwrap(); + + let profiles: Vec = + ciborium::de::from_reader(profiles.as_slice()).unwrap(); + + let ssids: Vec<_> = profiles.into_iter().map(|p| p.ssid).collect(); + assert_eq!(vec!["hotspot", "new_profile"], ssids); +}