lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit dbc4d19bf582b7715eb667203bb3f72b0f7e6fff
parent 3dfc0b021e8c36d7eb839ddc69fe6806279a26b4
Author: triesap <tyson@radroots.org>
Date:   Mon, 22 Jun 2026 23:17:41 +0000

simplex: carry official opaque payloads

- add an official encrypted-message branch to agent payloads
- preserve legacy payload encoding for current runtime traffic
- reject mixed official and legacy payload fields during encoding
- surface official receive attempts truthfully until runtime wiring lands

Diffstat:
Mcrates/simplex_agent_proto/src/codec.rs | 55+++++++++++++++++++++++++++++++++++++++++++++----------
Mcrates/simplex_agent_proto/src/model.rs | 1+
Mcrates/simplex_agent_runtime/src/runtime.rs | 6++++++
Mcrates/simplex_interop_tests/src/lib.rs | 1+
4 files changed, 53 insertions(+), 10 deletions(-)

diff --git a/crates/simplex_agent_proto/src/codec.rs b/crates/simplex_agent_proto/src/codec.rs @@ -294,6 +294,16 @@ fn encode_encrypted_payload( buffer: &mut Vec<u8>, encrypted: &RadrootsSimplexAgentEncryptedPayload, ) -> Result<(), RadrootsSimplexAgentProtoError> { + if let Some(official_message) = encrypted.official_message.as_ref() { + if encrypted.ratchet_header.is_some() || !encrypted.ciphertext.is_empty() { + return Err(RadrootsSimplexAgentProtoError::InvalidRatchetHeader( + "official encrypted payload cannot include legacy ratchet fields".into(), + )); + } + buffer.push(2); + push_large_bytes(buffer, official_message)?; + return Ok(()); + } match &encrypted.ratchet_header { Some(header) => { buffer.push(1); @@ -308,16 +318,24 @@ fn encode_encrypted_payload( fn decode_encrypted_payload( cursor: &mut Cursor<'_>, ) -> Result<RadrootsSimplexAgentEncryptedPayload, RadrootsSimplexAgentProtoError> { - let has_header = decode_bool(cursor.read_byte()?)?; - let ratchet_header = if has_header { - Some(decode_ratchet_header(&cursor.read_large_bytes()?)?) - } else { - None - }; - Ok(RadrootsSimplexAgentEncryptedPayload { - ratchet_header, - ciphertext: cursor.read_large_bytes()?, - }) + match cursor.read_byte()? { + 0 => Ok(RadrootsSimplexAgentEncryptedPayload { + ratchet_header: None, + official_message: None, + ciphertext: cursor.read_large_bytes()?, + }), + 1 => Ok(RadrootsSimplexAgentEncryptedPayload { + ratchet_header: Some(decode_ratchet_header(&cursor.read_large_bytes()?)?), + official_message: None, + ciphertext: cursor.read_large_bytes()?, + }), + 2 => Ok(RadrootsSimplexAgentEncryptedPayload { + ratchet_header: None, + official_message: Some(cursor.read_large_bytes()?), + ciphertext: Vec::new(), + }), + other => Err(RadrootsSimplexAgentProtoError::InvalidBoolEncoding(other)), + } } fn encode_queue_descriptor( @@ -732,6 +750,7 @@ mod tests { pq_public_key: Some(b"pq".to_vec()), pq_ciphertext: Some(b"ct".to_vec()), }), + official_message: None, ciphertext: encoded_decrypted, }); let encoded_envelope = encode_envelope(&envelope).unwrap(); @@ -759,6 +778,7 @@ mod tests { pq_public_key: Some(vec![8_u8; 1158]), pq_ciphertext: Some(vec![9_u8; 1039]), }), + official_message: None, ciphertext: b"opaque".to_vec(), }); @@ -778,6 +798,7 @@ mod tests { pq_public_key: Some(vec![2_u8; 1158]), pq_ciphertext: None, }), + official_message: None, ciphertext: b"opaque".to_vec(), }); @@ -787,4 +808,18 @@ mod tests { RadrootsSimplexAgentProtoError::InvalidRatchetHeader(_) )); } + + #[test] + fn roundtrips_official_opaque_encrypted_payload() { + let envelope = + RadrootsSimplexAgentEnvelope::Message(RadrootsSimplexAgentEncryptedPayload { + ratchet_header: None, + official_message: Some(b"official-encrypted-ratchet-message".to_vec()), + ciphertext: Vec::new(), + }); + + let encoded = encode_envelope(&envelope).unwrap(); + let decoded = decode_envelope(&encoded).unwrap(); + assert_eq!(decoded, envelope); + } } diff --git a/crates/simplex_agent_proto/src/model.rs b/crates/simplex_agent_proto/src/model.rs @@ -116,6 +116,7 @@ pub enum RadrootsSimplexAgentDecryptedMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RadrootsSimplexAgentEncryptedPayload { pub ratchet_header: Option<RadrootsSimplexSmpRatchetHeader>, + pub official_message: Option<Vec<u8>>, pub ciphertext: Vec<u8>, } diff --git a/crates/simplex_agent_runtime/src/runtime.rs b/crates/simplex_agent_runtime/src/runtime.rs @@ -1575,6 +1575,7 @@ impl RadrootsSimplexAgentRuntime { .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; Ok(RadrootsSimplexAgentEncryptedPayload { ratchet_header: Some(ratchet_header), + official_message: None, ciphertext, }) } @@ -1602,6 +1603,11 @@ impl RadrootsSimplexAgentRuntime { connection_id: &str, encrypted: &RadrootsSimplexAgentEncryptedPayload, ) -> Result<Vec<u8>, RadrootsSimplexAgentRuntimeError> { + if encrypted.official_message.is_some() { + return Err(RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` received official encrypted payload before official ratchet runtime wiring is enabled" + ))); + } let shared_secret = self .store .connection(connection_id)? diff --git a/crates/simplex_interop_tests/src/lib.rs b/crates/simplex_interop_tests/src/lib.rs @@ -252,6 +252,7 @@ mod tests { let envelope = RadrootsSimplexAgentEnvelope::Message(RadrootsSimplexAgentEncryptedPayload { ratchet_header: None, + official_message: None, ciphertext: b"opaque-agent-ciphertext".to_vec(), }); let decoded_envelope = decode_envelope(&encode_envelope(&envelope).unwrap()).unwrap();