diff --git a/Cargo.lock b/Cargo.lock index 988bd38..b30f3a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2030,6 +2030,7 @@ dependencies = [ "tokio", "trust-dns-resolver", "uuid", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ed73254..101bb0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [ "client", "crypto", "files", - "raw", ] } mtp-transport = { git = "https://git.methanium.net/Methanium/mtp.git" } @@ -34,3 +33,12 @@ livekit-protocol = "=0.7.10" thiserror = "2.0.19" trust-dns-resolver = "0.23.2" serde_json = "1.0.151" +zeroize = "1.9" + +[features] +raw-migration = ["mtp/raw"] + +[[bin]] +name = "migrate_raw_keyring" +path = "src/bin/migrate_raw_keyring.rs" +required-features = ["raw-migration"] diff --git a/README.md b/README.md index 9d560dc..1ca8ee3 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ The Omikron is only used when the Iota is in Centralized and Hybrid mode, or whe ## Configuration -Omikron stores its keyring unencrypted in `omikron.mk` and its public key bundle in `omikron.mpkb`. If the keyring does not exist, Omikron generates it on startup. +`OMIKRON_IDENTITY_SECRET` is required. Provision it through the deployment's secret environment before starting Omikron. Omikron uses it to load `omikron.mk` as a protected keyring and fails startup if the secret or existing identity cannot be loaded. Rho connection budgets can be configured with: @@ -19,3 +19,11 @@ Rho connection budgets can be configured with: semaphore and to the MTP WebServer admission semaphore. This means the same budget limits transport handshakes and authenticated Rho sessions instead of only limiting connections after authentication. + +To migrate an existing raw `omikron.mk`, provision `OMIKRON_IDENTITY_SECRET` and run: + +```sh +cargo run --features raw-migration --bin migrate_raw_keyring +``` + +The normal Omikron binary does not enable raw keyring support. diff --git a/mtp-type-maps b/mtp-type-maps index f4e45aa..f3c5037 160000 --- a/mtp-type-maps +++ b/mtp-type-maps @@ -1 +1 @@ -Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c +Subproject commit f3c5037b0a099ae5389486eec77471bd5addab4a diff --git a/src/bin/migrate_raw_keyring.rs b/src/bin/migrate_raw_keyring.rs new file mode 100644 index 0000000..bdd82d4 --- /dev/null +++ b/src/bin/migrate_raw_keyring.rs @@ -0,0 +1,23 @@ +#[path = "../identity.rs"] +mod identity; + +use identity::{ + KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, migrate_raw_keyring, +}; + +fn main() { + let secret = match identity_secret_from_environment() { + Ok(secret) => secret, + Err(error) => { + eprintln!("Unable to load Omikron identity secret: {error}"); + std::process::exit(1); + } + }; + + if let Err(error) = migrate_raw_keyring(secret.as_slice(), KEYRING_PATH, PUBLIC_KEY_PATH) { + eprintln!("Unable to migrate Omikron identity: {error}"); + std::process::exit(1); + } + + println!("Migrated {KEYRING_PATH} to protected keyring storage"); +} diff --git a/src/identity.rs b/src/identity.rs index b52d8cc..c0459c2 100644 --- a/src/identity.rs +++ b/src/identity.rs @@ -1,23 +1,37 @@ -use std::{io::ErrorKind, path::Path}; +use std::{env, io::ErrorKind, path::Path}; use mtp::crypto::Keyring; -use mtp::files::{FileError, load_keyring_raw, save_keyring_raw, save_public_key_bundle}; +use mtp::files::{FileError, load_keyring, save_keyring, save_public_key_bundle}; +use zeroize::Zeroizing; pub const KEYRING_PATH: &str = "./omikron.mk"; pub const PUBLIC_KEY_PATH: &str = "./omikron.mpkb"; +const IDENTITY_SECRET_ENV: &str = "OMIKRON_IDENTITY_SECRET"; +pub fn identity_secret_from_environment() -> Result>, String> { + let secret = env::var(IDENTITY_SECRET_ENV).map_err(|_| { + format!("{IDENTITY_SECRET_ENV} must be set before Omikron networking starts") + })?; + if secret.trim().is_empty() { + return Err(format!("{IDENTITY_SECRET_ENV} must not be empty")); + } + Ok(Zeroizing::new(secret.into_bytes())) +} + +#[allow(dead_code)] pub fn load_or_create_keyring( + passphrase: &[u8], keyring_path: impl AsRef, public_key_path: impl AsRef, ) -> Result { let keyring_path = keyring_path.as_ref(); let public_key_path = public_key_path.as_ref(); - match load_keyring_raw(keyring_path) { + match load_keyring(keyring_path, passphrase) { Ok(keyring) => Ok(keyring), Err(FileError::Io(error)) if error.kind() == ErrorKind::NotFound => { let keyring = Keyring::generate(); - save_keyring_raw(&keyring, keyring_path).map_err(|error| { + save_keyring(&keyring, keyring_path, passphrase).map_err(|error| { format!("unable to persist Omikron keyring at {keyring_path:?}: {error}") })?; save_public_key_bundle(&keyring.public_key_bundle(), public_key_path).map_err( @@ -27,7 +41,10 @@ pub fn load_or_create_keyring( ) }, )?; - eprintln!("Generated new keyring at {}", keyring_path.display()); + eprintln!( + "Generated new protected keyring at {}", + keyring_path.display() + ); Ok(keyring) } Err(error) => Err(format!( @@ -37,11 +54,29 @@ pub fn load_or_create_keyring( } } +#[cfg(feature = "raw-migration")] +#[allow(dead_code)] +pub fn migrate_raw_keyring( + passphrase: &[u8], + keyring_path: impl AsRef, + public_key_path: impl AsRef, +) -> Result { + use mtp::files::load_keyring_raw; + + let keyring = load_keyring_raw(keyring_path.as_ref()) + .map_err(|error| format!("raw keyring migration failed: {error}"))?; + save_keyring(&keyring, keyring_path.as_ref(), passphrase) + .map_err(|error| format!("protected keyring write failed: {error}"))?; + save_public_key_bundle(&keyring.public_key_bundle(), public_key_path.as_ref()) + .map_err(|error| format!("public key bundle write failed: {error}"))?; + Ok(keyring) +} + #[cfg(test)] mod tests { use super::*; use std::{ - env, fs, + fs, time::{SystemTime, UNIX_EPOCH}, }; @@ -76,15 +111,11 @@ mod tests { #[test] fn missing_identity_is_created_and_can_be_loaded_again() { let paths = TestPaths::new(); - let first = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap(); + let passphrase = b"test identity secret"; + let first = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap(); let first_public = first.public_key_bundle().try_as_bytes().unwrap(); - let stored = load_keyring_raw(&paths.keyring).unwrap(); - let second = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap(); + let second = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap(); - assert_eq!( - stored.public_key_bundle().try_as_bytes().unwrap(), - first_public - ); assert_eq!( second.public_key_bundle().try_as_bytes().unwrap(), first_public @@ -96,7 +127,9 @@ mod tests { let paths = TestPaths::new(); fs::write(&paths.keyring, b"not a keyring").unwrap(); - let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err(); + let error = + load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key) + .unwrap_err(); assert!(error.contains("unable to load existing Omikron identity")); } @@ -105,7 +138,9 @@ mod tests { let paths = TestPaths::new(); fs::create_dir(&paths.keyring).unwrap(); - let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err(); + let error = + load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key) + .unwrap_err(); assert!(error.contains("unable to load existing Omikron identity")); } @@ -114,7 +149,33 @@ mod tests { let paths = TestPaths::new(); fs::create_dir(&paths.public_key).unwrap(); - let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err(); + let error = + load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key) + .unwrap_err(); assert!(error.contains("unable to persist Omikron public key bundle")); } + + #[cfg(feature = "raw-migration")] + #[test] + fn raw_identity_migration_rewrites_protected_storage() { + use mtp::files::{load_keyring, save_keyring_raw}; + + let paths = TestPaths::new(); + let original = Keyring::generate(); + save_keyring_raw(&original, &paths.keyring).unwrap(); + + let migrated = + migrate_raw_keyring(b"test identity secret", &paths.keyring, &paths.public_key) + .unwrap(); + let loaded = load_keyring(&paths.keyring, b"test identity secret").unwrap(); + + assert_eq!( + migrated.public_key_bundle().try_as_bytes().unwrap(), + original.public_key_bundle().try_as_bytes().unwrap() + ); + assert_eq!( + loaded.public_key_bundle().try_as_bytes().unwrap(), + original.public_key_bundle().try_as_bytes().unwrap() + ); + } } diff --git a/src/main.rs b/src/main.rs index f7769fe..ffc3a70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,7 +24,9 @@ use crate::{ app_state::AppState, calls::{call_manager::CallManager, call_util::LiveKitService}, config::Config, - identity::{KEYRING_PATH, PUBLIC_KEY_PATH, load_or_create_keyring}, + identity::{ + KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, load_or_create_keyring, + }, omega::omega_connection::{OmegaConnection, start_task_cleanup_loop}, rho::rho_manager::RhoManager, rho::server::start, @@ -48,7 +50,18 @@ async fn main() { } }; - let keyring = match load_or_create_keyring(KEYRING_PATH, PUBLIC_KEY_PATH) { + let identity_secret = match identity_secret_from_environment() { + Ok(secret) => secret, + Err(error) => { + eprintln!("Unable to load Omikron identity secret: {error}"); + return; + } + }; + let keyring = match load_or_create_keyring( + &identity_secret, + KEYRING_PATH, + PUBLIC_KEY_PATH, + ) { Ok(keyring) => keyring, Err(error) => { eprintln!("Unable to load Omikron keyring: {error}"); diff --git a/src/rho/client_connection.rs b/src/rho/client_connection.rs index 2899c2a..d87eea1 100644 --- a/src/rho/client_connection.rs +++ b/src/rho/client_connection.rs @@ -17,13 +17,6 @@ use tokio::sync::RwLock; use trust_dns_resolver::TokioAsyncResolver; use uuid::Uuid; -fn authenticated_peer_control_request( - cv: &CommunicationValue, - user_id: u64, -) -> CommunicationValue { - cv.clone().with_sender(user_id) -} - pub struct ClientConnection { pub state: Arc, pub user_id: u64, @@ -217,10 +210,7 @@ impl ClientConnection { .await; return; }; - match rho - .await_peer_control(&authenticated_peer_control_request(&cv, self.user_id)) - .await - { + match rho.await_peer_control(&cv.with_sender(self.user_id)).await { Ok(response) => self.send_message(&response).await, Err(_) => { self.send_error_response(message_id, CommunicationType::ErrorInternal) @@ -978,29 +968,6 @@ impl ClientConnection { } } -#[cfg(test)] -mod tests { - use super::authenticated_peer_control_request; - use mtp::codec::{CommunicationType, CommunicationValue, DataType, DataValue}; - - #[test] - fn peer_control_requests_replace_a_client_supplied_sender() { - let request = CommunicationValue::new(CommunicationType::SyncedSettingSet) - .with_sender(99) - .add_typed_default(DataType::UserId, DataValue::SignedNumber(99)); - - let authenticated = authenticated_peer_control_request(&request, 7); - - assert_eq!(authenticated.sender(), Some(7)); - assert_eq!( - authenticated - .get_data(DataType::UserId) - .and_then(|value| value.as_number()), - Some(99) - ); - } -} - // Implement Clone to make it easier to work with Arc impl Clone for ClientConnection { fn clone(&self) -> Self { diff --git a/src/rho/iota_connection.rs b/src/rho/iota_connection.rs index 813131b..51d89fd 100755 --- a/src/rho/iota_connection.rs +++ b/src/rho/iota_connection.rs @@ -302,13 +302,6 @@ impl IotaConnection { return; } - log_cv_in!(PrintType::Iota, cv); - - if cv.is_type(CommunicationType::SyncedSettingChanged) { - self.forward_to_client(cv).await; - return; - } - let msg_id = message_id; if let Some((_, task)) = self.waiting_tasks.remove(&msg_id) { if (task)(self.clone(), cv.clone()) { @@ -316,6 +309,8 @@ impl IotaConnection { } } + log_cv_in!(PrintType::Iota, cv); + let cv = if cv.is_type(CommunicationType::ClientStateSync) { self.add_call_state(cv).await } else { diff --git a/src/rho/relay_router.rs b/src/rho/relay_router.rs index 19e46f8..97a5384 100644 --- a/src/rho/relay_router.rs +++ b/src/rho/relay_router.rs @@ -95,10 +95,6 @@ pub fn message_security_class(frame: &CommunicationValue) -> MessageSecurityClas } else if frame.is_type(CommunicationType::GetChatSecret) || frame.is_type(CommunicationType::MessageGet) || frame.is_type(CommunicationType::MessagesGet) - || frame.is_type(CommunicationType::SyncedSettingSet) - || frame.is_type(CommunicationType::SyncedSettingGet) - || frame.is_type(CommunicationType::SyncedSettingDelete) - || frame.is_type(CommunicationType::SyncedSettingsList) { MessageSecurityClass::AuthenticatedPeerControl } else { @@ -300,8 +296,7 @@ mod tests { use mtp::codec::{CommunicationType, CommunicationValue, DataValue}; use super::{ - MessageSecurityClass, RelayRouteError, RelaySource, RouteTarget, message_security_class, - prepare_frame, validate_source_next_hop, + RelayRouteError, RelaySource, RouteTarget, prepare_frame, validate_source_next_hop, }; fn wire(target: RouteTarget) -> u64 { @@ -423,37 +418,4 @@ mod tests { }; assert_eq!(routed.payload(), response.payload()); } - - #[test] - fn synchronized_setting_requests_use_authenticated_peer_control() { - for setting_type in [ - CommunicationType::SyncedSettingSet, - CommunicationType::SyncedSettingGet, - CommunicationType::SyncedSettingDelete, - CommunicationType::SyncedSettingsList, - ] { - let frame = CommunicationValue::new(setting_type); - assert_eq!( - message_security_class(&frame), - MessageSecurityClass::AuthenticatedPeerControl - ); - } - } - - #[test] - fn synchronized_setting_requests_are_not_relay_only() { - for setting_type in [ - CommunicationType::SyncedSettingSet, - CommunicationType::SyncedSettingGet, - CommunicationType::SyncedSettingDelete, - CommunicationType::SyncedSettingsList, - CommunicationType::SyncedSettingChanged, - ] { - let frame = CommunicationValue::new(setting_type); - assert_ne!( - message_security_class(&frame), - MessageSecurityClass::RelayOnly - ); - } - } } diff --git a/src/rho/rho_connection.rs b/src/rho/rho_connection.rs index 6d79c4a..c933acf 100644 --- a/src/rho/rho_connection.rs +++ b/src/rho/rho_connection.rs @@ -18,16 +18,6 @@ pub struct RhoConnection { app_connections: DashMap<(u64, String, Uuid), Arc>, } -fn message_targets_client(cv: &CommunicationValue, user_id: u64, session_id: u64) -> bool { - if cv.receiver() != Some(user_id) { - return false; - } - let Some(target_session_id) = cv.get_data(DataType::SessionId).as_number() else { - return true; - }; - target_session_id == i128::from(session_id) -} - impl RhoConnection { /// Create a new RhoConnection pub async fn new(iota_connection: Arc, user_ids: Vec) -> Self { @@ -264,19 +254,25 @@ impl RhoConnection { /// Send message from Iota to specific client pub async fn message_to_client(&self, cv: CommunicationValue) { let connections = self.get_client_connections().await; - if cv.receiver().is_none() { + let Some(receiver_id) = cv.receiver() else { log_err!( 0, crate::util::logger::PrintType::General, "Discarded message without an MTP receiver" ); return; - } + }; + let session_id = cv.get_data(DataType::SessionId).as_number(); for connection in connections.iter() { - if !message_targets_client(&cv, connection.user_id, connection.session_id) { + if connection.user_id != receiver_id { continue; } + if let Some(session_id) = session_id { + if connection.session_id as i128 != session_id { + continue; + } + } connection.clone().send_message(&cv).await; } } @@ -361,20 +357,3 @@ impl RhoConnection { self.client_connections.len() } } - -#[cfg(test)] -mod tests { - use super::message_targets_client; - use mtp::codec::{CommunicationType, CommunicationValue, DataType}; - - #[test] - fn sessionless_setting_change_targets_all_devices_for_one_user() { - let event = - CommunicationValue::new(CommunicationType::SyncedSettingChanged).with_receiver(7); - - assert!(message_targets_client(&event, 7, 1)); - assert!(message_targets_client(&event, 7, 2)); - assert!(!message_targets_client(&event, 8, 1)); - assert!(event.get_data(DataType::SessionId).is_none()); - } -}