From b6b3e473bc0bcfa2e6d1cfc4a4eaca88f70d3aab Mon Sep 17 00:00:00 2001 From: RealHinome Date: Sun, 21 Dec 2025 16:42:19 +0100 Subject: [PATCH 1/4] Init padding --- Cargo.lock | 4 ++++ error/Cargo.toml | 4 +++- error/src/lib.rs | 2 ++ libturms/Cargo.toml | 2 ++ libturms/src/lib.rs | 1 + libturms/src/padding.rs | 52 +++++++++++++++++++++++++++++++++++++++++ p2p/src/lib.rs | 9 +++---- p2p/src/models.rs | 2 ++ p2p/src/x3dh.rs | 4 ++-- 9 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 libturms/src/padding.rs diff --git a/Cargo.lock b/Cargo.lock index 5d02b0d..92b2852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,6 +648,8 @@ name = "error" version = "0.1.0" dependencies = [ "jsonwebtoken", + "rand 0.9.1", + "rand_chacha 0.9.0", "reqwest", "serde_json", "serde_yaml", @@ -1293,6 +1295,8 @@ dependencies = [ "futures-util", "hex", "p2p", + "rand 0.9.1", + "rand_chacha 0.9.0", "serde", "serde_json", "serde_yaml", diff --git a/error/Cargo.toml b/error/Cargo.toml index a17ea5b..628c7dc 100644 --- a/error/Cargo.toml +++ b/error/Cargo.toml @@ -14,4 +14,6 @@ jsonwebtoken = { workspace = true } url = "*" tungstenite = { workspace = true } reqwest = "0.12" -webrtc = { workspace = true } \ No newline at end of file +webrtc = { workspace = true } +rand = "*" +rand_chacha = "*" diff --git a/error/src/lib.rs b/error/src/lib.rs index e2fdc42..de55c30 100644 --- a/error/src/lib.rs +++ b/error/src/lib.rs @@ -39,6 +39,8 @@ pub enum Error { AuthenticationFailed, #[error("sess-id does not exist on sdp")] MissingSessionId, + #[error(transparent)] + RandOs(#[from] rand::rand_core::OsError), } impl From for Error { diff --git a/libturms/Cargo.toml b/libturms/Cargo.toml index caf9a1f..ac3c684 100644 --- a/libturms/Cargo.toml +++ b/libturms/Cargo.toml @@ -24,6 +24,8 @@ p2p = { path = "../p2p" } error = { path = "../error" } vodozemac = "0.9" blake3 = "1.8" +rand = "0.9" +rand_chacha = "0.9" hex = "0.4" serde = { workspace = true } serde_yaml = "0.9" diff --git a/libturms/src/lib.rs b/libturms/src/lib.rs index 5927466..619afd4 100644 --- a/libturms/src/lib.rs +++ b/libturms/src/lib.rs @@ -5,6 +5,7 @@ pub extern crate error; pub extern crate p2p; mod channel; +mod padding; use discover::spawn_heartbeat; use discover::websocket::WebSocket; diff --git a/libturms/src/padding.rs b/libturms/src/padding.rs new file mode 100644 index 0000000..b992a8a --- /dev/null +++ b/libturms/src/padding.rs @@ -0,0 +1,52 @@ +//! Random crypto-secure padding. + +use rand::{RngCore, SeedableRng, TryRngCore}; +use rand::rngs::OsRng; +use rand_chacha::ChaCha20Rng; +use error::Result; + +// Numbers from specification. +const MIN_LENGTH: usize = 1000; // 1kB. +const PADDING_LENGTH: [usize; 2] = [0, 8192]; + +/// Padding structure. +#[derive(Debug, Clone)] +pub(crate) struct Padding { + min_length: usize, + padding_length: [usize; 2], + fill_padding: u8, +} + +impl Default for Padding { + fn default() -> Self { + Padding { + min_length: MIN_LENGTH, + padding_length: PADDING_LENGTH, + fill_padding: 0, // adds lots of zeros. + } + } +} + +impl Padding { + /// Fill an entry with bunch of paddings. + pub fn fill_zero(entry: impl AsRef<[u8]>) -> Result> { + let config = Self::default(); + let data = entry.as_ref(); + let data_len = data.len(); + + let mut seed = [0u8; 32]; + OsRng.try_fill_bytes(&mut seed)?; + + let mut rng = ChaCha20Rng::from_seed(seed); + rng.fill_bytes(&mut data); + + let base_target = std::cmp::max(data_len, config.min_length); + let total_size = base_target + data.len(); + + let mut padded_data = Vec::with_capacity(total_size); + padded_data.extend_from_slice(data); + padded_data.resize(total_size, config.fill_padding); + + Ok(padded_data) + } +} diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index 2d067bf..1443af6 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -1,8 +1,9 @@ //! peer-to-peer communication via WebRTC. -/// Models. -pub mod models; #[forbid(unsafe_code)] #[deny(missing_docs, missing_debug_implementations)] + +/// Models. +pub mod models; /// WebRTC interface. pub mod webrtc; /// X3DH over WebRTC for Turms. @@ -10,7 +11,7 @@ mod x3dh; pub use x3dh::triple_diffie_hellman; -use tokio::sync::Mutex; +use parking_lot::Mutex; use vodozemac::olm::Account; use std::sync::OnceLock; @@ -26,7 +27,7 @@ pub fn get_account() -> &'static Mutex { pub async fn save_account() -> error::Result { let account = get_account(); - Ok(serde_json::to_string(&account.lock().await.pickle())?) + Ok(serde_json::to_string(&account.lock().pickle())?) } /// Set user account. diff --git a/p2p/src/models.rs b/p2p/src/models.rs index 0a9b81b..b419b66 100644 --- a/p2p/src/models.rs +++ b/p2p/src/models.rs @@ -17,6 +17,7 @@ pub enum Event { Typing, } +/// Triple-diffie Hellman exchange. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct X3DH { /// Curve25519 public key. @@ -76,6 +77,7 @@ bitflags! { /// Represents a set of message/attachment flags. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct Flags: u32 { + /// Message flagged as urgent. const URGENT = 1 << 0; /// Message MUST NOT be saved. const EPHEMERAL = 1 << 1; diff --git a/p2p/src/x3dh.rs b/p2p/src/x3dh.rs index e5a0319..0a3716e 100644 --- a/p2p/src/x3dh.rs +++ b/p2p/src/x3dh.rs @@ -15,7 +15,7 @@ pub async fn triple_diffie_hellman( // Generate public key and one-time key. let (public_key, otk) = { - let mut account = account.lock().await; + let mut account = account.lock(); account.generate_one_time_keys(1); @@ -38,7 +38,7 @@ pub async fn triple_diffie_hellman( }))?; if acc.send(message).await.is_ok() { - account.lock().await.mark_keys_as_published(); + account.lock().mark_keys_as_published(); tracing::debug!("public key and one-time key published"); }; From 791813a2b8f7dedb5e6067b55771284e7d4c64ac Mon Sep 17 00:00:00 2001 From: RealHinome Date: Sun, 21 Dec 2025 19:31:47 +0100 Subject: [PATCH 2/4] Add padding on messages --- Cargo.lock | 2 -- libturms/Cargo.toml | 2 -- libturms/src/channel.rs | 2 ++ libturms/src/padding.rs | 74 +++++++++++++++++++++-------------------- p2p/src/lib.rs | 8 ++--- p2p/src/webrtc.rs | 4 +-- p2p/src/x3dh.rs | 4 +-- 7 files changed, 48 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92b2852..b8e9a11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1295,8 +1295,6 @@ dependencies = [ "futures-util", "hex", "p2p", - "rand 0.9.1", - "rand_chacha 0.9.0", "serde", "serde_json", "serde_yaml", diff --git a/libturms/Cargo.toml b/libturms/Cargo.toml index ac3c684..caf9a1f 100644 --- a/libturms/Cargo.toml +++ b/libturms/Cargo.toml @@ -24,8 +24,6 @@ p2p = { path = "../p2p" } error = { path = "../error" } vodozemac = "0.9" blake3 = "1.8" -rand = "0.9" -rand_chacha = "0.9" hex = "0.4" serde = { workspace = true } serde_yaml = "0.9" diff --git a/libturms/src/channel.rs b/libturms/src/channel.rs index 652a912..c16a388 100644 --- a/libturms/src/channel.rs +++ b/libturms/src/channel.rs @@ -79,6 +79,7 @@ pub fn handle_channel(sender: mpsc::Sender, webrtc: WebRTCManager) { if data.is_empty() { return; } + let data = crate::padding::Padding::unpad(data); let json = match serde_json::from_slice(&data) { Ok(json) => json, Err(err) => { @@ -130,6 +131,7 @@ async fn handle_dhkey_event(webrtc: &WebRTCManager, x3dh: X3DH) { prekey: Some(pk), })) { Ok(message) => { + let message = crate::padding::Padding::pad(message); if let Err(err) = webrtc.send(message).await { tracing::error!(%err, "failed to send message"); } diff --git a/libturms/src/padding.rs b/libturms/src/padding.rs index b992a8a..7b93823 100644 --- a/libturms/src/padding.rs +++ b/libturms/src/padding.rs @@ -1,52 +1,54 @@ //! Random crypto-secure padding. -use rand::{RngCore, SeedableRng, TryRngCore}; -use rand::rngs::OsRng; -use rand_chacha::ChaCha20Rng; -use error::Result; - -// Numbers from specification. -const MIN_LENGTH: usize = 1000; // 1kB. -const PADDING_LENGTH: [usize; 2] = [0, 8192]; +// Minimum required by specification. +const MIN_LENGTH: usize = 1000; // ~1kB. /// Padding structure. #[derive(Debug, Clone)] -pub(crate) struct Padding { - min_length: usize, - padding_length: [usize; 2], - fill_padding: u8, -} +pub(crate) struct Padding; -impl Default for Padding { - fn default() -> Self { - Padding { - min_length: MIN_LENGTH, - padding_length: PADDING_LENGTH, - fill_padding: 0, // adds lots of zeros. +impl Padding { + fn bucket_size(len: usize) -> usize { + match len { + 0..=MIN_LENGTH => MIN_LENGTH, + 1001..8192 => len.div_ceil(1024) * 1024, + _ => len, } } -} -impl Padding { - /// Fill an entry with bunch of paddings. - pub fn fill_zero(entry: impl AsRef<[u8]>) -> Result> { - let config = Self::default(); - let data = entry.as_ref(); - let data_len = data.len(); + /// Fill an entry with bunch of paddings using ISO/IEC 7816-4. + pub fn pad(data: impl AsRef<[u8]>) -> Vec { + let data = data.as_ref(); - let mut seed = [0u8; 32]; - OsRng.try_fill_bytes(&mut seed)?; + let target_len = Self::bucket_size(data.len()); + let mut out = Vec::with_capacity(target_len); - let mut rng = ChaCha20Rng::from_seed(seed); - rng.fill_bytes(&mut data); + out.extend_from_slice(data); + out.push(0x80); - let base_target = std::cmp::max(data_len, config.min_length); - let total_size = base_target + data.len(); + if out.len() < MIN_LENGTH { + out.resize(MIN_LENGTH, 0x00); + } + + out + } - let mut padded_data = Vec::with_capacity(total_size); - padded_data.extend_from_slice(data); - padded_data.resize(total_size, config.fill_padding); + /// Remove zero padding at the end of data using ISO/IEC 7816-4. + pub fn unpad(mut data: Vec) -> Vec { + // Scan from the end. + while let Some(&last) = data.last() { + match last { + 0x00 => { + data.pop(); + }, + 0x80 => { + data.pop(); + return data; + }, + _ => break, + } + } - Ok(padded_data) + data } } diff --git a/p2p/src/lib.rs b/p2p/src/lib.rs index 1443af6..39a2eaf 100644 --- a/p2p/src/lib.rs +++ b/p2p/src/lib.rs @@ -1,6 +1,6 @@ //! peer-to-peer communication via WebRTC. -#[forbid(unsafe_code)] -#[deny(missing_docs, missing_debug_implementations)] +#![forbid(unsafe_code)] +#![deny(missing_docs, missing_debug_implementations)] /// Models. pub mod models; @@ -11,7 +11,7 @@ mod x3dh; pub use x3dh::triple_diffie_hellman; -use parking_lot::Mutex; +use tokio::sync::Mutex; use vodozemac::olm::Account; use std::sync::OnceLock; @@ -27,7 +27,7 @@ pub fn get_account() -> &'static Mutex { pub async fn save_account() -> error::Result { let account = get_account(); - Ok(serde_json::to_string(&account.lock().pickle())?) + Ok(serde_json::to_string(&account.lock().await.pickle())?) } /// Set user account. diff --git a/p2p/src/webrtc.rs b/p2p/src/webrtc.rs index 50826b0..440cbcc 100644 --- a/p2p/src/webrtc.rs +++ b/p2p/src/webrtc.rs @@ -149,11 +149,11 @@ impl WebRTCManager { } /// Sender with retries. - pub async fn send(&self, message: String) -> Result<()> { + pub async fn send(&self, message: impl AsRef<[u8]>) -> Result<()> { // Encryption is CPU-bound. Async have no effect. May use `spawn_blocking` thread later. let msg = match self.session.clone().lock().as_mut() { Some(session) => session.encrypt(message).message().to_vec(), - None => message.as_bytes().to_vec(), + None => message.as_ref().to_vec(), }; match self.channel.as_ref() { diff --git a/p2p/src/x3dh.rs b/p2p/src/x3dh.rs index 0a3716e..e5a0319 100644 --- a/p2p/src/x3dh.rs +++ b/p2p/src/x3dh.rs @@ -15,7 +15,7 @@ pub async fn triple_diffie_hellman( // Generate public key and one-time key. let (public_key, otk) = { - let mut account = account.lock(); + let mut account = account.lock().await; account.generate_one_time_keys(1); @@ -38,7 +38,7 @@ pub async fn triple_diffie_hellman( }))?; if acc.send(message).await.is_ok() { - account.lock().mark_keys_as_published(); + account.lock().await.mark_keys_as_published(); tracing::debug!("public key and one-time key published"); }; From b276dd8a24e30463cb17c35b250ad7b7c170e252 Mon Sep 17 00:00:00 2001 From: RealHinome Date: Sun, 21 Dec 2025 20:27:56 +0100 Subject: [PATCH 3/4] Fix issues --- Cargo.lock | 2 -- error/Cargo.toml | 2 -- error/src/lib.rs | 2 -- libturms/src/padding.rs | 6 +++--- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8e9a11..5d02b0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -648,8 +648,6 @@ name = "error" version = "0.1.0" dependencies = [ "jsonwebtoken", - "rand 0.9.1", - "rand_chacha 0.9.0", "reqwest", "serde_json", "serde_yaml", diff --git a/error/Cargo.toml b/error/Cargo.toml index 628c7dc..b0f0ebd 100644 --- a/error/Cargo.toml +++ b/error/Cargo.toml @@ -15,5 +15,3 @@ url = "*" tungstenite = { workspace = true } reqwest = "0.12" webrtc = { workspace = true } -rand = "*" -rand_chacha = "*" diff --git a/error/src/lib.rs b/error/src/lib.rs index de55c30..e2fdc42 100644 --- a/error/src/lib.rs +++ b/error/src/lib.rs @@ -39,8 +39,6 @@ pub enum Error { AuthenticationFailed, #[error("sess-id does not exist on sdp")] MissingSessionId, - #[error(transparent)] - RandOs(#[from] rand::rand_core::OsError), } impl From for Error { diff --git a/libturms/src/padding.rs b/libturms/src/padding.rs index 7b93823..b7ee145 100644 --- a/libturms/src/padding.rs +++ b/libturms/src/padding.rs @@ -1,4 +1,4 @@ -//! Random crypto-secure padding. +//! Deterministic ISO/IEC 7816-4 padding. // Minimum required by specification. const MIN_LENGTH: usize = 1000; // ~1kB. @@ -11,7 +11,7 @@ impl Padding { fn bucket_size(len: usize) -> usize { match len { 0..=MIN_LENGTH => MIN_LENGTH, - 1001..8192 => len.div_ceil(1024) * 1024, + 1001..=8192 => len.div_ceil(1024) * 1024, _ => len, } } @@ -26,7 +26,7 @@ impl Padding { out.extend_from_slice(data); out.push(0x80); - if out.len() < MIN_LENGTH { + if out.len() < target_len { out.resize(MIN_LENGTH, 0x00); } From 456ce12ef0de6676e8ccc96677d97be26e538ce6 Mon Sep 17 00:00:00 2001 From: Hinome <57831472+RealHinome@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:37:00 +0100 Subject: [PATCH 4/4] Update documentation Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- p2p/src/models.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/p2p/src/models.rs b/p2p/src/models.rs index b419b66..93d8f43 100644 --- a/p2p/src/models.rs +++ b/p2p/src/models.rs @@ -17,7 +17,7 @@ pub enum Event { Typing, } -/// Triple-diffie Hellman exchange. +/// Triple Diffie-Hellman exchange. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct X3DH { /// Curve25519 public key.