fix: WASM double-X3DH bug, federated aliases, deploy tooling
WASM fix (critical):
- encrypt_key_exchange_with_id was calling x3dh::initiate a second time,
generating a new ephemeral key that didn't match the ratchet — receiver
always failed to decrypt. Now stores X3DH result from initiate() and
reuses it. Added 2 protocol tests confirming the fix + the bug.
- Bumped service worker cache to wz-v2 to force browsers to re-fetch.
- Disabled wasm-opt for Hetzner builds (libc compat issue).
Federation — alias support:
- resolve_alias falls back to federation peer if not found locally
- register_alias checks peer server before allowing — globally unique aliases
- Added resolve_remote_alias() and is_alias_taken_remote() to FederationHandle
Federation — key proxy fix:
- Remote bundles no longer cached locally (stale cache caused decrypt failures)
- Local vs remote determined by device: prefix in keys DB
Client fixes:
- Self-messaging blocked ("Cannot send messages to yourself")
- /peer <self> blocked
- last_dm_peer never set to self
- /r <message> sends reply inline (switches peer + sends in one command)
Deploy tooling:
- scripts/build-linux.sh with --ship (build + deploy + destroy)
- --update-all, --status, --logs commands
- WASM rebuilt on Hetzner VM before server binary
- deploy/ directory: systemd service, federation configs, setup script
- Journald log cap (50MB, 7-day retention)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ impl App {
|
||||
db: &LocalDb,
|
||||
client: &ServerClient,
|
||||
) {
|
||||
let text = self.input.trim().to_string();
|
||||
let mut text = self.input.trim().to_string();
|
||||
self.input.clear();
|
||||
self.cursor_pos = 0;
|
||||
|
||||
@@ -223,15 +223,27 @@ impl App {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if text == "/r" || text == "/reply" {
|
||||
if text == "/r" || text == "/reply" || text.starts_with("/r ") || text.starts_with("/reply ") {
|
||||
let last = self.last_dm_peer.lock().unwrap().clone();
|
||||
if let Some(ref peer) = last {
|
||||
self.peer_fp = Some(peer.clone());
|
||||
self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
|
||||
// If there's a message after /r, mutate text and fall through to send
|
||||
let reply_msg = if text.starts_with("/reply ") {
|
||||
text[7..].trim().to_string()
|
||||
} else if text.starts_with("/r ") {
|
||||
text[3..].trim().to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
if reply_msg.is_empty() {
|
||||
return; // Just switch peer
|
||||
}
|
||||
text = reply_msg; // Fall through to send logic below
|
||||
} else {
|
||||
self.add_message(ChatLine { sender: "system".into(), text: "No one to reply to".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if text.starts_with("/peer ") || text.starts_with("/p ") {
|
||||
let text = if text.starts_with("/p ") { format!("/peer {}", &text[3..]) } else { text.clone() };
|
||||
@@ -244,6 +256,10 @@ impl App {
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
if normfp(&fp) == normfp(&self.our_fp) {
|
||||
self.add_message(ChatLine { sender: "system".into(), text: "Cannot set yourself as peer".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
|
||||
return;
|
||||
}
|
||||
self.add_message(ChatLine {
|
||||
sender: "system".into(),
|
||||
text: format!("Peer set to {}", fp),
|
||||
@@ -355,6 +371,18 @@ impl App {
|
||||
}
|
||||
};
|
||||
|
||||
// Prevent self-messaging (causes ratchet corruption)
|
||||
if normfp(&peer) == normfp(&self.our_fp) {
|
||||
self.add_message(ChatLine {
|
||||
sender: "system".into(),
|
||||
text: "Cannot send messages to yourself".into(),
|
||||
is_system: true,
|
||||
is_self: false,
|
||||
message_id: None, timestamp: Local::now(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let peer_fp = match Fingerprint::from_hex(&peer) {
|
||||
Ok(fp) => fp,
|
||||
Err(_) => {
|
||||
|
||||
@@ -112,7 +112,9 @@ fn process_wire_message(
|
||||
Ok(plaintext) => {
|
||||
let text = String::from_utf8_lossy(&plaintext).to_string();
|
||||
let _ = db.save_session(&sender_fp, &state);
|
||||
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
|
||||
if normfp(&sender_fingerprint) != normfp(our_fp) {
|
||||
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
|
||||
}
|
||||
store_received(db, &sender_fingerprint, &text);
|
||||
messages.lock().unwrap().push(ChatLine {
|
||||
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
||||
@@ -159,7 +161,9 @@ fn process_wire_message(
|
||||
Ok(plaintext) => {
|
||||
let text = String::from_utf8_lossy(&plaintext).to_string();
|
||||
let _ = db.save_session(&sender_fp, &state);
|
||||
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
|
||||
if normfp(&sender_fingerprint) != normfp(our_fp) {
|
||||
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
|
||||
}
|
||||
store_received(db, &sender_fingerprint, &text);
|
||||
messages.lock().unwrap().push(ChatLine {
|
||||
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
|
||||
|
||||
Reference in New Issue
Block a user