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:
Siavash Sameni
2026-03-28 22:59:19 +04:00
parent f8eaf30bb4
commit dbf5d136cf
16 changed files with 1026 additions and 24 deletions

View File

@@ -152,6 +152,13 @@ async fn register_alias(
delete_alias_record(&state.db.aliases, &existing)?;
}
// Check if alias is taken on federation peer (globally unique)
if let Some(ref federation) = state.federation {
if federation.is_alias_taken_remote(&alias).await {
return Ok(Json(serde_json::json!({ "error": "alias already taken on federated server" })));
}
}
// Remove old alias for this fingerprint (one alias per person)
if let Some(old_alias_bytes) = state.db.aliases.get(format!("fp:{}", fp).as_bytes())? {
let old_alias = String::from_utf8_lossy(&old_alias_bytes).to_string();
@@ -292,7 +299,20 @@ async fn resolve_alias(
})))
}
}
None => Ok(Json(serde_json::json!({ "error": "alias not found" }))),
None => {
// Try federation peer
if let Some(ref federation) = state.federation {
if let Some(fp) = federation.resolve_remote_alias(&alias).await {
tracing::info!("Alias @{} resolved via federation: {}", alias, fp);
return Ok(Json(serde_json::json!({
"alias": alias,
"fingerprint": fp,
"federated": true,
})));
}
}
Ok(Json(serde_json::json!({ "error": "alias not found" })))
}
}
}