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:
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();