lib

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

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:
Mcrates/simplex_agent_runtime/src/runtime.rs | 169++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mcrates/simplex_agent_store/src/store.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/simplex_smp_crypto/src/ratchet.rs | 357+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
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)