The onion routing, I admit, is pretty complex.
The public mailbox implementation? That's negative eleven lines of code, because your code already silently ignores messages that don't decrypt. All you have to do is not vary the topic string.
diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs
index d2ffe36..455d669 100644
--- a/src-tauri/src/app.rs
+++ b/src-tauri/src/app.rs
@@ -74,14 +74,13 @@ async fn cmd_connect(
offline: bool,
) -> Result<String, String> {
let broker = transport::parse_broker(&server)?;
- let topic = cipher::room_topic(&room);
let persistent = offline && !server.trim().is_empty();
let opts = transport::build_options(&broker, &room, &nickname, persistent);
let (client, mut eventloop) = AsyncClient::new(opts, 64);
client
- .subscribe(&topic, QoS::AtLeastOnce)
+ .subscribe(&config::TOPIC, QoS::AtLeastOnce)
.await
.map_err(|e| format!("subscribe: {e}"))?;
@@ -90,7 +89,7 @@ async fn cmd_connect(
let mut guard = state.0.lock().await;
*guard = Some(transport::MqttLink {
client: client.clone(),
- topic: topic.clone(),
+ topic: config::TOPIC.clone(),
nickname: nickname.clone(),
passphrase: passphrase.clone(),
is_persistent: persistent,
@@ -190,10 +189,12 @@ fn history_path(room: &str) -> std::path::PathBuf {
let dir = std::path::PathBuf::from(base).join("naihe");
std::fs::create_dir_all(&dir).ok();
- // Reuse cipher module's topic hash for consistent naming
- let topic = cipher::room_topic(room);
- let name = topic.replace('/', "_");
- dir.join(format!("{name}.{}", config::HISTORY_EXT))
+ // Use hash for naming that won't get filesystem corrupted.
+ let mut hasher = Sha256::new();
+ hasher.update(room.trim().as_bytes());
+ let digest = hasher.finalize();
+ let hex: String = digest.iter().take(4).map(|b| format!("{b:02x}")).collect();
+ dir.join(format!("{hex}.{}", config::HISTORY_EXT))
}
#[tauri::command]
@@ -238,6 +239,5 @@ async fn cmd_pull_offline(
since: i64,
) -> Result<Vec<transport::ChatMessage>, String> {
let broker = transport::parse_broker(&server)?;
- let topic = cipher::room_topic(&room);
- transport::fetch_offline(&broker.host, &topic, &passphrase, since).await
+ transport::fetch_offline(&broker.host, &passphrase, since).await
}
diff --git a/src-tauri/src/cipher.rs b/src-tauri/src/cipher.rs
index 590527b..a167079 100644
--- a/src-tauri/src/cipher.rs
+++ b/src-tauri/src/cipher.rs
@@ -184,15 +184,6 @@ pub fn resembles_cipher(text: &str) -> bool {
}
}
-/// Hash a room name into an MQTT topic path.
-pub fn room_topic(room: &str) -> String {
- let mut hasher = Sha256::new();
- hasher.update(room.trim().as_bytes());
- let digest = hasher.finalize();
- let hex: String = digest.iter().take(4).map(|b| format!("{b:02x}")).collect();
- format!("{}/{hex}", config::TOPIC_PREFIX)
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs
index 993258a..1755934 100644
--- a/src-tauri/src/config.rs
+++ b/src-tauri/src/config.rs
@@ -17,9 +17,10 @@ pub const ARGON2_MEMORY_KB: u32 = 65536; // 64 MB
pub const ARGON2_ITERATIONS: u32 = 3;
pub const ARGON2_PARALLELISM: u32 = 4;
-/// Topic prefix for MQTT channels.
-/// Messages are published to "{TOPIC_PREFIX}/{room_hash}".
-pub const TOPIC_PREFIX: &str = "nh";
+/// Topic for MQTT channels.
+/// Messages are published to "{TOPIC}".
+/// Different channels are distinguished by discarding messages that won't decrypt.
+pub const TOPIC: &str = "nh";
/// Client ID prefix for MQTT connections.
pub const CLIENT_PREFIX: &str = "nh-";
diff --git a/src-tauri/src/transport.rs b/src-tauri/src/transport.rs
index 6b0ced0..6616067 100644
--- a/src-tauri/src/transport.rs
+++ b/src-tauri/src/transport.rs
@@ -45,12 +45,10 @@ pub fn build_options(
) -> MqttOptions {
let client_id = if persistent {
// Deterministic ID so the broker can queue messages
- let room_hash = crate::cipher::room_topic(room);
let nick_part: String = nickname.chars().take(4).collect();
format!(
- "{}{}-{}",
+ "{}-{}",
config::CLIENT_PREFIX,
- &room_hash[room_hash.len().saturating_sub(4)..],
nick_part
)
} else {
@@ -86,7 +84,6 @@ pub struct ChatMessage {
/// Fetch offline messages from the companion HTTP service.
pub async fn fetch_offline(
host: &str,
- topic: &str,
passphrase: &str,
since: i64,
) -> Result<Vec<ChatMessage>, String> {
@@ -94,7 +91,7 @@ pub async fn fetch_offline(
"http://{}:{}/messages?topic={}&since={}",
host,
config::OFFLINE_SERVICE_PORT,
- topic,
+ &config::TOPIC,
since
);
To be clear, using the same MQTT topic for every user isn't quite a panacea. An evil broker could still figure out the social graph by watching who posts around the same time. But it hides the identity of lurkers, and, as I pointed out before, it requires -11 lines of code on net.