Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2fb75a099 | |||
|
|
82a5d9469b |
11 changed files with 132 additions and 149 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2030,7 +2030,6 @@ dependencies = [
|
|||
"tokio",
|
||||
"trust-dns-resolver",
|
||||
"uuid",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
12
Cargo.toml
12
Cargo.toml
|
|
@ -9,6 +9,7 @@ mtp = { git = "https://git.methanium.net/Methanium/mtp.git", features = [
|
|||
"client",
|
||||
"crypto",
|
||||
"files",
|
||||
"raw",
|
||||
] }
|
||||
mtp-transport = { git = "https://git.methanium.net/Methanium/mtp.git" }
|
||||
|
||||
|
|
@ -28,17 +29,8 @@ tokio = { version = "1.53.0", features = ["full"] }
|
|||
log = "0.4"
|
||||
dotenv = "0.15.0"
|
||||
strum_macros = "0.28.0"
|
||||
livekit-api = { version = "0.5.6", features = ["rustls-tls-native-roots"] }
|
||||
livekit-api = { version = "0.6.0", features = ["rustls-tls-native-roots"] }
|
||||
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"]
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -7,7 +7,7 @@ The Omikron is only used when the Iota is in Centralized and Hybrid mode, or whe
|
|||
|
||||
## Configuration
|
||||
|
||||
`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.
|
||||
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.
|
||||
|
||||
Rho connection budgets can be configured with:
|
||||
|
||||
|
|
@ -19,11 +19,3 @@ 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.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit f3c5037b0a099ae5389486eec77471bd5addab4a
|
||||
Subproject commit f4e45aa3a3ad0e3c3a257f66857b904a1af7901c
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
#[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");
|
||||
}
|
||||
|
|
@ -1,37 +1,23 @@
|
|||
use std::{env, io::ErrorKind, path::Path};
|
||||
use std::{io::ErrorKind, path::Path};
|
||||
|
||||
use mtp::crypto::Keyring;
|
||||
use mtp::files::{FileError, load_keyring, save_keyring, save_public_key_bundle};
|
||||
use zeroize::Zeroizing;
|
||||
use mtp::files::{FileError, load_keyring_raw, save_keyring_raw, save_public_key_bundle};
|
||||
|
||||
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<Zeroizing<Vec<u8>>, 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<Path>,
|
||||
public_key_path: impl AsRef<Path>,
|
||||
) -> Result<Keyring, String> {
|
||||
let keyring_path = keyring_path.as_ref();
|
||||
let public_key_path = public_key_path.as_ref();
|
||||
|
||||
match load_keyring(keyring_path, passphrase) {
|
||||
match load_keyring_raw(keyring_path) {
|
||||
Ok(keyring) => Ok(keyring),
|
||||
Err(FileError::Io(error)) if error.kind() == ErrorKind::NotFound => {
|
||||
let keyring = Keyring::generate();
|
||||
save_keyring(&keyring, keyring_path, passphrase).map_err(|error| {
|
||||
save_keyring_raw(&keyring, keyring_path).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(
|
||||
|
|
@ -41,10 +27,7 @@ pub fn load_or_create_keyring(
|
|||
)
|
||||
},
|
||||
)?;
|
||||
eprintln!(
|
||||
"Generated new protected keyring at {}",
|
||||
keyring_path.display()
|
||||
);
|
||||
eprintln!("Generated new keyring at {}", keyring_path.display());
|
||||
Ok(keyring)
|
||||
}
|
||||
Err(error) => Err(format!(
|
||||
|
|
@ -54,29 +37,11 @@ pub fn load_or_create_keyring(
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "raw-migration")]
|
||||
#[allow(dead_code)]
|
||||
pub fn migrate_raw_keyring(
|
||||
passphrase: &[u8],
|
||||
keyring_path: impl AsRef<Path>,
|
||||
public_key_path: impl AsRef<Path>,
|
||||
) -> Result<Keyring, String> {
|
||||
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::{
|
||||
fs,
|
||||
env, fs,
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
|
|
@ -111,11 +76,15 @@ mod tests {
|
|||
#[test]
|
||||
fn missing_identity_is_created_and_can_be_loaded_again() {
|
||||
let paths = TestPaths::new();
|
||||
let passphrase = b"test identity secret";
|
||||
let first = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap();
|
||||
let first = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap();
|
||||
let first_public = first.public_key_bundle().try_as_bytes().unwrap();
|
||||
let second = load_or_create_keyring(passphrase, &paths.keyring, &paths.public_key).unwrap();
|
||||
let stored = load_keyring_raw(&paths.keyring).unwrap();
|
||||
let second = load_or_create_keyring(&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
|
||||
|
|
@ -127,9 +96,7 @@ mod tests {
|
|||
let paths = TestPaths::new();
|
||||
fs::write(&paths.keyring, b"not a keyring").unwrap();
|
||||
|
||||
let error =
|
||||
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
|
||||
.unwrap_err();
|
||||
let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
|
||||
assert!(error.contains("unable to load existing Omikron identity"));
|
||||
}
|
||||
|
||||
|
|
@ -138,9 +105,7 @@ mod tests {
|
|||
let paths = TestPaths::new();
|
||||
fs::create_dir(&paths.keyring).unwrap();
|
||||
|
||||
let error =
|
||||
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
|
||||
.unwrap_err();
|
||||
let error = load_or_create_keyring(&paths.keyring, &paths.public_key).unwrap_err();
|
||||
assert!(error.contains("unable to load existing Omikron identity"));
|
||||
}
|
||||
|
||||
|
|
@ -149,33 +114,7 @@ mod tests {
|
|||
let paths = TestPaths::new();
|
||||
fs::create_dir(&paths.public_key).unwrap();
|
||||
|
||||
let error =
|
||||
load_or_create_keyring(b"test identity secret", &paths.keyring, &paths.public_key)
|
||||
.unwrap_err();
|
||||
let error = load_or_create_keyring(&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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
src/main.rs
17
src/main.rs
|
|
@ -24,9 +24,7 @@ use crate::{
|
|||
app_state::AppState,
|
||||
calls::{call_manager::CallManager, call_util::LiveKitService},
|
||||
config::Config,
|
||||
identity::{
|
||||
KEYRING_PATH, PUBLIC_KEY_PATH, identity_secret_from_environment, load_or_create_keyring,
|
||||
},
|
||||
identity::{KEYRING_PATH, PUBLIC_KEY_PATH, load_or_create_keyring},
|
||||
omega::omega_connection::{OmegaConnection, start_task_cleanup_loop},
|
||||
rho::rho_manager::RhoManager,
|
||||
rho::server::start,
|
||||
|
|
@ -50,18 +48,7 @@ async fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
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,
|
||||
) {
|
||||
let keyring = match load_or_create_keyring(KEYRING_PATH, PUBLIC_KEY_PATH) {
|
||||
Ok(keyring) => keyring,
|
||||
Err(error) => {
|
||||
eprintln!("Unable to load Omikron keyring: {error}");
|
||||
|
|
|
|||
|
|
@ -17,6 +17,13 @@ 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<AppState>,
|
||||
pub user_id: u64,
|
||||
|
|
@ -210,7 +217,10 @@ impl ClientConnection {
|
|||
.await;
|
||||
return;
|
||||
};
|
||||
match rho.await_peer_control(&cv.with_sender(self.user_id)).await {
|
||||
match rho
|
||||
.await_peer_control(&authenticated_peer_control_request(&cv, self.user_id))
|
||||
.await
|
||||
{
|
||||
Ok(response) => self.send_message(&response).await,
|
||||
Err(_) => {
|
||||
self.send_error_response(message_id, CommunicationType::ErrorInternal)
|
||||
|
|
@ -968,6 +978,29 @@ 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<ClientConnection>
|
||||
impl Clone for ClientConnection {
|
||||
fn clone(&self) -> Self {
|
||||
|
|
|
|||
|
|
@ -302,6 +302,13 @@ 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()) {
|
||||
|
|
@ -309,8 +316,6 @@ impl IotaConnection {
|
|||
}
|
||||
}
|
||||
|
||||
log_cv_in!(PrintType::Iota, cv);
|
||||
|
||||
let cv = if cv.is_type(CommunicationType::ClientStateSync) {
|
||||
self.add_call_state(cv).await
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -95,6 +95,10 @@ 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 {
|
||||
|
|
@ -296,7 +300,8 @@ mod tests {
|
|||
use mtp::codec::{CommunicationType, CommunicationValue, DataValue};
|
||||
|
||||
use super::{
|
||||
RelayRouteError, RelaySource, RouteTarget, prepare_frame, validate_source_next_hop,
|
||||
MessageSecurityClass, RelayRouteError, RelaySource, RouteTarget, message_security_class,
|
||||
prepare_frame, validate_source_next_hop,
|
||||
};
|
||||
|
||||
fn wire(target: RouteTarget) -> u64 {
|
||||
|
|
@ -418,4 +423,37 @@ 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ pub struct RhoConnection {
|
|||
app_connections: DashMap<(u64, String, Uuid), Arc<AppConnection>>,
|
||||
}
|
||||
|
||||
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<IotaConnection>, user_ids: Vec<i64>) -> Self {
|
||||
|
|
@ -254,25 +264,19 @@ 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;
|
||||
let Some(receiver_id) = cv.receiver() else {
|
||||
if cv.receiver().is_none() {
|
||||
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 connection.user_id != receiver_id {
|
||||
if !message_targets_client(&cv, connection.user_id, connection.session_id) {
|
||||
continue;
|
||||
}
|
||||
if let Some(session_id) = session_id {
|
||||
if connection.session_id as i128 != session_id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
connection.clone().send_message(&cv).await;
|
||||
}
|
||||
}
|
||||
|
|
@ -357,3 +361,20 @@ 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue