Crypto in vc

This commit is contained in:
2026-08-28 15:20:38 +02:00
parent cd564bec99
commit d5dc9d82d7
7 changed files with 246 additions and 21 deletions
+39 -16
View File
@@ -9,6 +9,7 @@ use std::{
time::{Duration, Instant},
};
use chacha20poly1305::{aead::KeyInit, ChaCha20Poly1305, Key};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use ringbuf::{
storage::Heap,
@@ -17,11 +18,10 @@ use ringbuf::{
};
use tauri::State;
use crate::commands::config::ConfigState;
use crate::{commands::config::ConfigState, crypto::SessionCipher};
const TARGET_SAMPLE_RATE: u32 = 48_000;
const PACKET_SAMPLES: usize = 960; // 20ms @ 48kHz mono
const HEADER_SIZE: usize = 4;
const PACKET_SAMPLES: usize = 960;
const MAX_PACKET_SIZE: usize = 4096;
const INITIAL_PACKET_CUSHION: usize = 3;
@@ -345,11 +345,15 @@ pub fn disconnect_from_vc(voice_state: State<'_, VoiceState>) -> Result<(), Stri
pub fn connect_to_vc(
hostname: String,
pin: u64,
shared_secret: Vec<u8>, // output of js `x25519.getSharedSecret`
config_state: State<'_, ConfigState>,
voice_state: State<'_, VoiceState>,
) -> Result<(), String> {
disconnect_from_vc(voice_state.clone())?;
let key = Key::try_from(shared_secret.as_slice()).map_err(|v| v.to_string())?;
let cipher = Arc::new(Mutex::new(SessionCipher::new(ChaCha20Poly1305::new(&key))));
let config = config_state.0.lock().unwrap().clone();
let state_inner = Arc::clone(&voice_state.inner);
@@ -444,10 +448,10 @@ pub fn connect_to_vc(
{
let input_socket = socket.clone();
let shutdown = shutdown.clone();
let cipher = cipher.clone();
thread::spawn(move || {
let mut sequence = 0u32;
let mut net_packet = vec![0u8; HEADER_SIZE + PACKET_SAMPLES * 2];
let mut frame_buf = vec![0.0f32; PACKET_SAMPLES];
let mut hangover_counter = 0;
@@ -469,13 +473,21 @@ pub fn connect_to_vc(
};
if is_speaking {
net_packet[0..4].copy_from_slice(&sequence.to_be_bytes());
for (i, sample) in frame_buf.iter().enumerate() {
// sequence goes INSIDE the plaintext now, prefixed before the PCM
let mut plaintext = Vec::with_capacity(4 + PACKET_SAMPLES * 2);
plaintext.extend_from_slice(&sequence.to_be_bytes());
for sample in frame_buf.iter() {
let pcm = (sample.clamp(-1.0, 1.0) * 32767.0) as i16;
let offset = HEADER_SIZE + i * 2;
net_packet[offset..offset + 2].copy_from_slice(&pcm.to_be_bytes());
plaintext.extend_from_slice(&pcm.to_be_bytes());
}
let Ok(net_packet) = cipher.lock().unwrap().encrypt(&plaintext) else {
eprintln!("[vc] failed to encrypt outgoing packet");
sequence = sequence.wrapping_add(1);
continue;
};
let _ = input_socket.send(&net_packet);
sequence = sequence.wrapping_add(1);
}
@@ -490,6 +502,7 @@ pub fn connect_to_vc(
{
let socket = socket.clone();
let shutdown = shutdown.clone();
let cipher = cipher.clone();
thread::spawn(move || {
let mut packets: BTreeMap<u32, Vec<f32>> = BTreeMap::new();
@@ -501,15 +514,25 @@ pub fn connect_to_vc(
while !shutdown.load(Ordering::Relaxed) {
if let Ok(len) = socket.recv(&mut udp_buffer) {
if len > HEADER_SIZE {
let seq = u32::from_be_bytes(udp_buffer[0..4].try_into().unwrap());
let pcm = &udp_buffer[HEADER_SIZE..len];
let samples: Vec<f32> = pcm
.chunks_exact(2)
.map(|c| i16::from_be_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect();
match cipher.lock().unwrap().decrypt(&udp_buffer[..len]) {
Ok(plaintext) => {
if plaintext.len() < 4 {
eprintln!("[vc] dropped packet: too short after decrypt");
} else {
let seq = u32::from_be_bytes(plaintext[..4].try_into().unwrap());
let pcm = &plaintext[4..];
packets.insert(seq, samples);
let samples: Vec<f32> = pcm
.chunks_exact(2)
.map(|c| i16::from_be_bytes([c[0], c[1]]) as f32 / 32768.0)
.collect();
packets.insert(seq, samples);
}
}
Err(_) => {
eprintln!("[vc] dropped packet: decryption failed");
}
}
}
+62
View File
@@ -0,0 +1,62 @@
use chacha20poly1305::{aead::Aead, ChaCha20Poly1305, Nonce};
pub struct SessionCipher {
cipher: ChaCha20Poly1305,
send_counter: u64,
recv_counter: u64,
}
impl SessionCipher {
pub fn new(cipher: ChaCha20Poly1305) -> Self {
Self {
cipher,
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]) -> Result<Vec<u8>, String> {
let nonce_bytes = self.next_send_nonce();
let nonce = Nonce::try_from(nonce_bytes).map_err(|v| v.to_string())?;
let ciphertext = self
.cipher
.encrypt(&nonce, plaintext)
.map_err(|v| v.to_string())?;
// 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]) -> Result<Vec<u8>, String> {
if data.len() < 12 {
return Err("message too short to contain a nonce".to_string());
}
let (nonce_bytes, ciphertext) = data.split_at(12);
let nonce = Nonce::try_from(nonce_bytes).map_err(|v| v.to_string())?;
self.cipher
.decrypt(&nonce, ciphertext)
.map_err(|v| v.to_string())
}
}
+1
View File
@@ -3,6 +3,7 @@ use std::sync::Mutex;
use crate::commands::config::ConfigState;
pub mod commands;
pub mod crypto;
pub mod types;
#[cfg_attr(mobile, tauri::mobile_entry_point)]