From 2ca2bafd61090e7dedc7649f506b7eb309ef6b05 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 03:39:34 +0200 Subject: [PATCH 1/7] Not sending to sender --- src/server.rs | 2 -- src/vc_server.rs | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server.rs b/src/server.rs index 39895f4..32a0fa6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -106,8 +106,6 @@ impl Server { if let Err(e) = read_loop(&s, public_key, &client).await { eprintln!("Failed to handle client: {e}"); - } else { - println!("Client connection closed") } let mut clients_meta = s.clients.lock().await; diff --git a/src/vc_server.rs b/src/vc_server.rs index 5a0a946..8801aef 100644 --- a/src/vc_server.rs +++ b/src/vc_server.rs @@ -121,6 +121,10 @@ impl Server { voice.last_speaking_sent = now; } + if *sender == user.public_key { + continue; + } + let _ = self.udp_send_to(&voice.addr, payload).await; } From 67215c4af05de5a7b1f2e7ce9ad3e76dc813833d Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 03:42:13 +0200 Subject: [PATCH 2/7] Removed public hostname --- src/data/config.rs | 3 --- src/protocol/initialize.rs | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/data/config.rs b/src/data/config.rs index 5a8693e..af358f8 100644 --- a/src/data/config.rs +++ b/src/data/config.rs @@ -8,7 +8,6 @@ use crate::types::{Channel, ChannelKind, ServerMeta}; pub struct Config { pub meta: ServerMeta, pub port: u16, - pub public_hostname: String, pub hostnames: HashSet, } @@ -49,8 +48,6 @@ impl Config { port: 3415, - public_hostname: "localhost:3415".to_string(), - hostnames: HashSet::from_iter(["localhost:3415".to_string()]), } } diff --git a/src/protocol/initialize.rs b/src/protocol/initialize.rs index f5e4778..264b839 100644 --- a/src/protocol/initialize.rs +++ b/src/protocol/initialize.rs @@ -48,11 +48,10 @@ impl UserConnections { return Err(anyhow::anyhow!("Client tampstamp wasn't correct")); } - if hostname != server.config.public_hostname || !server.config.hostnames.contains(&hostname) - { + if !server.config.hostnames.contains(&hostname) { socket.send( &ClientMethod::Error { - error: Cow::Owned(format!("Invalid Hostname, to avoid man-in-the-middle attacks, please use the correct hostname: {}", server.config.public_hostname)), + error: Cow::Owned(format!("Invalid Hostname, to avoid man-in-the-middle attacks, please use the correct hostname(s): {}", server.config.hostnames.clone().into_iter().collect::>().join(", "))), }, ) .await?; From 7a21c71817cf3d1d3d837be25e9b90f867ce0ad7 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 12:57:07 +0200 Subject: [PATCH 3/7] Sending pubkey --- src/protocol/initialize.rs | 11 +++++++++++ src/ws.rs | 6 +++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/protocol/initialize.rs b/src/protocol/initialize.rs index 264b839..b27f5cc 100644 --- a/src/protocol/initialize.rs +++ b/src/protocol/initialize.rs @@ -4,6 +4,7 @@ use std::{ }; use ed25519_dalek::{Signer, VerifyingKey}; +use futures_util::SinkExt; use crate::{server::Server, ws::EnclaveWebSocket}; @@ -15,6 +16,16 @@ impl UserConnections { server: &Arc, socket: Arc, ) -> anyhow::Result<(Arc, VerifyingKey, ClientMeta)> { + { + socket + .tx() + .await + .send(axum::extract::ws::Message::Binary( + server.key.verifying_key().to_bytes().to_vec().into(), + )) + .await?; + } + let Some(ServerMethod::Initialize { public_key: public_key_string, signature, diff --git a/src/ws.rs b/src/ws.rs index de5e692..225c9c1 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -5,7 +5,7 @@ use futures_util::{ SinkExt, StreamExt, stream::{SplitSink, SplitStream}, }; -use tokio::sync::Mutex; +use tokio::sync::{Mutex, MutexGuard}; use crate::protocol::{ClientMethod, ServerMethod}; @@ -62,4 +62,8 @@ impl EnclaveWebSocket { Ok(()) } + + pub async fn tx(&self) -> MutexGuard<'_, SplitSink> { + self.tx.lock().await + } } From 9d59da3a4e6d97666a5c135ba46c707036183675 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 13:39:26 +0200 Subject: [PATCH 4/7] crypto handshake and encryption --- Cargo.lock | 221 +++++++++++++++++++++++++++++++++++-- Cargo.toml | 4 + src/crypto.rs | 121 +++++++++++++++++++- src/protocol/initialize.rs | 17 +-- src/server.rs | 20 +++- src/ws.rs | 42 +++++-- 6 files changed, 383 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28940fa..18fd268 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + [[package]] name = "ahash" version = "0.8.12" @@ -108,6 +118,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bs58" version = "0.5.1" @@ -145,12 +164,58 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.3.1", +] + +[[package]] +name = "chacha20poly1305" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b89e1c441e926b9c82a8d023f6e1b7ae0adcfaa7d621814e4d60789bac751cb" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -160,6 +225,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -170,6 +244,26 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.3", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -177,10 +271,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.10.7", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5eed333089e2e1c1ac8c6c0398e5e2497b4c9926ca6d0365ed1e099afa5bc23" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "curve25519-dalek-derive", + "fiat-crypto 0.3.0", "rustc_version", "subtle", "zeroize", @@ -209,7 +318,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] @@ -219,8 +328,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", ] [[package]] @@ -239,11 +359,11 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "ed25519", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -255,15 +375,19 @@ dependencies = [ "anyhow", "axum", "bs58", + "chacha20poly1305", + "curve25519-dalek 5.0.0", "ed25519-dalek", "futures-util", "rand 0.8.7", "rusqlite", "serde", "serde_json", + "sha2 0.11.0", "tokio", "tower-http", "uuid", + "x25519-dalek", ] [[package]] @@ -284,6 +408,12 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "find-msvc-tools" version = "0.1.11" @@ -393,6 +523,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -464,6 +595,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.0" @@ -499,6 +639,15 @@ dependencies = [ "tower-service", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "itoa" version = "1.0.18" @@ -612,6 +761,16 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2d0073b297041425c7c3df6eb4792d598a15323fe63346852b092eca02904c" +dependencies = [ + "cpufeatures 0.3.1", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -710,6 +869,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rusqlite" version = "0.31.0" @@ -824,8 +989,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -835,8 +1000,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", ] [[package]] @@ -1113,6 +1289,16 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "uuid" version = "1.24.1" @@ -1217,6 +1403,17 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "x25519-dalek" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e8131a03190127fb2263afc72b322ecadae46b6ff8c6f399ff5d02f5559af6" +dependencies = [ + "curve25519-dalek 5.0.0", + "rand_core 0.10.1", + "zeroize", +] + [[package]] name = "zerocopy" version = "0.8.56" diff --git a/Cargo.toml b/Cargo.toml index 2d0a27a..4b8d01a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,7 @@ tower-http = { version = "0.7.0", features = ["fs", "cors"] } rusqlite = { version = "0.31", features = ["bundled"] } uuid = { version = "1.24.1", features = ["v4"] } futures-util = "0.3.34" +x25519-dalek = { version = "3.0.0", features = ["static_secrets"] } +curve25519-dalek = "5.0.0" +sha2 = "0.11.0" +chacha20poly1305 = "0.11.0" diff --git a/src/crypto.rs b/src/crypto.rs index 3dd0753..31cdbed 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,7 +1,17 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; +use axum::extract::ws::WebSocket; +use chacha20poly1305::{ + ChaCha20Poly1305, Key, Nonce, + aead::{Aead, KeyInit}, +}; +use curve25519_dalek::edwards::CompressedEdwardsY; use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; use rand::rngs::OsRng; +use sha2::{Digest, Sha512}; +use x25519_dalek::{PublicKey as X25519Public, SharedSecret, StaticSecret as X25519Secret}; + +use crate::{server::Server, ws::EnclaveWebSocket}; pub async fn get() -> anyhow::Result { let private_key_path = PathBuf::from("private.key"); @@ -45,3 +55,112 @@ pub fn from_string_sig(signature: &str) -> anyhow::Result { .map_err(|_| anyhow::anyhow!("Invalid signature"))?, )) } + +pub fn ed25519_signing_key_to_x25519(signing_key: &SigningKey) -> X25519Secret { + let hash = Sha512::digest(signing_key.as_bytes()); + let mut scalar_bytes = [0u8; 32]; + scalar_bytes.copy_from_slice(&hash[..32]); + X25519Secret::from(scalar_bytes) // clamping happens internally +} + +pub fn ed25519_verifying_key_to_x25519(verifying_key: &VerifyingKey) -> Option { + let compressed = CompressedEdwardsY(verifying_key.to_bytes()); + let edwards_point = compressed.decompress()?; + Some(X25519Public::from(edwards_point.to_montgomery().to_bytes())) +} + +pub struct SessionCipher { + cipher: ChaCha20Poly1305, + send_counter: u64, + recv_counter: u64, +} + +impl SessionCipher { + pub fn new(shared_secret: &SharedSecret) -> anyhow::Result { + // SharedSecret's raw bytes ARE suitable for direct use as a ChaCha20Poly1305 key + // (both are 32 bytes), though in a hardened design you'd typically run this + // through a KDF (e.g. HKDF) rather than using the raw ECDH output directly. + let key = Key::try_from(shared_secret.as_bytes().as_slice())?; + + Ok(Self { + cipher: ChaCha20Poly1305::new(&key), + send_counter: 0, + recv_counter: 0, + }) + } + + pub fn next_send_nonce(&mut self) -> [u8; 12] { + let mut nonce = [0u8; 12]; + nonce[..8].copy_from_slice(&self.send_counter.to_be_bytes()); + // top bit distinguishes "send" direction from "recv" direction, + // so client-send and server-send counters never collide even if + // both happened to reach the same numeric value + nonce[11] |= 0b1000_0000; + self.send_counter += 1; + nonce + } + + pub fn next_recv_nonce(&mut self) -> [u8; 12] { + let mut nonce = [0u8; 12]; + nonce[..8].copy_from_slice(&self.recv_counter.to_be_bytes()); + self.recv_counter += 1; + nonce + } + + pub fn encrypt(&mut self, plaintext: &[u8]) -> anyhow::Result> { + let nonce_bytes = self.next_send_nonce(); + let nonce = Nonce::try_from(nonce_bytes)?; + + let ciphertext = self + .cipher + .encrypt(&nonce, plaintext) + .map_err(|_| anyhow::anyhow!("encryption failed"))?; + + // prepend the nonce so the other side can reconstruct it on decrypt + let mut out = nonce_bytes.to_vec(); + out.extend(ciphertext); + Ok(out) + } + + pub fn decrypt(&mut self, data: &[u8]) -> anyhow::Result> { + if data.len() < 12 { + anyhow::bail!("message too short to contain a nonce"); + } + let (nonce_bytes, ciphertext) = data.split_at(12); + let nonce = Nonce::try_from(nonce_bytes)?; + + self.cipher + .decrypt(&nonce, ciphertext) + .map_err(|_| anyhow::anyhow!("decryption failed")) + } +} + +pub async fn crypto_handshake( + server: &Arc, + mut socket: WebSocket, +) -> anyhow::Result> { + socket + .send(axum::extract::ws::Message::Binary( + server.x_keypair.0.to_bytes().to_vec().into(), + )) + .await?; + + let axum::extract::ws::Message::Binary(raw_pubkey) = socket + .recv() + .await + .transpose()? + .ok_or(anyhow::anyhow!("Failed to get client x key"))? + else { + return Err(anyhow::anyhow!("")); + }; + + let client_pubkey = X25519Public::from(*raw_pubkey.as_array().ok_or(anyhow::anyhow!( + "Failed to get proper length of client x key" + ))?); + + let shared_secret = server.x_keypair.1.diffie_hellman(&client_pubkey); + + let cipher = SessionCipher::new(&shared_secret)?; + + Ok(Arc::new(EnclaveWebSocket::new(socket, cipher))) +} diff --git a/src/protocol/initialize.rs b/src/protocol/initialize.rs index b27f5cc..93b9f85 100644 --- a/src/protocol/initialize.rs +++ b/src/protocol/initialize.rs @@ -4,7 +4,6 @@ use std::{ }; use ed25519_dalek::{Signer, VerifyingKey}; -use futures_util::SinkExt; use crate::{server::Server, ws::EnclaveWebSocket}; @@ -14,18 +13,8 @@ use crate::server::UserConnections; impl UserConnections { pub async fn initialize( server: &Arc, - socket: Arc, - ) -> anyhow::Result<(Arc, VerifyingKey, ClientMeta)> { - { - socket - .tx() - .await - .send(axum::extract::ws::Message::Binary( - server.key.verifying_key().to_bytes().to_vec().into(), - )) - .await?; - } - + socket: &Arc, + ) -> anyhow::Result<(VerifyingKey, ClientMeta)> { let Some(ServerMethod::Initialize { public_key: public_key_string, signature, @@ -122,6 +111,6 @@ impl UserConnections { )); }; - Ok((socket, public_key, meta)) + Ok((public_key, meta)) } } diff --git a/src/server.rs b/src/server.rs index 32a0fa6..8436298 100644 --- a/src/server.rs +++ b/src/server.rs @@ -21,8 +21,8 @@ use crate::{ data::{config::Config, messages::MessageStore, users::UserMetaStore}, protocol::{ClientMethod, read_loop}, types::ClientMeta, - ws::EnclaveWebSocket, }; +use x25519_dalek::{PublicKey as X25519Public, StaticSecret as X25519Secret}; pub struct VoiceConnection { pub addr: SocketAddr, @@ -40,6 +40,7 @@ pub struct UserConnections { pub struct Server { pub key: SigningKey, + pub x_keypair: (X25519Public, X25519Secret), pub config: Config, pub clients: Mutex>>, pub voice_pins: Mutex>, @@ -50,8 +51,14 @@ pub struct Server { impl Server { pub async fn new() -> anyhow::Result> { + let key = crate::crypto::get().await?; Ok(Arc::new(Self { - key: crate::crypto::get().await?, + x_keypair: ( + crate::crypto::ed25519_verifying_key_to_x25519(&key.verifying_key()) + .ok_or(anyhow::anyhow!("Failed to convert ed pubkey to x"))?, + crate::crypto::ed25519_signing_key_to_x25519(&key), + ), + key, config: Config::get().await?, clients: Mutex::new(HashMap::new()), voice_pins: Mutex::new(HashMap::new()), @@ -67,8 +74,13 @@ impl Server { let s = self.clone(); ws.on_upgrade(move |socket: WebSocket| async move { - match UserConnections::initialize(&s, Arc::new(EnclaveWebSocket::new(socket))).await { - Ok((client, public_key, meta)) => { + let Ok(client) = crate::crypto::crypto_handshake(&s, socket).await else { + eprintln!("Filed to initialize crypto"); + return; + }; + + match UserConnections::initialize(&s, &client).await { + Ok((public_key, meta)) => { if let Err(e) = s .user_store .upsert_user(&crate::crypto::to_string(&public_key), &meta) diff --git a/src/ws.rs b/src/ws.rs index 225c9c1..07f2c8a 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -1,26 +1,31 @@ use std::borrow::Cow; -use axum::extract::ws::{Message, Utf8Bytes, WebSocket}; +use axum::extract::ws::{Message, WebSocket}; use futures_util::{ SinkExt, StreamExt, stream::{SplitSink, SplitStream}, }; -use tokio::sync::{Mutex, MutexGuard}; +use tokio::sync::Mutex; -use crate::protocol::{ClientMethod, ServerMethod}; +use crate::{ + crypto::SessionCipher, + protocol::{ClientMethod, ServerMethod}, +}; pub struct EnclaveWebSocket { tx: Mutex>, rx: Mutex>, + cihper: Mutex, } impl EnclaveWebSocket { - pub fn new(ws: WebSocket) -> Self { + pub fn new(ws: WebSocket, cipher: SessionCipher) -> Self { let (tx, rx) = ws.split(); Self { tx: Mutex::new(tx), rx: Mutex::new(rx), + cihper: Mutex::new(cipher), } } @@ -39,6 +44,23 @@ impl EnclaveWebSocket { } }, + Some(Message::Binary(encrypted)) => { + let text = String::from_utf8(self.cihper.lock().await.decrypt(&encrypted)?)?; + + match serde_json::from_str(&text.to_string()) { + Ok(msg) => Ok(Some(msg)), + + Err(e) => { + self.send(&ClientMethod::Error { + error: Cow::Owned(format!("Unable to parse message: {e}")), + }) + .await?; + + Ok(None) + } + } + } + Some(Message::Ping(v)) => { self.tx.lock().await.send(Message::Pong(v)).await?; @@ -52,18 +74,16 @@ impl EnclaveWebSocket { } pub async fn send(&self, message: &ClientMethod) -> anyhow::Result<()> { + let text = serde_json::to_string(message)?; + + let encrypted = self.cihper.lock().await.encrypt(text.as_bytes())?; + self.tx .lock() .await - .send(Message::Text(Utf8Bytes::from(serde_json::to_string( - message, - )?))) + .send(Message::Binary(encrypted.into())) .await?; Ok(()) } - - pub async fn tx(&self) -> MutexGuard<'_, SplitSink> { - self.tx.lock().await - } } From ba17cec5e157a2c0314d8a47fb06128d062d5ad3 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 14:16:58 +0200 Subject: [PATCH 5/7] Improved a few things --- src/crypto.rs | 2 +- src/server.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index 31cdbed..b1dd985 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -151,7 +151,7 @@ pub async fn crypto_handshake( .transpose()? .ok_or(anyhow::anyhow!("Failed to get client x key"))? else { - return Err(anyhow::anyhow!("")); + return Err(anyhow::anyhow!("WS message is not binary")); }; let client_pubkey = X25519Public::from(*raw_pubkey.as_array().ok_or(anyhow::anyhow!( diff --git a/src/server.rs b/src/server.rs index 8436298..083d01f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -74,9 +74,12 @@ impl Server { let s = self.clone(); ws.on_upgrade(move |socket: WebSocket| async move { - let Ok(client) = crate::crypto::crypto_handshake(&s, socket).await else { - eprintln!("Filed to initialize crypto"); - return; + let client = match crate::crypto::crypto_handshake(&s, socket).await { + Ok(client) => client, + Err(err) => { + eprintln!("Failed to initialize crypto: {err}"); + return; + } }; match UserConnections::initialize(&s, &client).await { From a60979579a62e334eeda415b1710ed694be774a0 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 14:30:15 +0200 Subject: [PATCH 6/7] Improved cipher storage --- src/crypto.rs | 7 ++++--- src/protocol/initialize.rs | 2 +- src/server.rs | 9 ++++++++- src/ws.rs | 12 ++++++------ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/crypto.rs b/src/crypto.rs index b1dd985..a848622 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -9,6 +9,7 @@ use curve25519_dalek::edwards::CompressedEdwardsY; use ed25519_dalek::{Signature, SigningKey, VerifyingKey}; use rand::rngs::OsRng; use sha2::{Digest, Sha512}; +use tokio::sync::Mutex; use x25519_dalek::{PublicKey as X25519Public, SharedSecret, StaticSecret as X25519Secret}; use crate::{server::Server, ws::EnclaveWebSocket}; @@ -138,7 +139,7 @@ impl SessionCipher { pub async fn crypto_handshake( server: &Arc, mut socket: WebSocket, -) -> anyhow::Result> { +) -> anyhow::Result { socket .send(axum::extract::ws::Message::Binary( server.x_keypair.0.to_bytes().to_vec().into(), @@ -160,7 +161,7 @@ pub async fn crypto_handshake( let shared_secret = server.x_keypair.1.diffie_hellman(&client_pubkey); - let cipher = SessionCipher::new(&shared_secret)?; + let cipher = Arc::new(Mutex::new(SessionCipher::new(&shared_secret)?)); - Ok(Arc::new(EnclaveWebSocket::new(socket, cipher))) + Ok(EnclaveWebSocket::new(socket, cipher)) } diff --git a/src/protocol/initialize.rs b/src/protocol/initialize.rs index 93b9f85..cb97f8d 100644 --- a/src/protocol/initialize.rs +++ b/src/protocol/initialize.rs @@ -13,7 +13,7 @@ use crate::server::UserConnections; impl UserConnections { pub async fn initialize( server: &Arc, - socket: &Arc, + socket: &EnclaveWebSocket, ) -> anyhow::Result<(VerifyingKey, ClientMeta)> { let Some(ServerMethod::Initialize { public_key: public_key_string, diff --git a/src/server.rs b/src/server.rs index 083d01f..201f93e 100644 --- a/src/server.rs +++ b/src/server.rs @@ -18,6 +18,7 @@ use tokio::{ }; use crate::{ + crypto::SessionCipher, data::{config::Config, messages::MessageStore, users::UserMetaStore}, protocol::{ClientMethod, read_loop}, types::ClientMeta, @@ -35,6 +36,7 @@ pub struct UserConnections { pub counter: AtomicU16, pub public_key: VerifyingKey, pub connections: Mutex>>, + pub cihper: Arc>, pub voice: Mutex>, } @@ -74,7 +76,7 @@ impl Server { let s = self.clone(); ws.on_upgrade(move |socket: WebSocket| async move { - let client = match crate::crypto::crypto_handshake(&s, socket).await { + let mut client = match crate::crypto::crypto_handshake(&s, socket).await { Ok(client) => client, Err(err) => { eprintln!("Failed to initialize crypto: {err}"); @@ -103,10 +105,15 @@ impl Server { counter: AtomicU16::new(0), connections: Mutex::new(HashMap::new()), voice: Mutex::new(None), + cihper: client.cipher.clone(), }) }) .clone(); + client.cipher = clients.cihper.clone(); + + let client = Arc::new(client); + let conid = clients .counter .fetch_add(1, std::sync::atomic::Ordering::Relaxed); diff --git a/src/ws.rs b/src/ws.rs index 07f2c8a..efae90e 100644 --- a/src/ws.rs +++ b/src/ws.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use axum::extract::ws::{Message, WebSocket}; use futures_util::{ @@ -15,17 +15,17 @@ use crate::{ pub struct EnclaveWebSocket { tx: Mutex>, rx: Mutex>, - cihper: Mutex, + pub cipher: Arc>, } impl EnclaveWebSocket { - pub fn new(ws: WebSocket, cipher: SessionCipher) -> Self { + pub fn new(ws: WebSocket, cipher: Arc>) -> Self { let (tx, rx) = ws.split(); Self { tx: Mutex::new(tx), rx: Mutex::new(rx), - cihper: Mutex::new(cipher), + cipher, } } @@ -45,7 +45,7 @@ impl EnclaveWebSocket { }, Some(Message::Binary(encrypted)) => { - let text = String::from_utf8(self.cihper.lock().await.decrypt(&encrypted)?)?; + let text = String::from_utf8(self.cipher.lock().await.decrypt(&encrypted)?)?; match serde_json::from_str(&text.to_string()) { Ok(msg) => Ok(Some(msg)), @@ -76,7 +76,7 @@ impl EnclaveWebSocket { pub async fn send(&self, message: &ClientMethod) -> anyhow::Result<()> { let text = serde_json::to_string(message)?; - let encrypted = self.cihper.lock().await.encrypt(text.as_bytes())?; + let encrypted = self.cipher.lock().await.encrypt(text.as_bytes())?; self.tx .lock() From 281238f8e1fb43272d64f036de883c8fa5f35ff4 Mon Sep 17 00:00:00 2001 From: Klesti Selimaj Date: Fri, 28 Aug 2026 15:20:08 +0200 Subject: [PATCH 7/7] Crypto in vc --- src/vc_server.rs | 52 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/src/vc_server.rs b/src/vc_server.rs index 8801aef..8da599f 100644 --- a/src/vc_server.rs +++ b/src/vc_server.rs @@ -2,9 +2,9 @@ use std::{net::SocketAddr, sync::Arc}; use anyhow::Context; use ed25519_dalek::VerifyingKey; -use tokio::net::UdpSocket; +use tokio::{net::UdpSocket, sync::Mutex}; -use crate::{protocol::ClientMethod, server::Server}; +use crate::{crypto::SessionCipher, protocol::ClientMethod, server::Server}; use tokio::time::Instant; @@ -31,16 +31,18 @@ impl Server { let pin_bytes: [u8; 8] = buf[..8].try_into().unwrap(); let pin = u64::from_be_bytes(pin_bytes); - let (sender_pubkey, channel_id, payload) = { + // is_first_packet distinguishes the plaintext pin-bootstrap packet + // from subsequent encrypted audio packets. + let (sender_pubkey, channel_id, payload, is_first_packet) = { let mut pins = self.voice_pins.lock().await; if let Some((pubkey, channel_id)) = pins.remove(&pin) { - (pubkey, channel_id, &buf[8..len]) + (pubkey, channel_id, &buf[8..len], true) } else { drop(pins); match self.find_voice_sender(&addr).await { - Some((pubkey, channel_id)) => (pubkey, channel_id, &buf[..]), + Some((pubkey, channel_id)) => (pubkey, channel_id, &buf[..len], false), None => { continue; } @@ -69,8 +71,31 @@ impl Server { drop(voice); - self.relay_voice(&sender_pubkey, &channel_id, payload) - .await?; + // The pin-bearing bootstrap packet carries no payload to decrypt — + // it's purely "here's my pin, bind my address." Everything after + // this first packet is the real, encrypted audio stream. + if is_first_packet { + continue; + } + + let decrypted_payload = match user.cihper.lock().await.decrypt(payload) { + Ok(pt) => pt, + Err(e) => { + eprintln!("[vc] dropping packet: decryption failed: {e}"); + continue; + } + }; + + let s = self.clone(); + + tokio::spawn(async move { + if let Err(e) = s + .relay_voice(&sender_pubkey, &channel_id, &decrypted_payload) + .await + { + eprintln!("{e}"); + } + }); } } @@ -125,7 +150,7 @@ impl Server { continue; } - let _ = self.udp_send_to(&voice.addr, payload).await; + let _ = self.udp_send_to(&user.cihper, &voice.addr, payload).await; } Ok(()) @@ -137,10 +162,17 @@ impl Server { .context("Failed to get voice socket") } - pub async fn udp_send_to(&self, addr: &SocketAddr, payload: &[u8]) -> anyhow::Result<()> { + pub async fn udp_send_to( + &self, + cipher: &Arc>, + addr: &SocketAddr, + payload: &[u8], + ) -> anyhow::Result<()> { let socket = self.get_voice_socket()?; - socket.send_to(payload, addr).await?; + socket + .send_to(&cipher.lock().await.encrypt(payload)?, addr) + .await?; Ok(()) }