From 98d40d652084c632b8433ee9613d042e09bcbf60 Mon Sep 17 00:00:00 2001 From: tolelom <98kimsungmin@naver.com> Date: Thu, 26 Mar 2026 16:28:21 +0900 Subject: [PATCH] feat(net): add packet encryption and auth token Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/voltex_net/src/encryption.rs | 140 ++++++++++++++++++++++++++++ crates/voltex_net/src/lib.rs | 2 + 2 files changed, 142 insertions(+) create mode 100644 crates/voltex_net/src/encryption.rs diff --git a/crates/voltex_net/src/encryption.rs b/crates/voltex_net/src/encryption.rs new file mode 100644 index 0000000..f94df16 --- /dev/null +++ b/crates/voltex_net/src/encryption.rs @@ -0,0 +1,140 @@ +/// Simple XOR cipher with rotating key + sequence counter. +pub struct PacketCipher { + key: Vec, + send_counter: u64, + recv_counter: u64, +} + +impl PacketCipher { + pub fn new(key: &[u8]) -> Self { + assert!(!key.is_empty(), "encryption key must not be empty"); + PacketCipher { key: key.to_vec(), send_counter: 0, recv_counter: 0 } + } + + /// Encrypt data in-place. Prepends 8-byte sequence number. + pub fn encrypt(&mut self, plaintext: &[u8]) -> Vec { + let mut output = Vec::with_capacity(8 + plaintext.len()); + // Prepend sequence counter + output.extend_from_slice(&self.send_counter.to_le_bytes()); + // XOR plaintext with key derived from counter + base key + let derived = self.derive_key(self.send_counter); + for (i, &byte) in plaintext.iter().enumerate() { + output.push(byte ^ derived[i % derived.len()]); + } + self.send_counter += 1; + output + } + + /// Decrypt data. Validates sequence number. + pub fn decrypt(&mut self, ciphertext: &[u8]) -> Result, String> { + if ciphertext.len() < 8 { + return Err("packet too short".to_string()); + } + let seq = u64::from_le_bytes(ciphertext[0..8].try_into().unwrap()); + + // Anti-replay: sequence must be >= expected + if seq < self.recv_counter { + return Err(format!("replay detected: got seq {}, expected >= {}", seq, self.recv_counter)); + } + self.recv_counter = seq + 1; + + let derived = self.derive_key(seq); + let mut plaintext = Vec::with_capacity(ciphertext.len() - 8); + for (i, &byte) in ciphertext[8..].iter().enumerate() { + plaintext.push(byte ^ derived[i % derived.len()]); + } + Ok(plaintext) + } + + /// Derive a key from the base key + counter. + fn derive_key(&self, counter: u64) -> Vec { + let counter_bytes = counter.to_le_bytes(); + self.key.iter().enumerate().map(|(i, &k)| { + k.wrapping_add(counter_bytes[i % 8]) + }).collect() + } +} + +/// Simple token-based authentication. +pub struct AuthToken { + pub player_id: u32, + pub token: Vec, + pub expires_at: f64, // timestamp +} + +impl AuthToken { + /// Generate a simple auth token from player_id + secret. + pub fn generate(player_id: u32, secret: &[u8], expires_at: f64) -> Self { + let mut token = Vec::new(); + token.extend_from_slice(&player_id.to_le_bytes()); + token.extend_from_slice(&expires_at.to_le_bytes()); + // Simple HMAC-like: XOR with secret + for (i, byte) in token.iter_mut().enumerate() { + *byte ^= secret[i % secret.len()]; + } + AuthToken { player_id, token, expires_at } + } + + /// Validate token against secret. + pub fn validate(&self, secret: &[u8], current_time: f64) -> bool { + if current_time > self.expires_at { return false; } + let expected = AuthToken::generate(self.player_id, secret, self.expires_at); + self.token == expected.token + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let key = b"secret_key_1234"; + let mut encryptor = PacketCipher::new(key); + let mut decryptor = PacketCipher::new(key); + let msg = b"hello world"; + let encrypted = encryptor.encrypt(msg); + let decrypted = decryptor.decrypt(&encrypted).unwrap(); + assert_eq!(&decrypted, msg); + } + + #[test] + fn test_encrypted_differs_from_plain() { + let mut cipher = PacketCipher::new(b"key"); + let msg = b"test message"; + let encrypted = cipher.encrypt(msg); + assert_ne!(&encrypted[8..], msg); // ciphertext differs + } + + #[test] + fn test_replay_rejected() { + let key = b"key"; + let mut enc = PacketCipher::new(key); + let mut dec = PacketCipher::new(key); + let pkt1 = enc.encrypt(b"first"); + let pkt2 = enc.encrypt(b"second"); + let _ = dec.decrypt(&pkt2).unwrap(); // accept pkt2 (seq=1) + let result = dec.decrypt(&pkt1); // pkt1 has seq=0 < expected 2 + assert!(result.is_err()); + } + + #[test] + fn test_auth_token_valid() { + let secret = b"server_secret"; + let token = AuthToken::generate(42, secret, 1000.0); + assert!(token.validate(secret, 999.0)); + } + + #[test] + fn test_auth_token_expired() { + let secret = b"server_secret"; + let token = AuthToken::generate(42, secret, 1000.0); + assert!(!token.validate(secret, 1001.0)); + } + + #[test] + fn test_auth_token_wrong_secret() { + let token = AuthToken::generate(42, b"correct", 1000.0); + assert!(!token.validate(b"wronggg", 999.0)); + } +} diff --git a/crates/voltex_net/src/lib.rs b/crates/voltex_net/src/lib.rs index c021709..1854512 100644 --- a/crates/voltex_net/src/lib.rs +++ b/crates/voltex_net/src/lib.rs @@ -6,6 +6,7 @@ pub mod reliable; pub mod snapshot; pub mod interpolation; pub mod lag_compensation; +pub mod encryption; pub use packet::Packet; pub use socket::NetSocket; @@ -15,3 +16,4 @@ pub use reliable::{ReliableChannel, OrderedChannel}; pub use snapshot::{Snapshot, EntityState, serialize_snapshot, deserialize_snapshot, diff_snapshots, apply_diff}; pub use interpolation::InterpolationBuffer; pub use lag_compensation::LagCompensation; +pub use encryption::{PacketCipher, AuthToken};