commit b6d9efa3630ea7fbe852c598a7b345ceb92ac769
parent 5e24621e7386d99866f5a0fbf2b5104ea4413a08
Author: triesap <tyson@radroots.org>
Date: Tue, 23 Jun 2026 00:27:42 +0000
simplex: initialize official ratchet state
- store official X3DH root, chain, and header keys in ratchet state
- encrypt and decrypt official payloads from initialized ratchet keys
- persist official ratchet material through agent store snapshots
- initialize sender and receiver ratchets from confirmation X3DH params
Diffstat:
3 files changed, 485 insertions(+), 95 deletions(-)
diff --git a/crates/simplex_agent_runtime/src/runtime.rs b/crates/simplex_agent_runtime/src/runtime.rs
@@ -27,7 +27,8 @@ use radroots_simplex_smp_crypto::prelude::{
RadrootsSimplexSmpCryptoError, RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair,
decode_x25519_public_key_x509, decrypt_padded, derive_shared_secret,
encode_ed25519_public_key_x509, encode_x25519_public_key_x509, encrypt_padded,
- official_x448_keypair_from_seed, random_nonce,
+ official_x3dh_receiver_init, official_x3dh_sender_init, official_x448_keypair_from_seed,
+ random_nonce,
};
use radroots_simplex_smp_proto::prelude::{
RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION,
@@ -296,17 +297,26 @@ impl RadrootsSimplexAgentRuntime {
&now.to_be_bytes(),
],
));
- let ratchet_state = RadrootsSimplexSmpRatchetState::responder(
+ let mut ratchet_state = RadrootsSimplexSmpRatchetState::responder(
local_x3dh_key_2.public_key.clone(),
invitation.e2e_ratchet_params.key_2.clone(),
None,
)
- .ok();
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
+ let sender_init = official_x3dh_sender_init(
+ &local_x3dh_key_1,
+ &local_x3dh_key_2,
+ &invitation.e2e_ratchet_params,
+ )
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
+ ratchet_state
+ .initialize_official_sender(local_x3dh_key_2.private_key.clone(), sender_init)
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
let connection = self.store.create_connection(
RadrootsSimplexAgentConnectionMode::Direct,
RadrootsSimplexAgentConnectionStatus::JoinPending,
Some(invitation.clone()),
- ratchet_state,
+ Some(ratchet_state),
);
let send_auth_state = self.store.generate_queue_auth_state()?;
let send_descriptor = RadrootsSimplexAgentQueueDescriptor {
@@ -1489,6 +1499,7 @@ impl RadrootsSimplexAgentRuntime {
if let Some(shared_secret) = derived_secret {
self.store.connection_mut(connection_id)?.shared_secret = Some(shared_secret);
}
+ self.initialize_receiver_ratchet_from_confirmation(connection_id, &envelope)?;
let decrypted = self.extract_decrypted_message(connection_id, &envelope)?;
{
let connection = self.store.connection_mut(connection_id)?;
@@ -1590,6 +1601,46 @@ impl RadrootsSimplexAgentRuntime {
))
}
+ fn initialize_receiver_ratchet_from_confirmation(
+ &mut self,
+ connection_id: &str,
+ envelope: &RadrootsSimplexAgentEnvelope,
+ ) -> Result<(), RadrootsSimplexAgentRuntimeError> {
+ let RadrootsSimplexAgentEnvelope::Confirmation {
+ e2e_ratchet_params: Some(params),
+ ..
+ } = envelope
+ else {
+ return Ok(());
+ };
+ let connection = self.store.connection(connection_id)?;
+ let local_key_1 = connection.local_x3dh_key_1.clone().ok_or_else(|| {
+ RadrootsSimplexAgentRuntimeError::Runtime(format!(
+ "SimpleX connection `{connection_id}` missing local X3DH key 1"
+ ))
+ })?;
+ let local_key_2 = connection.local_x3dh_key_2.clone().ok_or_else(|| {
+ RadrootsSimplexAgentRuntimeError::Runtime(format!(
+ "SimpleX connection `{connection_id}` missing local X3DH key 2"
+ ))
+ })?;
+ let local_key_1 = official_x3dh_keypair_from_agent(local_key_1);
+ let local_key_2 = official_x3dh_keypair_from_agent(local_key_2);
+ let receiver_init = official_x3dh_receiver_init(&local_key_1, &local_key_2, params)
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?;
+ self.store
+ .connection_mut(connection_id)?
+ .ratchet_state
+ .as_mut()
+ .ok_or_else(|| {
+ RadrootsSimplexAgentRuntimeError::Runtime(format!(
+ "SimpleX connection `{connection_id}` has no ratchet state"
+ ))
+ })?
+ .initialize_official_receiver(local_key_2.private_key, receiver_init)
+ .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))
+ }
+
fn next_encrypted_payload(
&mut self,
connection_id: &str,
@@ -1749,6 +1800,15 @@ fn agent_x3dh_keypair(
}
}
+fn official_x3dh_keypair_from_agent(
+ keypair: RadrootsSimplexAgentX3dhKeypair,
+) -> RadrootsSimplexOfficialX448Keypair {
+ RadrootsSimplexOfficialX448Keypair {
+ public_key: keypair.public_key,
+ private_key: keypair.private_key,
+ }
+}
+
fn official_x3dh_params_from_keys(
key_1: &[u8],
key_2: &[u8],
@@ -1981,7 +2041,7 @@ mod tests {
};
use radroots_simplex_smp_proto::prelude::{
RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpError,
- RadrootsSimplexSmpQueueIdsResponse,
+ RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpVersionRange,
};
use radroots_simplex_smp_transport::prelude::RadrootsSimplexSmpTransportBlock;
@@ -2029,6 +2089,43 @@ mod tests {
.unwrap();
}
+ fn initialize_test_outbound_official_ratchet(
+ runtime: &mut RadrootsSimplexAgentRuntime,
+ connection_id: &str,
+ ) {
+ let local_key_1 = official_x448_keypair_from_seed(b"rr-synth-runtime-test-local-x3dh-1");
+ let local_key_2 = official_x448_keypair_from_seed(b"rr-synth-runtime-test-local-x3dh-2");
+ let remote_key_1 = official_x448_keypair_from_seed(b"rr-synth-runtime-test-remote-x3dh-1");
+ let remote_key_2 = official_x448_keypair_from_seed(b"rr-synth-runtime-test-remote-x3dh-2");
+ let remote_params = RadrootsSimplexOfficialX3dhParams {
+ version_range: RadrootsSimplexSmpVersionRange::new(
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION,
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
+ )
+ .unwrap(),
+ key_1: remote_key_1.public_key,
+ key_2: remote_key_2.public_key.clone(),
+ pq_public_key: None,
+ pq_ciphertext: None,
+ };
+ let sender_init =
+ official_x3dh_sender_init(&local_key_1, &local_key_2, &remote_params).unwrap();
+ let mut ratchet = RadrootsSimplexSmpRatchetState::responder(
+ local_key_2.public_key,
+ remote_key_2.public_key,
+ None,
+ )
+ .unwrap();
+ ratchet
+ .initialize_official_sender(local_key_2.private_key, sender_init)
+ .unwrap();
+ runtime
+ .store
+ .connection_mut(connection_id)
+ .unwrap()
+ .ratchet_state = Some(ratchet);
+ }
+
fn ids_response(
recipient_id: &[u8],
sender_id: &[u8],
@@ -2289,6 +2386,67 @@ mod tests {
}
#[test]
+ fn confirmation_params_initialize_receiver_ratchet() {
+ let mut runtime = RadrootsSimplexAgentRuntimeBuilder::new().build().unwrap();
+ let created = runtime
+ .create_connection(invitation_queue(), b"e2e".to_vec(), false, 10)
+ .unwrap();
+ let invitation = runtime
+ .store
+ .connection(&created)
+ .unwrap()
+ .invitation
+ .clone()
+ .unwrap();
+ let joined = runtime
+ .join_connection(invitation, reply_queue(), 20)
+ .unwrap();
+ let joined_connection = runtime.store.connection(&joined).unwrap();
+ let joined_key_1 = joined_connection.local_x3dh_key_1.as_ref().unwrap();
+ let joined_key_2 = joined_connection.local_x3dh_key_2.as_ref().unwrap();
+ let e2e_ratchet_params =
+ official_x3dh_params_from_keys(&joined_key_1.public_key, &joined_key_2.public_key)
+ .unwrap();
+ let envelope = RadrootsSimplexAgentEnvelope::Confirmation {
+ reply_queue: true,
+ e2e_ratchet_params: Some(e2e_ratchet_params),
+ encrypted: RadrootsSimplexAgentEncryptedPayload {
+ ratchet_header: None,
+ official_message: Some(Vec::new()),
+ ciphertext: Vec::new(),
+ },
+ };
+
+ runtime
+ .initialize_receiver_ratchet_from_confirmation(&created, &envelope)
+ .unwrap();
+ let mut sender_ratchet = runtime
+ .store
+ .connection(&joined)
+ .unwrap()
+ .ratchet_state
+ .clone()
+ .unwrap();
+ let encrypted = sender_ratchet
+ .encrypt_official_payload(&[0_u8; 32], b"reply-info", 96)
+ .unwrap();
+ let receiver_ratchet = runtime
+ .store
+ .connection_mut(&created)
+ .unwrap()
+ .ratchet_state
+ .as_mut()
+ .unwrap();
+ let decrypted = receiver_ratchet
+ .decrypt_official_payload(&[0_u8; 32], &encrypted)
+ .unwrap();
+
+ assert_eq!(decrypted, b"reply-info");
+ assert!(receiver_ratchet.official_sending_chain_key.is_some());
+ assert!(receiver_ratchet.official_receiving_chain_key.is_some());
+ }
+
+ #[test]
fn explicit_get_connection_message_executes_smp_get() {
let mut runtime = RadrootsSimplexAgentRuntimeBuilder::new().build().unwrap();
let created = runtime
@@ -2424,6 +2582,7 @@ mod tests {
runtime.store.connection(&created).unwrap().status,
RadrootsSimplexAgentConnectionStatus::AwaitingApproval
);
+ initialize_test_outbound_official_ratchet(&mut runtime, &created);
runtime
.allow_connection(&created, b"local-info".to_vec(), 40)
diff --git a/crates/simplex_agent_store/src/store.rs b/crates/simplex_agent_store/src/store.rs
@@ -229,6 +229,15 @@ struct RadrootsSimplexAgentRatchetStateSnapshot {
pending_outbound_pq_ciphertext: Option<Vec<u8>>,
pending_inbound_pq_ciphertext: Option<Vec<u8>>,
current_pq_shared_secret: Option<Vec<u8>>,
+ local_dh_private_key: Option<Vec<u8>>,
+ official_associated_data: Option<Vec<u8>>,
+ official_root_key: Option<Vec<u8>>,
+ official_sending_chain_key: Option<Vec<u8>>,
+ official_receiving_chain_key: Option<Vec<u8>>,
+ official_sending_header_key: Option<Vec<u8>>,
+ official_receiving_header_key: Option<Vec<u8>>,
+ official_next_sending_header_key: Option<Vec<u8>>,
+ official_next_receiving_header_key: Option<Vec<u8>>,
}
#[cfg(feature = "std")]
@@ -1033,6 +1042,15 @@ fn ratchet_state_to_snapshot(
pending_outbound_pq_ciphertext: state.pending_outbound_pq_ciphertext,
pending_inbound_pq_ciphertext: state.pending_inbound_pq_ciphertext,
current_pq_shared_secret: state.current_pq_shared_secret,
+ local_dh_private_key: state.local_dh_private_key,
+ official_associated_data: state.official_associated_data,
+ official_root_key: state.official_root_key,
+ official_sending_chain_key: state.official_sending_chain_key,
+ official_receiving_chain_key: state.official_receiving_chain_key,
+ official_sending_header_key: state.official_sending_header_key,
+ official_receiving_header_key: state.official_receiving_header_key,
+ official_next_sending_header_key: state.official_next_sending_header_key,
+ official_next_receiving_header_key: state.official_next_receiving_header_key,
}
}
@@ -1076,6 +1094,15 @@ fn ratchet_state_from_snapshot(
state.pending_outbound_pq_ciphertext = snapshot.pending_outbound_pq_ciphertext;
state.pending_inbound_pq_ciphertext = snapshot.pending_inbound_pq_ciphertext;
state.current_pq_shared_secret = snapshot.current_pq_shared_secret;
+ state.local_dh_private_key = snapshot.local_dh_private_key;
+ state.official_associated_data = snapshot.official_associated_data;
+ state.official_root_key = snapshot.official_root_key;
+ state.official_sending_chain_key = snapshot.official_sending_chain_key;
+ state.official_receiving_chain_key = snapshot.official_receiving_chain_key;
+ state.official_sending_header_key = snapshot.official_sending_header_key;
+ state.official_receiving_header_key = snapshot.official_receiving_header_key;
+ state.official_next_sending_header_key = snapshot.official_next_sending_header_key;
+ state.official_next_receiving_header_key = snapshot.official_next_receiving_header_key;
Ok(state)
}
@@ -1480,6 +1507,20 @@ mod tests {
let connection = store.connection_mut(&connection.id).unwrap();
connection.hello_sent = true;
connection.hello_received = true;
+ let mut ratchet =
+ RadrootsSimplexSmpRatchetState::initiator(vec![1_u8; 56], vec![2_u8; 56], None)
+ .unwrap();
+ ratchet.local_dh_private_key = Some(b"official-private".to_vec());
+ ratchet.official_associated_data = Some(b"official-ad".to_vec());
+ ratchet.official_root_key = Some(b"official-root".to_vec());
+ ratchet.official_sending_chain_key = Some(b"official-send-chain".to_vec());
+ ratchet.official_receiving_chain_key = Some(b"official-recv-chain".to_vec());
+ ratchet.official_sending_header_key = Some(b"official-send-header".to_vec());
+ ratchet.official_receiving_header_key = Some(b"official-recv-header".to_vec());
+ ratchet.official_next_sending_header_key = Some(b"official-next-send-header".to_vec());
+ ratchet.official_next_receiving_header_key =
+ Some(b"official-next-recv-header".to_vec());
+ connection.ratchet_state = Some(ratchet);
connection.local_x3dh_key_1 = Some(RadrootsSimplexAgentX3dhKeypair {
public_key: b"x3dh-public-1".to_vec(),
private_key: b"x3dh-private-1".to_vec(),
@@ -1502,6 +1543,19 @@ mod tests {
);
assert!(loaded_connection.hello_sent);
assert!(loaded_connection.hello_received);
+ let loaded_ratchet = loaded_connection.ratchet_state.as_ref().unwrap();
+ assert_eq!(
+ loaded_ratchet.official_associated_data.as_deref(),
+ Some(&b"official-ad"[..])
+ );
+ assert_eq!(
+ loaded_ratchet.official_sending_chain_key.as_deref(),
+ Some(&b"official-send-chain"[..])
+ );
+ assert_eq!(
+ loaded_ratchet.official_next_receiving_header_key.as_deref(),
+ Some(&b"official-next-recv-header"[..])
+ );
assert_eq!(
loaded_connection
.local_x3dh_key_1
diff --git a/crates/simplex_smp_crypto/src/ratchet.rs b/crates/simplex_smp_crypto/src/ratchet.rs
@@ -4,18 +4,19 @@ use crate::message::{
encrypt_padded,
};
use crate::official_ratchet::{
- RADROOTS_SIMPLEX_OFFICIAL_AES_KEY_LENGTH, RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
- RadrootsSimplexOfficialAesGcmPayload, RadrootsSimplexOfficialChainKdfOutput,
- RadrootsSimplexOfficialEncryptedHeader, RadrootsSimplexOfficialEncryptedMessage,
- RadrootsSimplexOfficialMsgHeader, decode_official_encrypted_header,
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION, RADROOTS_SIMPLEX_OFFICIAL_X448_KEY_LENGTH,
+ RadrootsSimplexOfficialAesGcmPayload, RadrootsSimplexOfficialEncryptedHeader,
+ RadrootsSimplexOfficialEncryptedMessage, RadrootsSimplexOfficialMsgHeader,
+ RadrootsSimplexOfficialX3dhInit, decode_official_encrypted_header,
decode_official_encrypted_message, decode_official_msg_header,
- encode_official_encrypted_header, encode_official_encrypted_message,
- encode_official_msg_header, official_aes_gcm_decrypt_padded, official_aes_gcm_encrypt_padded,
- official_chain_kdf, official_ratchet_header_len,
+ derive_official_x448_shared_secret, encode_official_encrypted_header,
+ encode_official_encrypted_message, encode_official_msg_header, generate_official_x448_keypair,
+ official_aes_gcm_decrypt_padded, official_aes_gcm_encrypt_padded, official_chain_kdf,
+ official_ratchet_header_len, official_root_kdf,
};
use alloc::vec::Vec;
use hkdf::Hkdf;
-use sha2::{Digest, Sha256, Sha512};
+use sha2::Sha512;
const RADROOTS_SIMPLEX_AGENT_RATCHET_INFO: &[u8] = b"SimpleXAgentRatchetMessage";
const RADROOTS_SIMPLEX_AGENT_RATCHET_OUTPUT_LENGTH: usize =
@@ -64,6 +65,15 @@ pub struct RadrootsSimplexSmpRatchetState {
pub pending_outbound_pq_ciphertext: Option<Vec<u8>>,
pub pending_inbound_pq_ciphertext: Option<Vec<u8>>,
pub current_pq_shared_secret: Option<Vec<u8>>,
+ pub local_dh_private_key: Option<Vec<u8>>,
+ pub official_associated_data: Option<Vec<u8>>,
+ pub official_root_key: Option<Vec<u8>>,
+ pub official_sending_chain_key: Option<Vec<u8>>,
+ pub official_receiving_chain_key: Option<Vec<u8>>,
+ pub official_sending_header_key: Option<Vec<u8>>,
+ pub official_receiving_header_key: Option<Vec<u8>>,
+ pub official_next_sending_header_key: Option<Vec<u8>>,
+ pub official_next_receiving_header_key: Option<Vec<u8>>,
}
impl RadrootsSimplexSmpRatchetState {
@@ -91,6 +101,15 @@ impl RadrootsSimplexSmpRatchetState {
pending_outbound_pq_ciphertext: None,
pending_inbound_pq_ciphertext: None,
current_pq_shared_secret: None,
+ local_dh_private_key: None,
+ official_associated_data: None,
+ official_root_key: None,
+ official_sending_chain_key: None,
+ official_receiving_chain_key: None,
+ official_sending_header_key: None,
+ official_receiving_header_key: None,
+ official_next_sending_header_key: None,
+ official_next_receiving_header_key: None,
})
}
@@ -118,9 +137,65 @@ impl RadrootsSimplexSmpRatchetState {
pending_outbound_pq_ciphertext: None,
pending_inbound_pq_ciphertext: None,
current_pq_shared_secret: None,
+ local_dh_private_key: None,
+ official_associated_data: None,
+ official_root_key: None,
+ official_sending_chain_key: None,
+ official_receiving_chain_key: None,
+ official_sending_header_key: None,
+ official_receiving_header_key: None,
+ official_next_sending_header_key: None,
+ official_next_receiving_header_key: None,
})
}
+ pub fn initialize_official_sender(
+ &mut self,
+ local_dh_private_key: Vec<u8>,
+ init: RadrootsSimplexOfficialX3dhInit,
+ ) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ validate_official_private_key(&local_dh_private_key)?;
+ let root_dh =
+ derive_official_x448_shared_secret(&local_dh_private_key, &self.remote_dh_public_key)?;
+ let root = official_root_kdf(&init.ratchet_key, &root_dh, None)?;
+ self.local_dh_private_key = Some(local_dh_private_key);
+ self.official_associated_data = Some(init.associated_data);
+ self.official_root_key = Some(root.root_key);
+ self.official_sending_chain_key = Some(root.chain_key);
+ self.official_receiving_chain_key = None;
+ self.official_sending_header_key = Some(init.sending_header_key);
+ self.official_receiving_header_key = None;
+ self.official_next_sending_header_key = Some(root.next_header_key);
+ self.official_next_receiving_header_key = Some(init.receiving_next_header_key);
+ self.previous_sending_chain_length = 0;
+ self.sending_chain_length = 0;
+ self.receiving_chain_length = 0;
+ self.root_epoch = 0;
+ Ok(())
+ }
+
+ pub fn initialize_official_receiver(
+ &mut self,
+ local_dh_private_key: Vec<u8>,
+ init: RadrootsSimplexOfficialX3dhInit,
+ ) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ validate_official_private_key(&local_dh_private_key)?;
+ self.local_dh_private_key = Some(local_dh_private_key);
+ self.official_associated_data = Some(init.associated_data);
+ self.official_root_key = Some(init.ratchet_key);
+ self.official_sending_chain_key = None;
+ self.official_receiving_chain_key = None;
+ self.official_sending_header_key = None;
+ self.official_receiving_header_key = None;
+ self.official_next_sending_header_key = Some(init.receiving_next_header_key);
+ self.official_next_receiving_header_key = Some(init.sending_header_key);
+ self.previous_sending_chain_length = 0;
+ self.sending_chain_length = 0;
+ self.receiving_chain_length = 0;
+ self.root_epoch = 0;
+ Ok(())
+ }
+
pub fn stage_outbound_pq_step(
&mut self,
pq_public_key: Vec<u8>,
@@ -251,26 +326,36 @@ impl RadrootsSimplexSmpRatchetState {
pub fn encrypt_official_payload(
&mut self,
- shared_secret: &[u8],
+ _shared_secret: &[u8],
plaintext: &[u8],
padded_len: usize,
) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
let message_number = self.sending_chain_length;
- let header = self.next_outbound_header()?;
+ let header = RadrootsSimplexSmpRatchetHeader {
+ previous_sending_chain_length: self.previous_sending_chain_length,
+ message_number,
+ dh_public_key: self.local_dh_public_key.clone(),
+ pq_public_key: self.current_pq_public_key.clone(),
+ pq_ciphertext: self.pending_outbound_pq_ciphertext.clone(),
+ };
+ header.validate()?;
let header_plaintext = encode_official_msg_header(
RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
&official_msg_header_from_ratchet_header(&header),
)?;
- let official = derive_official_payload_keys(
- shared_secret,
- self.current_pq_shared_secret.as_deref(),
- self.root_epoch,
- message_number,
+ let ratchet_ad = self.official_associated_data.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_associated_data"),
)?;
- let ratchet_ad = official_ratchet_associated_data(shared_secret, self.root_epoch);
+ let sending_header_key = self.official_sending_header_key.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_sending_header_key"),
+ )?;
+ let sending_chain_key = self.official_sending_chain_key.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_sending_chain_key"),
+ )?;
+ let chain = official_chain_kdf(&sending_chain_key)?;
let header_payload = official_aes_gcm_encrypt_padded(
- &official.header_key,
- &official.chain.header_iv,
+ &sending_header_key,
+ &chain.header_iv,
&header_plaintext,
official_ratchet_header_len(
RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
@@ -279,17 +364,20 @@ impl RadrootsSimplexSmpRatchetState {
&ratchet_ad,
)?;
let encrypted_header = encode_official_encrypted_header(&official_encrypted_header(
- official.chain.header_iv,
+ chain.header_iv,
header_payload,
)?)?;
let message_ad = official_message_associated_data(&ratchet_ad, &encrypted_header);
let message_payload = official_aes_gcm_encrypt_padded(
- &official.chain.message_key,
- &official.chain.message_iv,
+ &chain.message_key,
+ &chain.message_iv,
plaintext,
padded_len,
&message_ad,
)?;
+ self.official_sending_chain_key = Some(chain.chain_key);
+ self.sending_chain_length = self.sending_chain_length.saturating_add(1);
+ self.pending_outbound_pq_ciphertext = None;
encode_official_encrypted_message(
RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
&RadrootsSimplexOfficialEncryptedMessage {
@@ -302,52 +390,98 @@ impl RadrootsSimplexSmpRatchetState {
pub fn decrypt_official_payload(
&mut self,
- shared_secret: &[u8],
+ _shared_secret: &[u8],
encrypted_message: &[u8],
) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> {
- let message_number = self.receiving_chain_length;
- let official = derive_official_payload_keys(
- shared_secret,
- self.current_pq_shared_secret.as_deref(),
- self.root_epoch,
- message_number,
- )?;
let message = decode_official_encrypted_message(encrypted_message)?;
let header = decode_official_encrypted_header(&message.encrypted_header)?;
- let ratchet_ad = official_ratchet_associated_data(shared_secret, self.root_epoch);
- let header_plaintext = official_aes_gcm_decrypt_padded(
- &official.header_key,
- &header.iv,
- &RadrootsSimplexOfficialAesGcmPayload {
- auth_tag: header.auth_tag,
- ciphertext: header.body,
- },
- &ratchet_ad,
+ let ratchet_ad = self.official_associated_data.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_associated_data"),
)?;
- let ratchet_header = ratchet_header_from_official_msg_header(decode_official_msg_header(
- header.version,
- &header_plaintext,
- )?);
+ let (ratchet_step, ratchet_header) = self.decrypt_official_header(&header, &ratchet_ad)?;
if ratchet_header.message_number < self.receiving_chain_length {
return Err(RadrootsSimplexSmpCryptoError::RatchetMessageRegression {
received: ratchet_header.message_number,
current: self.receiving_chain_length,
});
}
+ if ratchet_step == OfficialRatchetStep::Advance {
+ self.advance_official_receiving_ratchet(&ratchet_header)?;
+ }
+ let receiving_chain_key = self.official_receiving_chain_key.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_receiving_chain_key"),
+ )?;
+ let chain = official_chain_kdf(&receiving_chain_key)?;
let message_ad = official_message_associated_data(&ratchet_ad, &message.encrypted_header);
let plaintext = official_aes_gcm_decrypt_padded(
- &official.chain.message_key,
- &official.chain.message_iv,
+ &chain.message_key,
+ &chain.message_iv,
&RadrootsSimplexOfficialAesGcmPayload {
auth_tag: message.auth_tag,
ciphertext: message.body,
},
&message_ad,
)?;
+ self.official_receiving_chain_key = Some(chain.chain_key);
self.apply_inbound_header(&ratchet_header, None)?;
Ok(plaintext)
}
+ fn decrypt_official_header(
+ &self,
+ header: &RadrootsSimplexOfficialEncryptedHeader,
+ ratchet_ad: &[u8],
+ ) -> Result<(OfficialRatchetStep, RadrootsSimplexSmpRatchetHeader), RadrootsSimplexSmpCryptoError>
+ {
+ if let Some(receiving_header_key) = self.official_receiving_header_key.as_ref() {
+ if let Ok(ratchet_header) =
+ decrypt_official_header_with_key(header, receiving_header_key, ratchet_ad)
+ {
+ return Ok((OfficialRatchetStep::Same, ratchet_header));
+ }
+ }
+ let next_receiving_header_key = self.official_next_receiving_header_key.as_ref().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_next_receiving_header_key"),
+ )?;
+ decrypt_official_header_with_key(header, next_receiving_header_key, ratchet_ad)
+ .map(|ratchet_header| (OfficialRatchetStep::Advance, ratchet_header))
+ }
+
+ fn advance_official_receiving_ratchet(
+ &mut self,
+ header: &RadrootsSimplexSmpRatchetHeader,
+ ) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ let local_private_key = self.local_dh_private_key.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("local_dh_private_key"),
+ )?;
+ let root_key = self.official_root_key.clone().ok_or(
+ RadrootsSimplexSmpCryptoError::MissingRatchetKey("official_root_key"),
+ )?;
+ let receiving_dh =
+ derive_official_x448_shared_secret(&local_private_key, &header.dh_public_key)?;
+ let receiving_root = official_root_kdf(&root_key, &receiving_dh, None)?;
+ let next_local_keypair = generate_official_x448_keypair()?;
+ let sending_dh = derive_official_x448_shared_secret(
+ &next_local_keypair.private_key,
+ &header.dh_public_key,
+ )?;
+ let sending_root = official_root_kdf(&receiving_root.root_key, &sending_dh, None)?;
+ self.previous_sending_chain_length = self.sending_chain_length;
+ self.sending_chain_length = 0;
+ self.receiving_chain_length = 0;
+ self.remote_dh_public_key = header.dh_public_key.clone();
+ self.local_dh_public_key = next_local_keypair.public_key;
+ self.local_dh_private_key = Some(next_local_keypair.private_key);
+ self.official_root_key = Some(sending_root.root_key);
+ self.official_receiving_chain_key = Some(receiving_root.chain_key);
+ self.official_receiving_header_key = self.official_next_receiving_header_key.take();
+ self.official_next_receiving_header_key = Some(receiving_root.next_header_key);
+ self.official_sending_chain_key = Some(sending_root.chain_key);
+ self.official_sending_header_key = self.official_next_sending_header_key.take();
+ self.official_next_sending_header_key = Some(sending_root.next_header_key);
+ Ok(())
+ }
+
fn pq_enabled(&self) -> bool {
self.current_pq_public_key.is_some()
|| self.remote_pq_public_key.is_some()
@@ -362,6 +496,15 @@ fn validate_public_key(value: &[u8]) -> Result<(), RadrootsSimplexSmpCryptoError
Ok(())
}
+fn validate_official_private_key(value: &[u8]) -> Result<(), RadrootsSimplexSmpCryptoError> {
+ if value.len() != RADROOTS_SIMPLEX_OFFICIAL_X448_KEY_LENGTH {
+ return Err(RadrootsSimplexSmpCryptoError::InvalidPrivateKeyLength(
+ value.len(),
+ ));
+ }
+ Ok(())
+}
+
fn derive_ratchet_message_key(
shared_secret: &[u8],
pq_shared_secret: Option<&[u8]>,
@@ -392,35 +535,29 @@ fn derive_ratchet_message_key(
))
}
-struct OfficialPayloadKeys {
- header_key: Vec<u8>,
- chain: RadrootsSimplexOfficialChainKdfOutput,
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum OfficialRatchetStep {
+ Same,
+ Advance,
}
-fn derive_official_payload_keys(
- shared_secret: &[u8],
- pq_shared_secret: Option<&[u8]>,
- root_epoch: u64,
- message_number: u32,
-) -> Result<OfficialPayloadKeys, RadrootsSimplexSmpCryptoError> {
- let mut seed_input =
- Vec::with_capacity(shared_secret.len() + pq_shared_secret.map_or(0, <[u8]>::len) + 12);
- seed_input.extend_from_slice(shared_secret);
- if let Some(secret) = pq_shared_secret {
- seed_input.extend_from_slice(secret);
- }
- seed_input.extend_from_slice(&root_epoch.to_be_bytes());
- seed_input.extend_from_slice(&message_number.to_be_bytes());
- let chain_seed = Sha256::digest(&seed_input);
- let chain = official_chain_kdf(&chain_seed)?;
- let mut header_input = Vec::with_capacity(chain.chain_key.len() + shared_secret.len() + 12);
- header_input.extend_from_slice(&chain.chain_key);
- header_input.extend_from_slice(shared_secret);
- header_input.extend_from_slice(&root_epoch.to_be_bytes());
- header_input.extend_from_slice(&message_number.to_be_bytes());
- let header_key =
- Sha256::digest(&header_input)[..RADROOTS_SIMPLEX_OFFICIAL_AES_KEY_LENGTH].to_vec();
- Ok(OfficialPayloadKeys { header_key, chain })
+fn decrypt_official_header_with_key(
+ header: &RadrootsSimplexOfficialEncryptedHeader,
+ header_key: &[u8],
+ ratchet_ad: &[u8],
+) -> Result<RadrootsSimplexSmpRatchetHeader, RadrootsSimplexSmpCryptoError> {
+ let header_plaintext = official_aes_gcm_decrypt_padded(
+ header_key,
+ &header.iv,
+ &RadrootsSimplexOfficialAesGcmPayload {
+ auth_tag: header.auth_tag.clone(),
+ ciphertext: header.body.clone(),
+ },
+ ratchet_ad,
+ )?;
+ Ok(ratchet_header_from_official_msg_header(
+ decode_official_msg_header(header.version, &header_plaintext)?,
+ ))
}
fn official_encrypted_header(
@@ -435,13 +572,6 @@ fn official_encrypted_header(
})
}
-fn official_ratchet_associated_data(shared_secret: &[u8], root_epoch: u64) -> Vec<u8> {
- let mut associated_data = Vec::with_capacity(shared_secret.len() + 8);
- associated_data.extend_from_slice(shared_secret);
- associated_data.extend_from_slice(&root_epoch.to_be_bytes());
- associated_data
-}
-
fn official_message_associated_data(ratchet_ad: &[u8], encrypted_header: &[u8]) -> Vec<u8> {
let mut associated_data = Vec::with_capacity(ratchet_ad.len() + encrypted_header.len());
associated_data.extend_from_slice(ratchet_ad);
@@ -520,6 +650,63 @@ fn push_large_bytes(
#[cfg(test)]
mod tests {
use super::*;
+ use crate::official_ratchet::{
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION, RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION,
+ RadrootsSimplexOfficialX3dhParams, official_x3dh_receiver_init, official_x3dh_sender_init,
+ official_x448_keypair_from_seed,
+ };
+ use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpVersionRange;
+
+ fn official_sender_receiver_ratchets() -> (
+ RadrootsSimplexSmpRatchetState,
+ RadrootsSimplexSmpRatchetState,
+ ) {
+ let receiver_key_1 = official_x448_keypair_from_seed(b"rr-synth-ratchet-rcv-1");
+ let receiver_key_2 = official_x448_keypair_from_seed(b"rr-synth-ratchet-rcv-2");
+ let sender_key_1 = official_x448_keypair_from_seed(b"rr-synth-ratchet-snd-1");
+ let sender_key_2 = official_x448_keypair_from_seed(b"rr-synth-ratchet-snd-2");
+ let receiver_params = RadrootsSimplexOfficialX3dhParams {
+ version_range: RadrootsSimplexSmpVersionRange::new(
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_KDF_VERSION,
+ RADROOTS_SIMPLEX_OFFICIAL_E2E_CURRENT_VERSION,
+ )
+ .unwrap(),
+ key_1: receiver_key_1.public_key.clone(),
+ key_2: receiver_key_2.public_key.clone(),
+ pq_public_key: None,
+ pq_ciphertext: None,
+ };
+ let sender_params = RadrootsSimplexOfficialX3dhParams {
+ version_range: receiver_params.version_range,
+ key_1: sender_key_1.public_key.clone(),
+ key_2: sender_key_2.public_key.clone(),
+ pq_public_key: None,
+ pq_ciphertext: None,
+ };
+ let sender_init =
+ official_x3dh_sender_init(&sender_key_1, &sender_key_2, &receiver_params).unwrap();
+ let receiver_init =
+ official_x3dh_receiver_init(&receiver_key_1, &receiver_key_2, &sender_params).unwrap();
+ let mut sender = RadrootsSimplexSmpRatchetState::responder(
+ sender_key_2.public_key.clone(),
+ receiver_key_2.public_key.clone(),
+ None,
+ )
+ .unwrap();
+ sender
+ .initialize_official_sender(sender_key_2.private_key, sender_init)
+ .unwrap();
+ let mut receiver = RadrootsSimplexSmpRatchetState::initiator(
+ receiver_key_2.public_key.clone(),
+ receiver_key_1.public_key.clone(),
+ None,
+ )
+ .unwrap();
+ receiver
+ .initialize_official_receiver(receiver_key_2.private_key, receiver_init)
+ .unwrap();
+ (sender, receiver)
+ }
#[test]
fn stages_outbound_pq_state_and_emits_header() {
@@ -666,12 +853,7 @@ mod tests {
#[test]
fn encrypts_official_payload_as_opaque_message() {
- let mut sender =
- RadrootsSimplexSmpRatchetState::initiator(vec![1_u8; 56], vec![2_u8; 56], None)
- .unwrap();
- let mut receiver =
- RadrootsSimplexSmpRatchetState::responder(vec![2_u8; 56], vec![1_u8; 56], None)
- .unwrap();
+ let (mut sender, mut receiver) = official_sender_receiver_ratchets();
let shared_secret = [11_u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH];
let encrypted = sender
@@ -689,12 +871,7 @@ mod tests {
#[test]
fn rejects_tampered_official_payload_body() {
- let mut sender =
- RadrootsSimplexSmpRatchetState::initiator(vec![1_u8; 56], vec![2_u8; 56], None)
- .unwrap();
- let mut receiver =
- RadrootsSimplexSmpRatchetState::responder(vec![2_u8; 56], vec![1_u8; 56], None)
- .unwrap();
+ let (mut sender, mut receiver) = official_sender_receiver_ratchets();
let shared_secret = [12_u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH];
let mut encrypted = sender
.encrypt_official_payload(&shared_secret, b"official agent body", 96)