store.rs (176092B)
1 use crate::error::RadrootsSimplexAgentStoreError; 2 use alloc::collections::BTreeMap; 3 use alloc::format; 4 use alloc::string::String; 5 #[cfg(feature = "std")] 6 use alloc::string::ToString; 7 #[cfg(feature = "std")] 8 use alloc::sync::Arc; 9 use alloc::vec::Vec; 10 #[cfg(feature = "std")] 11 use chacha20poly1305::aead::{Aead, KeyInit, Payload}; 12 #[cfg(feature = "std")] 13 use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce}; 14 #[cfg(feature = "std")] 15 use getrandom::getrandom; 16 #[cfg(feature = "std")] 17 use radroots_protected_store::file::{ 18 RADROOTS_PROTECTED_FILE_SECRET_SUFFIX, RADROOTS_PROTECTED_FILE_WRAPPING_KEY_FILE, 19 }; 20 #[cfg(feature = "std")] 21 use radroots_protected_store::{ 22 RADROOTS_PROTECTED_STORE_KEY_LENGTH, RADROOTS_PROTECTED_STORE_NONCE_LENGTH, 23 RadrootsProtectedFileKeySource, RadrootsProtectedStoreEnvelope, sidecar_path, 24 }; 25 #[cfg(all(feature = "std", feature = "os-keyring"))] 26 use radroots_secret_vault::RadrootsSecretVaultOsKeyring; 27 #[cfg(feature = "std")] 28 use radroots_secret_vault::{ 29 RadrootsSecretKeyWrapping, RadrootsSecretVault, RadrootsSecretVaultAccessError, 30 }; 31 use radroots_simplex_agent_proto::prelude::{ 32 RadrootsSimplexAgentConnectionLink, RadrootsSimplexAgentConnectionMode, 33 RadrootsSimplexAgentConnectionStatus, RadrootsSimplexAgentEnvelope, 34 RadrootsSimplexAgentMessageId, RadrootsSimplexAgentMessageReceipt, 35 RadrootsSimplexAgentQueueAddress, RadrootsSimplexAgentQueueDescriptor, 36 RadrootsSimplexAgentShortInvitationLink, RadrootsSimplexAgentShortLinkScheme, 37 RadrootsSimplexSmpRatchetState, 38 }; 39 #[cfg(feature = "std")] 40 use radroots_simplex_agent_proto::prelude::{ 41 decode_connection_link, decode_envelope, encode_connection_link, encode_envelope, 42 }; 43 use radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpEd25519Keypair; 44 #[cfg(feature = "std")] 45 use radroots_simplex_smp_crypto::prelude::{ 46 RADROOTS_SIMPLEX_OFFICIAL_AES_IV_LENGTH, RadrootsSimplexSmpSkippedMessageKey, 47 }; 48 use radroots_simplex_smp_proto::prelude::{ 49 RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress, 50 }; 51 #[cfg(feature = "std")] 52 use serde::{Deserialize, Serialize}; 53 #[cfg(feature = "std")] 54 use sha2::{Digest, Sha256}; 55 #[cfg(feature = "std")] 56 use std::ffi::OsString; 57 #[cfg(feature = "std")] 58 use std::fs; 59 #[cfg(feature = "std")] 60 use std::io::Write; 61 #[cfg(feature = "std")] 62 use std::path::{Path, PathBuf}; 63 #[cfg(feature = "std")] 64 use std::time::{SystemTime, UNIX_EPOCH}; 65 #[cfg(feature = "std")] 66 use zeroize::Zeroize; 67 68 #[cfg(feature = "std")] 69 const RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_VERSION: u8 = 1; 70 #[cfg(feature = "std")] 71 const RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_KEY_SLOT: &str = 72 "radroots_simplex_agent_store_secrets"; 73 #[cfg(feature = "std")] 74 const RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SNAPSHOT_KEY_SLOT: &str = 75 "radroots_simplex_agent_store_snapshot"; 76 #[cfg(feature = "std")] 77 const RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES: usize = 78 RADROOTS_PROTECTED_STORE_KEY_LENGTH; 79 #[cfg(feature = "std")] 80 const RADROOTS_SIMPLEX_AGENT_STORE_WRAPPED_KEY_VERSION: u8 = 1; 81 #[cfg(all(feature = "std", feature = "os-keyring"))] 82 const RADROOTS_SIMPLEX_AGENT_STORE_KEYCHAIN_SERVICE: &str = "org.radroots.simplex.agent-store"; 83 84 #[cfg(feature = "std")] 85 #[derive(Debug, Clone, PartialEq, Eq)] 86 pub struct RadrootsSimplexAgentStoreProtectedSecretsDiagnostics { 87 pub store_path: PathBuf, 88 pub protected_secrets_path: PathBuf, 89 pub wrapping_key_path: PathBuf, 90 pub public_snapshot_exists: bool, 91 pub protected_secrets_configured: bool, 92 pub protected_secrets_exists: bool, 93 pub wrapping_key_exists: bool, 94 pub protected_connection_count: usize, 95 pub protected_pending_command_count: usize, 96 pub protected_generation: Option<String>, 97 pub protected_envelope_suffix: Option<String>, 98 pub protected_wrapping_key_suffix: Option<String>, 99 } 100 101 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 102 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 103 pub enum RadrootsSimplexAgentQueueRole { 104 Receive, 105 Send, 106 } 107 108 #[derive(Debug, Clone, PartialEq, Eq)] 109 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 110 pub struct RadrootsSimplexAgentQueueAuthState { 111 pub public_key: Vec<u8>, 112 pub private_key: Vec<u8>, 113 } 114 115 #[derive(Debug, Clone, PartialEq, Eq)] 116 pub struct RadrootsSimplexAgentQueueRecord { 117 pub descriptor: RadrootsSimplexAgentQueueDescriptor, 118 pub entity_id: Vec<u8>, 119 pub role: RadrootsSimplexAgentQueueRole, 120 pub subscribed: bool, 121 pub primary: bool, 122 pub tested: bool, 123 pub auth_state: Option<RadrootsSimplexAgentQueueAuthState>, 124 pub delivery_private_key: Option<Vec<u8>>, 125 pub delivery_shared_secret: Option<Vec<u8>>, 126 } 127 128 #[derive(Debug, Clone, PartialEq, Eq)] 129 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 130 pub struct RadrootsSimplexAgentDeliveryCursor { 131 pub last_sent_message_id: Option<RadrootsSimplexAgentMessageId>, 132 pub last_received_message_id: Option<RadrootsSimplexAgentMessageId>, 133 pub last_sent_message_hash: Option<Vec<u8>>, 134 pub last_received_message_hash: Option<Vec<u8>>, 135 } 136 137 #[derive(Debug, Clone, PartialEq, Eq)] 138 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 139 pub struct RadrootsSimplexAgentRecentMessageRecord { 140 pub message_id: RadrootsSimplexAgentMessageId, 141 pub message_hash: Vec<u8>, 142 #[cfg_attr( 143 feature = "std", 144 serde(default, skip_serializing_if = "Option::is_none") 145 )] 146 pub inbound_queue: Option<RadrootsSimplexAgentRecentQueueAddress>, 147 #[cfg_attr( 148 feature = "std", 149 serde(default, skip_serializing_if = "Option::is_none") 150 )] 151 pub inbound_broker_message_id: Option<Vec<u8>>, 152 } 153 154 #[derive(Debug, Clone, PartialEq, Eq)] 155 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 156 pub struct RadrootsSimplexAgentRecentQueueAddress { 157 pub server_identity: String, 158 pub hosts: Vec<String>, 159 pub port: Option<u16>, 160 pub sender_id: Vec<u8>, 161 } 162 163 impl RadrootsSimplexAgentRecentQueueAddress { 164 fn from_queue_address(queue: &RadrootsSimplexAgentQueueAddress) -> Self { 165 Self { 166 server_identity: queue.server.server_identity.clone(), 167 hosts: queue.server.hosts.clone(), 168 port: queue.server.port, 169 sender_id: queue.sender_id.clone(), 170 } 171 } 172 173 fn into_queue_address(self) -> RadrootsSimplexAgentQueueAddress { 174 RadrootsSimplexAgentQueueAddress { 175 server: RadrootsSimplexSmpServerAddress { 176 server_identity: self.server_identity, 177 hosts: self.hosts, 178 port: self.port, 179 }, 180 sender_id: self.sender_id, 181 } 182 } 183 } 184 185 #[derive(Debug, Clone, PartialEq, Eq)] 186 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 187 pub struct RadrootsSimplexAgentOutboundMessage { 188 pub message_id: RadrootsSimplexAgentMessageId, 189 pub message_hash: Vec<u8>, 190 } 191 192 #[derive(Debug, Clone, PartialEq, Eq)] 193 pub struct RadrootsSimplexAgentPreparedOutboundMessage { 194 pub message_id: RadrootsSimplexAgentMessageId, 195 pub previous_message_hash: Vec<u8>, 196 pub message_hash: Vec<u8>, 197 } 198 199 #[derive(Debug, Clone, PartialEq, Eq)] 200 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 201 pub struct RadrootsSimplexAgentX3dhKeypair { 202 pub public_key: Vec<u8>, 203 pub private_key: Vec<u8>, 204 } 205 206 #[derive(Debug, Clone, PartialEq, Eq)] 207 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] 208 pub struct RadrootsSimplexAgentPqKeypair { 209 pub public_key: Vec<u8>, 210 pub private_key: Vec<u8>, 211 } 212 213 #[derive(Debug, Clone, PartialEq, Eq)] 214 pub struct RadrootsSimplexAgentShortLinkCredentials { 215 pub scheme: RadrootsSimplexAgentShortLinkScheme, 216 pub hosts: Vec<String>, 217 pub port: Option<u16>, 218 pub server_key_hash: Option<Vec<u8>>, 219 pub link_id: Vec<u8>, 220 pub link_key: Vec<u8>, 221 pub link_public_signature_key: Vec<u8>, 222 pub link_private_signature_key: Vec<u8>, 223 pub encrypted_fixed_data: Option<Vec<u8>>, 224 pub encrypted_user_data: Option<Vec<u8>>, 225 } 226 227 impl RadrootsSimplexAgentShortLinkCredentials { 228 pub fn invitation_link(&self) -> RadrootsSimplexAgentShortInvitationLink { 229 RadrootsSimplexAgentShortInvitationLink { 230 scheme: self.scheme, 231 hosts: self.hosts.clone(), 232 port: self.port, 233 server_key_hash: self.server_key_hash.clone(), 234 link_id: self.link_id.clone(), 235 link_key: self.link_key.clone(), 236 } 237 } 238 } 239 240 #[derive(Debug, Clone, PartialEq, Eq)] 241 pub enum RadrootsSimplexAgentPendingCommandKind { 242 CreateQueue { 243 descriptor: RadrootsSimplexAgentQueueDescriptor, 244 }, 245 SecureQueue { 246 queue: RadrootsSimplexAgentQueueAddress, 247 sender_key: Option<Vec<u8>>, 248 }, 249 SendEnvelope { 250 queue: RadrootsSimplexAgentQueueAddress, 251 envelope: RadrootsSimplexAgentEnvelope, 252 delivery: Option<RadrootsSimplexAgentOutboundMessage>, 253 }, 254 SubscribeQueue { 255 queue: RadrootsSimplexAgentQueueAddress, 256 }, 257 GetQueueMessage { 258 queue: RadrootsSimplexAgentQueueAddress, 259 }, 260 AckInboxMessage { 261 queue: RadrootsSimplexAgentQueueAddress, 262 broker_message_id: Vec<u8>, 263 receipt: Option<RadrootsSimplexAgentMessageReceipt>, 264 }, 265 RotateQueues { 266 descriptors: Vec<RadrootsSimplexAgentQueueDescriptor>, 267 }, 268 TestQueues { 269 queues: Vec<RadrootsSimplexAgentQueueAddress>, 270 }, 271 SetQueueLinkData { 272 queue: RadrootsSimplexAgentQueueAddress, 273 link_id: Vec<u8>, 274 link_data: RadrootsSimplexSmpQueueLinkData, 275 }, 276 SecureGetQueueLinkData { 277 invitation: RadrootsSimplexAgentShortInvitationLink, 278 reply_queue: RadrootsSimplexSmpQueueUri, 279 sender_auth_state: RadrootsSimplexAgentQueueAuthState, 280 }, 281 GetQueueLinkData { 282 invitation: RadrootsSimplexAgentShortInvitationLink, 283 reply_queue: RadrootsSimplexSmpQueueUri, 284 }, 285 } 286 287 #[derive(Debug, Clone, PartialEq, Eq)] 288 pub struct RadrootsSimplexAgentPendingCommand { 289 pub id: u64, 290 pub connection_id: String, 291 pub kind: RadrootsSimplexAgentPendingCommandKind, 292 pub attempts: u32, 293 pub ready_at: u64, 294 pub inflight: bool, 295 } 296 297 #[derive(Debug, Clone, PartialEq, Eq)] 298 pub struct RadrootsSimplexAgentConnectionRecord { 299 pub id: String, 300 pub mode: RadrootsSimplexAgentConnectionMode, 301 pub status: RadrootsSimplexAgentConnectionStatus, 302 pub invitation: Option<RadrootsSimplexAgentConnectionLink>, 303 pub short_link: Option<RadrootsSimplexAgentShortLinkCredentials>, 304 pub queues: Vec<RadrootsSimplexAgentQueueRecord>, 305 pub ratchet_state: Option<RadrootsSimplexSmpRatchetState>, 306 pub local_e2e_public_key: Option<Vec<u8>>, 307 pub local_e2e_private_key: Option<Vec<u8>>, 308 pub local_x3dh_key_1: Option<RadrootsSimplexAgentX3dhKeypair>, 309 pub local_x3dh_key_2: Option<RadrootsSimplexAgentX3dhKeypair>, 310 pub local_pq_keypair: Option<RadrootsSimplexAgentPqKeypair>, 311 pub shared_secret: Option<Vec<u8>>, 312 pub delivery_cursor: RadrootsSimplexAgentDeliveryCursor, 313 pub last_received_queue: Option<RadrootsSimplexAgentQueueAddress>, 314 pub last_received_broker_message_id: Option<Vec<u8>>, 315 pub recent_messages: Vec<RadrootsSimplexAgentRecentMessageRecord>, 316 pub staged_outbound_message: Option<RadrootsSimplexAgentOutboundMessage>, 317 pub hello_sent: bool, 318 pub hello_received: bool, 319 } 320 321 #[cfg(feature = "std")] 322 #[derive(Debug, Clone, Serialize, Deserialize)] 323 struct RadrootsSimplexAgentStoreSnapshot { 324 next_connection_sequence: u64, 325 next_command_sequence: u64, 326 #[serde(default, skip_serializing_if = "Option::is_none")] 327 protected_secrets: Option<RadrootsSimplexAgentStoreProtectedSecretsRef>, 328 connections: Vec<RadrootsSimplexAgentConnectionSnapshot>, 329 pending_commands: Vec<RadrootsSimplexAgentPendingCommandSnapshot>, 330 } 331 332 #[cfg(feature = "std")] 333 #[derive(Debug, Clone, Serialize, Deserialize)] 334 struct RadrootsSimplexAgentStoreProtectedSecretsRef { 335 version: u8, 336 generation: String, 337 envelope_suffix: String, 338 wrapping_key_suffix: String, 339 key_slot: String, 340 connection_count: usize, 341 pending_command_count: usize, 342 } 343 344 #[cfg(feature = "std")] 345 #[derive(Debug, Clone, Serialize, Deserialize)] 346 struct RadrootsSimplexAgentConnectionSnapshot { 347 id: String, 348 mode: String, 349 status: String, 350 invitation: Option<Vec<u8>>, 351 short_link: Option<RadrootsSimplexAgentShortLinkCredentialsSnapshot>, 352 queues: Vec<RadrootsSimplexAgentQueueRecordSnapshot>, 353 ratchet_state: Option<RadrootsSimplexAgentRatchetStateSnapshot>, 354 local_e2e_public_key: Option<Vec<u8>>, 355 local_e2e_private_key: Option<Vec<u8>>, 356 local_x3dh_key_1: Option<RadrootsSimplexAgentX3dhKeypair>, 357 local_x3dh_key_2: Option<RadrootsSimplexAgentX3dhKeypair>, 358 local_pq_keypair: Option<RadrootsSimplexAgentPqKeypair>, 359 shared_secret: Option<Vec<u8>>, 360 delivery_cursor: RadrootsSimplexAgentDeliveryCursor, 361 last_received_queue: Option<RadrootsSimplexAgentQueueAddressSnapshot>, 362 last_received_broker_message_id: Option<Vec<u8>>, 363 recent_messages: Vec<RadrootsSimplexAgentRecentMessageRecord>, 364 staged_outbound_message: Option<RadrootsSimplexAgentOutboundMessage>, 365 hello_sent: bool, 366 hello_received: bool, 367 } 368 369 #[cfg(feature = "std")] 370 #[derive(Debug, Clone, Serialize, Deserialize)] 371 struct RadrootsSimplexAgentQueueRecordSnapshot { 372 descriptor: RadrootsSimplexAgentQueueDescriptorSnapshot, 373 entity_id: Vec<u8>, 374 role: String, 375 subscribed: bool, 376 primary: bool, 377 tested: bool, 378 auth_state: Option<RadrootsSimplexAgentQueueAuthState>, 379 delivery_private_key: Option<Vec<u8>>, 380 delivery_shared_secret: Option<Vec<u8>>, 381 } 382 383 #[cfg(feature = "std")] 384 #[derive(Debug, Clone, Serialize, Deserialize)] 385 struct RadrootsSimplexAgentQueueDescriptorSnapshot { 386 queue_uri: String, 387 replaced_queue: Option<RadrootsSimplexAgentQueueAddressSnapshot>, 388 primary: bool, 389 sender_key: Option<Vec<u8>>, 390 } 391 392 #[cfg(feature = "std")] 393 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 394 struct RadrootsSimplexAgentQueueAddressSnapshot { 395 server_identity: String, 396 hosts: Vec<String>, 397 port: Option<u16>, 398 sender_id: Vec<u8>, 399 } 400 401 #[cfg(feature = "std")] 402 #[derive(Debug, Clone, Serialize, Deserialize)] 403 struct RadrootsSimplexAgentShortLinkCredentialsSnapshot { 404 scheme: String, 405 hosts: Vec<String>, 406 port: Option<u16>, 407 server_key_hash: Option<Vec<u8>>, 408 link_id: Vec<u8>, 409 link_key: Vec<u8>, 410 link_public_signature_key: Vec<u8>, 411 link_private_signature_key: Vec<u8>, 412 encrypted_fixed_data: Option<Vec<u8>>, 413 encrypted_user_data: Option<Vec<u8>>, 414 } 415 416 #[cfg(feature = "std")] 417 #[derive(Debug, Clone, Serialize, Deserialize)] 418 struct RadrootsSimplexAgentShortInvitationLinkSnapshot { 419 scheme: String, 420 hosts: Vec<String>, 421 port: Option<u16>, 422 server_key_hash: Option<Vec<u8>>, 423 link_id: Vec<u8>, 424 link_key: Vec<u8>, 425 } 426 427 #[cfg(feature = "std")] 428 #[derive(Debug, Clone, Serialize, Deserialize)] 429 struct RadrootsSimplexAgentQueueLinkDataSnapshot { 430 fixed_data: Vec<u8>, 431 user_data: Vec<u8>, 432 } 433 434 #[cfg(feature = "std")] 435 #[derive(Debug, Clone, Serialize, Deserialize)] 436 struct RadrootsSimplexAgentRatchetStateSnapshot { 437 role: String, 438 root_epoch: u64, 439 previous_sending_chain_length: u32, 440 sending_chain_length: u32, 441 receiving_chain_length: u32, 442 local_dh_public_key: Vec<u8>, 443 remote_dh_public_key: Vec<u8>, 444 current_pq_public_key: Option<Vec<u8>>, 445 remote_pq_public_key: Option<Vec<u8>>, 446 pending_outbound_pq_ciphertext: Option<Vec<u8>>, 447 pending_inbound_pq_ciphertext: Option<Vec<u8>>, 448 current_pq_shared_secret: Option<Vec<u8>>, 449 local_pq_private_key: Option<Vec<u8>>, 450 local_dh_private_key: Option<Vec<u8>>, 451 official_associated_data: Option<Vec<u8>>, 452 official_root_key: Option<Vec<u8>>, 453 official_sending_chain_key: Option<Vec<u8>>, 454 official_receiving_chain_key: Option<Vec<u8>>, 455 official_sending_header_key: Option<Vec<u8>>, 456 official_receiving_header_key: Option<Vec<u8>>, 457 official_next_sending_header_key: Option<Vec<u8>>, 458 official_next_receiving_header_key: Option<Vec<u8>>, 459 official_skipped_message_keys: Vec<RadrootsSimplexAgentSkippedMessageKeySnapshot>, 460 } 461 462 #[cfg(feature = "std")] 463 #[derive(Debug, Clone, Serialize, Deserialize)] 464 struct RadrootsSimplexAgentSkippedMessageKeySnapshot { 465 header_key: Vec<u8>, 466 message_number: u32, 467 message_key: Vec<u8>, 468 message_iv: Vec<u8>, 469 } 470 471 #[cfg(feature = "std")] 472 #[derive(Debug, Clone, Serialize, Deserialize)] 473 struct RadrootsSimplexAgentPendingCommandSnapshot { 474 id: u64, 475 connection_id: String, 476 kind: RadrootsSimplexAgentPendingCommandKindSnapshot, 477 attempts: u32, 478 ready_at: u64, 479 inflight: bool, 480 } 481 482 #[cfg(feature = "std")] 483 #[derive(Debug, Clone, Serialize, Deserialize)] 484 #[serde(tag = "kind", rename_all = "snake_case")] 485 enum RadrootsSimplexAgentPendingCommandKindSnapshot { 486 CreateQueue { 487 descriptor: RadrootsSimplexAgentQueueDescriptorSnapshot, 488 }, 489 SecureQueue { 490 queue: RadrootsSimplexAgentQueueAddressSnapshot, 491 sender_key: Option<Vec<u8>>, 492 }, 493 SendEnvelope { 494 queue: RadrootsSimplexAgentQueueAddressSnapshot, 495 envelope: Vec<u8>, 496 delivery: Option<RadrootsSimplexAgentOutboundMessage>, 497 }, 498 SubscribeQueue { 499 queue: RadrootsSimplexAgentQueueAddressSnapshot, 500 }, 501 GetQueueMessage { 502 queue: RadrootsSimplexAgentQueueAddressSnapshot, 503 }, 504 AckInboxMessage { 505 queue: RadrootsSimplexAgentQueueAddressSnapshot, 506 broker_message_id: Vec<u8>, 507 receipt: Option<RadrootsSimplexAgentMessageReceiptSnapshot>, 508 }, 509 RotateQueues { 510 descriptors: Vec<RadrootsSimplexAgentQueueDescriptorSnapshot>, 511 }, 512 TestQueues { 513 queues: Vec<RadrootsSimplexAgentQueueAddressSnapshot>, 514 }, 515 SetQueueLinkData { 516 queue: RadrootsSimplexAgentQueueAddressSnapshot, 517 link_id: Vec<u8>, 518 link_data: RadrootsSimplexAgentQueueLinkDataSnapshot, 519 }, 520 SecureGetQueueLinkData { 521 invitation: RadrootsSimplexAgentShortInvitationLinkSnapshot, 522 reply_queue: String, 523 sender_auth_public_key: Vec<u8>, 524 sender_auth_private_key: Vec<u8>, 525 }, 526 GetQueueLinkData { 527 invitation: RadrootsSimplexAgentShortInvitationLinkSnapshot, 528 reply_queue: String, 529 }, 530 } 531 532 #[cfg(feature = "std")] 533 #[derive(Debug, Clone, Serialize, Deserialize)] 534 struct RadrootsSimplexAgentMessageReceiptSnapshot { 535 message_id: RadrootsSimplexAgentMessageId, 536 message_hash: Vec<u8>, 537 receipt_info: Vec<u8>, 538 } 539 540 #[cfg(feature = "std")] 541 #[derive(Debug, Clone, Serialize, Deserialize)] 542 struct RadrootsSimplexAgentStoreSecretsSnapshot { 543 version: u8, 544 generation: String, 545 connections: Vec<RadrootsSimplexAgentConnectionSecretsSnapshot>, 546 pending_commands: Vec<RadrootsSimplexAgentPendingCommandSecretsSnapshot>, 547 } 548 549 #[cfg(feature = "std")] 550 #[derive(Debug, Clone, Serialize, Deserialize)] 551 struct RadrootsSimplexAgentConnectionSecretsSnapshot { 552 id: String, 553 short_link_link_key: Option<Vec<u8>>, 554 short_link_private_signature_key: Option<Vec<u8>>, 555 queues: Vec<RadrootsSimplexAgentQueueSecretsSnapshot>, 556 ratchet_state: Option<RadrootsSimplexAgentRatchetSecretsSnapshot>, 557 local_e2e_private_key: Option<Vec<u8>>, 558 local_x3dh_key_1_private_key: Option<Vec<u8>>, 559 local_x3dh_key_2_private_key: Option<Vec<u8>>, 560 local_pq_private_key: Option<Vec<u8>>, 561 shared_secret: Option<Vec<u8>>, 562 } 563 564 #[cfg(feature = "std")] 565 #[derive(Debug, Clone, Serialize, Deserialize)] 566 struct RadrootsSimplexAgentQueueSecretsSnapshot { 567 entity_id: Vec<u8>, 568 role: String, 569 #[serde(default, skip_serializing_if = "Option::is_none")] 570 queue_address: Option<RadrootsSimplexAgentQueueAddressSnapshot>, 571 auth_private_key: Option<Vec<u8>>, 572 delivery_private_key: Option<Vec<u8>>, 573 delivery_shared_secret: Option<Vec<u8>>, 574 } 575 576 #[cfg(feature = "std")] 577 #[derive(Debug, Clone, Serialize, Deserialize)] 578 struct RadrootsSimplexAgentRatchetSecretsSnapshot { 579 current_pq_shared_secret: Option<Vec<u8>>, 580 local_pq_private_key: Option<Vec<u8>>, 581 local_dh_private_key: Option<Vec<u8>>, 582 official_root_key: Option<Vec<u8>>, 583 official_sending_chain_key: Option<Vec<u8>>, 584 official_receiving_chain_key: Option<Vec<u8>>, 585 official_sending_header_key: Option<Vec<u8>>, 586 official_receiving_header_key: Option<Vec<u8>>, 587 official_next_sending_header_key: Option<Vec<u8>>, 588 official_next_receiving_header_key: Option<Vec<u8>>, 589 official_skipped_message_keys: Vec<RadrootsSimplexAgentSkippedMessageKeySnapshot>, 590 } 591 592 #[cfg(feature = "std")] 593 #[derive(Debug, Clone, Serialize, Deserialize)] 594 struct RadrootsSimplexAgentPendingCommandSecretsSnapshot { 595 id: u64, 596 connection_id: String, 597 short_invitation_link_key: Option<Vec<u8>>, 598 short_invitation_sender_auth_private_key: Option<Vec<u8>>, 599 } 600 601 #[cfg(feature = "std")] 602 #[derive(Clone)] 603 enum RadrootsSimplexAgentStorePersistence { 604 PublicSnapshot { 605 path: PathBuf, 606 }, 607 ProtectedSnapshot { 608 path: PathBuf, 609 key_source: RadrootsSimplexAgentStoreVaultKeySource, 610 }, 611 } 612 613 #[cfg(feature = "std")] 614 impl core::fmt::Debug for RadrootsSimplexAgentStorePersistence { 615 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 616 match self { 617 Self::PublicSnapshot { path } => f 618 .debug_struct("PublicSnapshot") 619 .field("path", path) 620 .finish(), 621 Self::ProtectedSnapshot { path, key_source } => f 622 .debug_struct("ProtectedSnapshot") 623 .field("path", path) 624 .field("key_source", key_source) 625 .finish(), 626 } 627 } 628 } 629 630 #[cfg(feature = "std")] 631 #[derive(Clone)] 632 struct RadrootsSimplexAgentStoreVaultKeySource { 633 vault: Arc<dyn RadrootsSecretVault>, 634 master_key_slot: String, 635 } 636 637 #[cfg(feature = "std")] 638 impl core::fmt::Debug for RadrootsSimplexAgentStoreVaultKeySource { 639 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 640 f.debug_struct("RadrootsSimplexAgentStoreVaultKeySource") 641 .field("master_key_slot", &self.master_key_slot) 642 .finish_non_exhaustive() 643 } 644 } 645 646 #[derive(Debug, Clone, Default)] 647 pub struct RadrootsSimplexAgentStore { 648 next_connection_sequence: u64, 649 next_command_sequence: u64, 650 connections: BTreeMap<String, RadrootsSimplexAgentConnectionRecord>, 651 pending_commands: BTreeMap<u64, RadrootsSimplexAgentPendingCommand>, 652 #[cfg(feature = "std")] 653 persistence: Option<RadrootsSimplexAgentStorePersistence>, 654 } 655 656 impl RadrootsSimplexAgentStore { 657 pub fn new() -> Self { 658 Self::default() 659 } 660 661 #[cfg(feature = "std")] 662 pub fn open(path: impl AsRef<Path>) -> Result<Self, RadrootsSimplexAgentStoreError> { 663 let path = path.as_ref().to_path_buf(); 664 if !path.exists() { 665 return Ok(Self { 666 persistence: Some(RadrootsSimplexAgentStorePersistence::PublicSnapshot { path }), 667 ..Default::default() 668 }); 669 } 670 671 let raw = fs::read(&path).map_err(|error| { 672 RadrootsSimplexAgentStoreError::Persistence(format!( 673 "failed to read SimpleX agent store snapshot `{}`: {error}", 674 path.display() 675 )) 676 })?; 677 678 let mut snapshot: RadrootsSimplexAgentStoreSnapshot = serde_json::from_slice(&raw) 679 .map_err(|error| { 680 RadrootsSimplexAgentStoreError::Persistence(format!( 681 "failed to parse SimpleX agent store snapshot `{}`: {error}", 682 path.display() 683 )) 684 })?; 685 let protected_secrets_configured = snapshot.protected_secrets.is_some(); 686 validate_public_snapshot_secret_posture(&snapshot, protected_secrets_configured)?; 687 if protected_secrets_configured { 688 let protected = read_protected_secrets_snapshot(&path, &snapshot)?; 689 merge_protected_secrets(&mut snapshot, protected)?; 690 } 691 692 let mut store = Self::from_snapshot(snapshot)?; 693 store.persistence = Some(RadrootsSimplexAgentStorePersistence::PublicSnapshot { path }); 694 Ok(store) 695 } 696 697 #[cfg(all(feature = "std", feature = "os-keyring"))] 698 pub fn open_keychain_protected( 699 path: impl AsRef<Path>, 700 ) -> Result<Self, RadrootsSimplexAgentStoreError> { 701 let path = path.as_ref(); 702 Self::open_protected_with_vault( 703 path, 704 Arc::new(RadrootsSecretVaultOsKeyring::new( 705 RADROOTS_SIMPLEX_AGENT_STORE_KEYCHAIN_SERVICE, 706 )), 707 protected_snapshot_master_key_slot(path), 708 ) 709 } 710 711 #[cfg(feature = "std")] 712 pub fn open_protected_with_vault( 713 path: impl AsRef<Path>, 714 vault: Arc<dyn RadrootsSecretVault>, 715 master_key_slot: impl Into<String>, 716 ) -> Result<Self, RadrootsSimplexAgentStoreError> { 717 let path = path.as_ref().to_path_buf(); 718 let key_source = RadrootsSimplexAgentStoreVaultKeySource { 719 vault, 720 master_key_slot: master_key_slot.into(), 721 }; 722 if !path.exists() { 723 return Ok(Self { 724 persistence: Some(RadrootsSimplexAgentStorePersistence::ProtectedSnapshot { 725 path, 726 key_source, 727 }), 728 ..Default::default() 729 }); 730 } 731 732 let snapshot = read_protected_snapshot(&path, &key_source)?; 733 let mut store = Self::from_snapshot(snapshot)?; 734 store.persistence = 735 Some(RadrootsSimplexAgentStorePersistence::ProtectedSnapshot { path, key_source }); 736 Ok(store) 737 } 738 739 #[cfg(feature = "std")] 740 pub fn set_persistence_path(&mut self, path: impl AsRef<Path>) { 741 self.persistence = Some(RadrootsSimplexAgentStorePersistence::PublicSnapshot { 742 path: path.as_ref().to_path_buf(), 743 }); 744 } 745 746 #[cfg(feature = "std")] 747 pub fn set_protected_persistence( 748 &mut self, 749 path: impl AsRef<Path>, 750 vault: Arc<dyn RadrootsSecretVault>, 751 master_key_slot: impl Into<String>, 752 ) { 753 self.persistence = Some(RadrootsSimplexAgentStorePersistence::ProtectedSnapshot { 754 path: path.as_ref().to_path_buf(), 755 key_source: RadrootsSimplexAgentStoreVaultKeySource { 756 vault, 757 master_key_slot: master_key_slot.into(), 758 }, 759 }); 760 } 761 762 #[cfg(feature = "std")] 763 pub fn flush(&self) -> Result<(), RadrootsSimplexAgentStoreError> { 764 let Some(persistence) = self.persistence.as_ref() else { 765 return Ok(()); 766 }; 767 match persistence { 768 RadrootsSimplexAgentStorePersistence::PublicSnapshot { path } => { 769 ensure_parent_dir(path)?; 770 let mut snapshot = self.snapshot()?; 771 let mut secrets = redact_snapshot_secrets(&mut snapshot)?; 772 if secrets.has_secret_material() { 773 let generation = compute_protected_generation(&snapshot, &secrets)?; 774 secrets.generation = generation.clone(); 775 snapshot.protected_secrets = Some(write_protected_secrets_snapshot( 776 path, &secrets, generation, 777 )?); 778 atomic_write_public_snapshot(path, &snapshot) 779 } else { 780 snapshot.protected_secrets = None; 781 atomic_write_public_snapshot(path, &snapshot)?; 782 remove_protected_secrets_files(path) 783 } 784 } 785 RadrootsSimplexAgentStorePersistence::ProtectedSnapshot { path, key_source } => { 786 ensure_parent_dir(path)?; 787 write_protected_snapshot(path, key_source, &self.snapshot()?)?; 788 remove_protected_secrets_files(path) 789 } 790 } 791 } 792 793 #[cfg(feature = "std")] 794 pub fn protected_secrets_path(path: impl AsRef<Path>) -> PathBuf { 795 protected_secrets_path(path.as_ref()) 796 } 797 798 #[cfg(feature = "std")] 799 pub fn protected_secrets_wrapping_key_path(path: impl AsRef<Path>) -> PathBuf { 800 protected_secrets_wrapping_key_path(path.as_ref()) 801 } 802 803 #[cfg(feature = "std")] 804 pub fn protected_secrets_diagnostics( 805 path: impl AsRef<Path>, 806 ) -> Result<RadrootsSimplexAgentStoreProtectedSecretsDiagnostics, RadrootsSimplexAgentStoreError> 807 { 808 protected_secrets_diagnostics(path.as_ref()) 809 } 810 811 pub fn create_connection( 812 &mut self, 813 mode: RadrootsSimplexAgentConnectionMode, 814 status: RadrootsSimplexAgentConnectionStatus, 815 invitation: Option<RadrootsSimplexAgentConnectionLink>, 816 ratchet_state: Option<RadrootsSimplexSmpRatchetState>, 817 ) -> RadrootsSimplexAgentConnectionRecord { 818 self.next_connection_sequence = self.next_connection_sequence.saturating_add(1); 819 let id = alloc::format!("conn-{}", self.next_connection_sequence); 820 let record = RadrootsSimplexAgentConnectionRecord { 821 id: id.clone(), 822 mode, 823 status, 824 invitation, 825 short_link: None, 826 queues: Vec::new(), 827 ratchet_state, 828 local_e2e_public_key: None, 829 local_e2e_private_key: None, 830 local_x3dh_key_1: None, 831 local_x3dh_key_2: None, 832 local_pq_keypair: None, 833 shared_secret: None, 834 delivery_cursor: RadrootsSimplexAgentDeliveryCursor { 835 last_sent_message_id: None, 836 last_received_message_id: None, 837 last_sent_message_hash: None, 838 last_received_message_hash: None, 839 }, 840 last_received_queue: None, 841 last_received_broker_message_id: None, 842 recent_messages: Vec::new(), 843 staged_outbound_message: None, 844 hello_sent: false, 845 hello_received: false, 846 }; 847 self.connections.insert(id, record.clone()); 848 record 849 } 850 851 pub fn connection( 852 &self, 853 connection_id: &str, 854 ) -> Result<&RadrootsSimplexAgentConnectionRecord, RadrootsSimplexAgentStoreError> { 855 self.connections 856 .get(connection_id) 857 .ok_or_else(|| RadrootsSimplexAgentStoreError::ConnectionNotFound(connection_id.into())) 858 } 859 860 pub fn connection_mut( 861 &mut self, 862 connection_id: &str, 863 ) -> Result<&mut RadrootsSimplexAgentConnectionRecord, RadrootsSimplexAgentStoreError> { 864 self.connections 865 .get_mut(connection_id) 866 .ok_or_else(|| RadrootsSimplexAgentStoreError::ConnectionNotFound(connection_id.into())) 867 } 868 869 pub fn set_status( 870 &mut self, 871 connection_id: &str, 872 status: RadrootsSimplexAgentConnectionStatus, 873 ) -> Result<(), RadrootsSimplexAgentStoreError> { 874 self.connection_mut(connection_id)?.status = status; 875 Ok(()) 876 } 877 878 pub fn add_queue( 879 &mut self, 880 connection_id: &str, 881 descriptor: RadrootsSimplexAgentQueueDescriptor, 882 role: RadrootsSimplexAgentQueueRole, 883 primary: bool, 884 auth_state: RadrootsSimplexAgentQueueAuthState, 885 ) -> Result<(), RadrootsSimplexAgentStoreError> { 886 let connection = self.connection_mut(connection_id)?; 887 let address = descriptor.queue_address(); 888 if let Some(queue) = connection 889 .queues 890 .iter_mut() 891 .find(|queue| queue.descriptor.queue_address() == address) 892 { 893 queue.descriptor = descriptor; 894 queue.entity_id = address.sender_id.clone(); 895 queue.role = role; 896 queue.primary = primary; 897 queue.auth_state = Some(auth_state); 898 return Ok(()); 899 } 900 connection.queues.push(RadrootsSimplexAgentQueueRecord { 901 entity_id: address.sender_id.clone(), 902 descriptor, 903 role, 904 subscribed: false, 905 primary, 906 tested: false, 907 auth_state: Some(auth_state), 908 delivery_private_key: None, 909 delivery_shared_secret: None, 910 }); 911 Ok(()) 912 } 913 914 pub fn generate_queue_auth_state( 915 &self, 916 ) -> Result<RadrootsSimplexAgentQueueAuthState, RadrootsSimplexAgentStoreError> { 917 let keypair = RadrootsSimplexSmpEd25519Keypair::generate().map_err(|error| { 918 RadrootsSimplexAgentStoreError::Persistence(format!( 919 "failed to generate SimpleX queue auth keypair: {error}" 920 )) 921 })?; 922 Ok(RadrootsSimplexAgentQueueAuthState { 923 public_key: keypair.public_key, 924 private_key: keypair.private_key, 925 }) 926 } 927 928 pub fn queue_record( 929 &self, 930 connection_id: &str, 931 queue_address: &RadrootsSimplexAgentQueueAddress, 932 ) -> Result<RadrootsSimplexAgentQueueRecord, RadrootsSimplexAgentStoreError> { 933 let connection = self.connection(connection_id)?; 934 connection 935 .queues 936 .iter() 937 .find(|queue| &queue.descriptor.queue_address() == queue_address) 938 .cloned() 939 .ok_or_else(|| RadrootsSimplexAgentStoreError::QueueNotFound(connection_id.into())) 940 } 941 942 pub fn mark_queue_subscribed( 943 &mut self, 944 connection_id: &str, 945 queue_address: &RadrootsSimplexAgentQueueAddress, 946 ) -> Result<(), RadrootsSimplexAgentStoreError> { 947 let connection = self.connection_mut(connection_id)?; 948 let Some(queue) = connection 949 .queues 950 .iter_mut() 951 .find(|queue| &queue.descriptor.queue_address() == queue_address) 952 else { 953 return Err(RadrootsSimplexAgentStoreError::QueueNotFound( 954 connection_id.into(), 955 )); 956 }; 957 queue.subscribed = true; 958 Ok(()) 959 } 960 961 pub fn mark_queue_tested( 962 &mut self, 963 connection_id: &str, 964 queue_address: &RadrootsSimplexAgentQueueAddress, 965 ) -> Result<(), RadrootsSimplexAgentStoreError> { 966 let connection = self.connection_mut(connection_id)?; 967 let Some(queue) = connection 968 .queues 969 .iter_mut() 970 .find(|queue| &queue.descriptor.queue_address() == queue_address) 971 else { 972 return Err(RadrootsSimplexAgentStoreError::QueueNotFound( 973 connection_id.into(), 974 )); 975 }; 976 queue.tested = true; 977 Ok(()) 978 } 979 980 pub fn primary_send_queue( 981 &self, 982 connection_id: &str, 983 ) -> Result<RadrootsSimplexAgentQueueRecord, RadrootsSimplexAgentStoreError> { 984 let connection = self.connection(connection_id)?; 985 connection 986 .queues 987 .iter() 988 .find(|queue| queue.role == RadrootsSimplexAgentQueueRole::Send && queue.primary) 989 .cloned() 990 .ok_or_else(|| { 991 RadrootsSimplexAgentStoreError::MissingPrimarySendQueue(connection_id.into()) 992 }) 993 } 994 995 pub fn receive_queues( 996 &self, 997 connection_id: &str, 998 ) -> Result<Vec<RadrootsSimplexAgentQueueRecord>, RadrootsSimplexAgentStoreError> { 999 let connection = self.connection(connection_id)?; 1000 Ok(connection 1001 .queues 1002 .iter() 1003 .filter(|queue| queue.role == RadrootsSimplexAgentQueueRole::Receive) 1004 .cloned() 1005 .collect()) 1006 } 1007 1008 pub fn subscribed_receive_servers(&self) -> Vec<RadrootsSimplexSmpServerAddress> { 1009 let mut servers = Vec::new(); 1010 for connection in self.connections.values() { 1011 for queue in &connection.queues { 1012 if queue.role == RadrootsSimplexAgentQueueRole::Receive 1013 && queue.subscribed 1014 && !servers.contains(&queue.descriptor.queue_uri.server) 1015 { 1016 servers.push(queue.descriptor.queue_uri.server.clone()); 1017 } 1018 } 1019 } 1020 servers 1021 } 1022 1023 pub fn subscribed_receive_queues(&self) -> Vec<(String, RadrootsSimplexAgentQueueAddress)> { 1024 let mut queues = Vec::new(); 1025 for connection in self.connections.values() { 1026 for queue in &connection.queues { 1027 if queue.role == RadrootsSimplexAgentQueueRole::Receive && queue.subscribed { 1028 queues.push((connection.id.clone(), queue.descriptor.queue_address())); 1029 } 1030 } 1031 } 1032 queues 1033 } 1034 1035 pub fn receive_queue_by_entity_id( 1036 &self, 1037 server: &RadrootsSimplexSmpServerAddress, 1038 entity_id: &[u8], 1039 ) -> Option<(String, RadrootsSimplexAgentQueueAddress)> { 1040 for connection in self.connections.values() { 1041 for queue in &connection.queues { 1042 if queue.role == RadrootsSimplexAgentQueueRole::Receive 1043 && queue.descriptor.queue_uri.server == *server 1044 && queue.entity_id == entity_id 1045 { 1046 return Some((connection.id.clone(), queue.descriptor.queue_address())); 1047 } 1048 } 1049 } 1050 None 1051 } 1052 1053 pub fn queue_auth_state( 1054 &self, 1055 connection_id: &str, 1056 queue_address: &RadrootsSimplexAgentQueueAddress, 1057 ) -> Result<RadrootsSimplexAgentQueueAuthState, RadrootsSimplexAgentStoreError> { 1058 self.queue_record(connection_id, queue_address)? 1059 .auth_state 1060 .ok_or_else(|| { 1061 RadrootsSimplexAgentStoreError::QueueAuthStateMissing(connection_id.into()) 1062 }) 1063 } 1064 1065 pub fn prepare_outbound_message( 1066 &mut self, 1067 connection_id: &str, 1068 message_hash: Vec<u8>, 1069 ) -> Result<RadrootsSimplexAgentPreparedOutboundMessage, RadrootsSimplexAgentStoreError> { 1070 let connection = self.connection_mut(connection_id)?; 1071 if connection.staged_outbound_message.is_some() { 1072 return Err(RadrootsSimplexAgentStoreError::PendingOutboundMessage( 1073 connection_id.into(), 1074 )); 1075 } 1076 let prepared = RadrootsSimplexAgentPreparedOutboundMessage { 1077 message_id: connection 1078 .delivery_cursor 1079 .last_sent_message_id 1080 .unwrap_or(0) 1081 .saturating_add(1), 1082 previous_message_hash: connection 1083 .delivery_cursor 1084 .last_sent_message_hash 1085 .clone() 1086 .unwrap_or_default(), 1087 message_hash: message_hash.clone(), 1088 }; 1089 connection.staged_outbound_message = Some(RadrootsSimplexAgentOutboundMessage { 1090 message_id: prepared.message_id, 1091 message_hash, 1092 }); 1093 Ok(prepared) 1094 } 1095 1096 pub fn confirm_outbound_message( 1097 &mut self, 1098 connection_id: &str, 1099 message_id: RadrootsSimplexAgentMessageId, 1100 ) -> Result<RadrootsSimplexAgentOutboundMessage, RadrootsSimplexAgentStoreError> { 1101 let connection = self.connection_mut(connection_id)?; 1102 let staged = connection.staged_outbound_message.take().ok_or_else(|| { 1103 RadrootsSimplexAgentStoreError::StagedOutboundMessageMissing(connection_id.into()) 1104 })?; 1105 if staged.message_id != message_id { 1106 connection.staged_outbound_message = Some(staged.clone()); 1107 return Err( 1108 RadrootsSimplexAgentStoreError::StagedOutboundMessageMismatch { 1109 connection_id: connection_id.into(), 1110 expected: staged.message_id, 1111 actual: message_id, 1112 }, 1113 ); 1114 } 1115 connection.delivery_cursor.last_sent_message_id = Some(staged.message_id); 1116 connection.delivery_cursor.last_sent_message_hash = Some(staged.message_hash.clone()); 1117 connection 1118 .recent_messages 1119 .push(RadrootsSimplexAgentRecentMessageRecord { 1120 message_id: staged.message_id, 1121 message_hash: staged.message_hash.clone(), 1122 inbound_queue: None, 1123 inbound_broker_message_id: None, 1124 }); 1125 Ok(staged) 1126 } 1127 1128 pub fn clear_staged_outbound_message( 1129 &mut self, 1130 connection_id: &str, 1131 message_id: RadrootsSimplexAgentMessageId, 1132 ) -> Result<RadrootsSimplexAgentOutboundMessage, RadrootsSimplexAgentStoreError> { 1133 let connection = self.connection_mut(connection_id)?; 1134 let staged = connection.staged_outbound_message.take().ok_or_else(|| { 1135 RadrootsSimplexAgentStoreError::StagedOutboundMessageMissing(connection_id.into()) 1136 })?; 1137 if staged.message_id != message_id { 1138 connection.staged_outbound_message = Some(staged.clone()); 1139 return Err( 1140 RadrootsSimplexAgentStoreError::StagedOutboundMessageMismatch { 1141 connection_id: connection_id.into(), 1142 expected: staged.message_id, 1143 actual: message_id, 1144 }, 1145 ); 1146 } 1147 Ok(staged) 1148 } 1149 1150 pub fn record_inbound_message( 1151 &mut self, 1152 connection_id: &str, 1153 queue_address: RadrootsSimplexAgentQueueAddress, 1154 broker_message_id: Vec<u8>, 1155 message_id: RadrootsSimplexAgentMessageId, 1156 message_hash: Vec<u8>, 1157 ) -> Result<(), RadrootsSimplexAgentStoreError> { 1158 let connection = self.connection_mut(connection_id)?; 1159 connection.delivery_cursor.last_received_message_id = Some(message_id); 1160 connection.delivery_cursor.last_received_message_hash = Some(message_hash.clone()); 1161 connection.last_received_queue = Some(queue_address.clone()); 1162 connection.last_received_broker_message_id = Some(broker_message_id.clone()); 1163 connection 1164 .recent_messages 1165 .push(RadrootsSimplexAgentRecentMessageRecord { 1166 message_id, 1167 message_hash, 1168 inbound_queue: Some(RadrootsSimplexAgentRecentQueueAddress::from_queue_address( 1169 &queue_address, 1170 )), 1171 inbound_broker_message_id: Some(broker_message_id), 1172 }); 1173 Ok(()) 1174 } 1175 1176 pub fn enqueue_command( 1177 &mut self, 1178 connection_id: &str, 1179 kind: RadrootsSimplexAgentPendingCommandKind, 1180 ready_at: u64, 1181 ) -> Result<RadrootsSimplexAgentPendingCommand, RadrootsSimplexAgentStoreError> { 1182 let _ = self.connection(connection_id)?; 1183 self.next_command_sequence = self.next_command_sequence.saturating_add(1); 1184 let command = RadrootsSimplexAgentPendingCommand { 1185 id: self.next_command_sequence, 1186 connection_id: connection_id.into(), 1187 kind, 1188 attempts: 0, 1189 ready_at, 1190 inflight: false, 1191 }; 1192 self.pending_commands.insert(command.id, command.clone()); 1193 Ok(command) 1194 } 1195 1196 pub fn has_pending_ack_message( 1197 &self, 1198 connection_id: &str, 1199 message_id: RadrootsSimplexAgentMessageId, 1200 message_hash: &[u8], 1201 ) -> bool { 1202 self.pending_commands.values().any(|command| { 1203 command.connection_id == connection_id 1204 && matches!( 1205 &command.kind, 1206 RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { 1207 receipt: Some(receipt), 1208 .. 1209 } 1210 if receipt.message_id == message_id && receipt.message_hash == message_hash 1211 ) 1212 }) 1213 } 1214 1215 pub fn has_pending_broker_ack( 1216 &self, 1217 connection_id: &str, 1218 queue_address: &RadrootsSimplexAgentQueueAddress, 1219 broker_message_id: &[u8], 1220 ) -> bool { 1221 self.pending_commands.values().any(|command| { 1222 command.connection_id == connection_id 1223 && matches!( 1224 &command.kind, 1225 RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { 1226 queue, 1227 broker_message_id: pending_broker_message_id, 1228 .. 1229 } if queue == queue_address && pending_broker_message_id == broker_message_id 1230 ) 1231 }) 1232 } 1233 1234 pub fn has_pending_subscribe_queue( 1235 &self, 1236 connection_id: &str, 1237 queue_address: &RadrootsSimplexAgentQueueAddress, 1238 ) -> bool { 1239 self.pending_commands.values().any(|command| { 1240 command.connection_id == connection_id 1241 && matches!( 1242 &command.kind, 1243 RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { queue } 1244 if queue == queue_address 1245 ) 1246 }) 1247 } 1248 1249 pub fn inbound_ack_target( 1250 &self, 1251 connection_id: &str, 1252 message_id: RadrootsSimplexAgentMessageId, 1253 message_hash: &[u8], 1254 ) -> Result<Option<(RadrootsSimplexAgentQueueAddress, Vec<u8>)>, RadrootsSimplexAgentStoreError> 1255 { 1256 let connection = self.connection(connection_id)?; 1257 Ok(connection 1258 .recent_messages 1259 .iter() 1260 .rev() 1261 .find(|message| { 1262 message.message_id == message_id && message.message_hash == message_hash 1263 }) 1264 .and_then(|message| { 1265 Some(( 1266 message.inbound_queue.clone()?.into_queue_address(), 1267 message.inbound_broker_message_id.clone()?, 1268 )) 1269 })) 1270 } 1271 1272 pub fn outbound_message_hash( 1273 &self, 1274 connection_id: &str, 1275 message_id: RadrootsSimplexAgentMessageId, 1276 ) -> Result<Option<Vec<u8>>, RadrootsSimplexAgentStoreError> { 1277 let connection = self.connection(connection_id)?; 1278 Ok(connection 1279 .recent_messages 1280 .iter() 1281 .rev() 1282 .find(|message| { 1283 message.message_id == message_id 1284 && message.inbound_queue.is_none() 1285 && message.inbound_broker_message_id.is_none() 1286 }) 1287 .map(|message| message.message_hash.clone())) 1288 } 1289 1290 pub fn take_ready_commands( 1291 &mut self, 1292 now: u64, 1293 limit: usize, 1294 ) -> Vec<RadrootsSimplexAgentPendingCommand> { 1295 let ready_ids = self 1296 .pending_commands 1297 .iter() 1298 .filter(|(_, command)| !command.inflight && command.ready_at <= now) 1299 .map(|(id, _)| *id) 1300 .take(limit) 1301 .collect::<Vec<_>>(); 1302 1303 ready_ids 1304 .into_iter() 1305 .filter_map(|id| { 1306 let command = self.pending_commands.get_mut(&id)?; 1307 command.inflight = true; 1308 command.attempts = command.attempts.saturating_add(1); 1309 Some(command.clone()) 1310 }) 1311 .collect() 1312 } 1313 1314 pub fn mark_command_delivered( 1315 &mut self, 1316 command_id: u64, 1317 ) -> Result<RadrootsSimplexAgentPendingCommand, RadrootsSimplexAgentStoreError> { 1318 self.pending_commands 1319 .remove(&command_id) 1320 .ok_or(RadrootsSimplexAgentStoreError::CommandNotFound(command_id)) 1321 } 1322 1323 pub fn mark_command_retry( 1324 &mut self, 1325 command_id: u64, 1326 ready_at: u64, 1327 ) -> Result<RadrootsSimplexAgentPendingCommand, RadrootsSimplexAgentStoreError> { 1328 let command = self 1329 .pending_commands 1330 .get_mut(&command_id) 1331 .ok_or(RadrootsSimplexAgentStoreError::CommandNotFound(command_id))?; 1332 command.inflight = false; 1333 command.ready_at = ready_at; 1334 Ok(command.clone()) 1335 } 1336 1337 pub fn mark_command_failed( 1338 &mut self, 1339 command_id: u64, 1340 ) -> Result<RadrootsSimplexAgentPendingCommand, RadrootsSimplexAgentStoreError> { 1341 self.pending_commands 1342 .remove(&command_id) 1343 .ok_or(RadrootsSimplexAgentStoreError::CommandNotFound(command_id)) 1344 } 1345 1346 #[cfg(feature = "std")] 1347 fn snapshot( 1348 &self, 1349 ) -> Result<RadrootsSimplexAgentStoreSnapshot, RadrootsSimplexAgentStoreError> { 1350 let connections = self 1351 .connections 1352 .values() 1353 .cloned() 1354 .map(connection_to_snapshot) 1355 .collect::<Result<Vec<_>, _>>()?; 1356 let pending_commands = self 1357 .pending_commands 1358 .values() 1359 .cloned() 1360 .map(command_to_snapshot) 1361 .collect::<Result<Vec<_>, _>>()?; 1362 Ok(RadrootsSimplexAgentStoreSnapshot { 1363 next_connection_sequence: self.next_connection_sequence, 1364 next_command_sequence: self.next_command_sequence, 1365 protected_secrets: None, 1366 connections, 1367 pending_commands, 1368 }) 1369 } 1370 1371 #[cfg(feature = "std")] 1372 fn from_snapshot( 1373 snapshot: RadrootsSimplexAgentStoreSnapshot, 1374 ) -> Result<Self, RadrootsSimplexAgentStoreError> { 1375 let mut connections = BTreeMap::new(); 1376 for connection in snapshot.connections { 1377 let record = connection_from_snapshot(connection)?; 1378 connections.insert(record.id.clone(), record); 1379 } 1380 let mut pending_commands = BTreeMap::new(); 1381 for command in snapshot.pending_commands { 1382 let record = command_from_snapshot(command)?; 1383 pending_commands.insert(record.id, record); 1384 } 1385 Ok(Self { 1386 next_connection_sequence: snapshot.next_connection_sequence, 1387 next_command_sequence: snapshot.next_command_sequence, 1388 connections, 1389 pending_commands, 1390 persistence: None, 1391 }) 1392 } 1393 } 1394 1395 #[cfg(feature = "std")] 1396 impl RadrootsSimplexAgentStoreSecretsSnapshot { 1397 fn has_secret_material(&self) -> bool { 1398 self.connections 1399 .iter() 1400 .any(RadrootsSimplexAgentConnectionSecretsSnapshot::has_secret_material) 1401 || self 1402 .pending_commands 1403 .iter() 1404 .any(RadrootsSimplexAgentPendingCommandSecretsSnapshot::has_secret_material) 1405 } 1406 } 1407 1408 #[cfg(feature = "std")] 1409 impl RadrootsSecretKeyWrapping for RadrootsSimplexAgentStoreVaultKeySource { 1410 type Error = RadrootsSimplexAgentStoreError; 1411 1412 fn wrap_data_key(&self, key_slot: &str, plaintext_key: &[u8]) -> Result<Vec<u8>, Self::Error> { 1413 let mut master_key = load_or_create_vault_master_key(self)?; 1414 let mut nonce = [0_u8; RADROOTS_PROTECTED_STORE_NONCE_LENGTH]; 1415 getrandom(&mut nonce).map_err(|_| { 1416 RadrootsSimplexAgentStoreError::Persistence( 1417 "entropy unavailable for SimpleX agent protected snapshot key wrapping".into(), 1418 ) 1419 })?; 1420 let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); 1421 let ciphertext = cipher 1422 .encrypt( 1423 XNonce::from_slice(&nonce), 1424 Payload { 1425 msg: plaintext_key, 1426 aad: key_slot.as_bytes(), 1427 }, 1428 ) 1429 .map_err(|_| { 1430 RadrootsSimplexAgentStoreError::Persistence( 1431 "failed to wrap SimpleX agent protected snapshot data key".into(), 1432 ) 1433 })?; 1434 master_key.zeroize(); 1435 let mut encoded = Vec::with_capacity(1 + nonce.len() + ciphertext.len()); 1436 encoded.push(RADROOTS_SIMPLEX_AGENT_STORE_WRAPPED_KEY_VERSION); 1437 encoded.extend_from_slice(&nonce); 1438 encoded.extend_from_slice(ciphertext.as_slice()); 1439 Ok(encoded) 1440 } 1441 1442 fn unwrap_data_key(&self, key_slot: &str, wrapped_key: &[u8]) -> Result<Vec<u8>, Self::Error> { 1443 if wrapped_key.len() <= 1 + RADROOTS_PROTECTED_STORE_NONCE_LENGTH { 1444 return Err(RadrootsSimplexAgentStoreError::Persistence( 1445 "SimpleX agent protected snapshot wrapped key is truncated".into(), 1446 )); 1447 } 1448 if wrapped_key[0] != RADROOTS_SIMPLEX_AGENT_STORE_WRAPPED_KEY_VERSION { 1449 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 1450 "unsupported SimpleX agent protected snapshot wrapped key version `{}`", 1451 wrapped_key[0] 1452 ))); 1453 } 1454 1455 let mut master_key = load_vault_master_key(self)?; 1456 let nonce_offset = 1; 1457 let ciphertext_offset = nonce_offset + RADROOTS_PROTECTED_STORE_NONCE_LENGTH; 1458 let cipher = XChaCha20Poly1305::new(Key::from_slice(&master_key)); 1459 let plaintext = cipher 1460 .decrypt( 1461 XNonce::from_slice(&wrapped_key[nonce_offset..ciphertext_offset]), 1462 Payload { 1463 msg: &wrapped_key[ciphertext_offset..], 1464 aad: key_slot.as_bytes(), 1465 }, 1466 ) 1467 .map_err(|_| { 1468 RadrootsSimplexAgentStoreError::Persistence( 1469 "failed to unwrap SimpleX agent protected snapshot data key".into(), 1470 ) 1471 })?; 1472 master_key.zeroize(); 1473 Ok(plaintext) 1474 } 1475 } 1476 1477 #[cfg(all(feature = "std", feature = "os-keyring"))] 1478 fn protected_snapshot_master_key_slot(path: &Path) -> String { 1479 let mut hasher = Sha256::new(); 1480 hasher.update(path.as_os_str().as_encoded_bytes()); 1481 format!( 1482 "radroots_simplex_agent_store_snapshot_{}", 1483 encode_digest_hex(hasher.finalize().as_slice()) 1484 ) 1485 } 1486 1487 #[cfg(feature = "std")] 1488 fn load_or_create_vault_master_key( 1489 source: &RadrootsSimplexAgentStoreVaultKeySource, 1490 ) -> Result<[u8; RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES], RadrootsSimplexAgentStoreError> 1491 { 1492 if let Some(encoded) = source 1493 .vault 1494 .load_secret(&source.master_key_slot) 1495 .map_err(|error| vault_access_error("load", error))? 1496 { 1497 return decode_vault_master_key(&encoded); 1498 } 1499 1500 let mut key = [0_u8; RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES]; 1501 getrandom(&mut key).map_err(|_| { 1502 RadrootsSimplexAgentStoreError::Persistence( 1503 "entropy unavailable for SimpleX agent protected snapshot master key".into(), 1504 ) 1505 })?; 1506 let encoded = encode_digest_hex(key.as_slice()); 1507 let store_result = source 1508 .vault 1509 .store_secret(&source.master_key_slot, &encoded) 1510 .map_err(|error| vault_access_error("store", error)); 1511 if let Err(error) = store_result { 1512 key.zeroize(); 1513 return Err(error); 1514 } 1515 Ok(key) 1516 } 1517 1518 #[cfg(feature = "std")] 1519 fn load_vault_master_key( 1520 source: &RadrootsSimplexAgentStoreVaultKeySource, 1521 ) -> Result<[u8; RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES], RadrootsSimplexAgentStoreError> 1522 { 1523 let encoded = source 1524 .vault 1525 .load_secret(&source.master_key_slot) 1526 .map_err(|error| vault_access_error("load", error))? 1527 .ok_or_else(|| { 1528 RadrootsSimplexAgentStoreError::Persistence( 1529 "SimpleX agent protected snapshot master key is missing".into(), 1530 ) 1531 })?; 1532 decode_vault_master_key(&encoded) 1533 } 1534 1535 #[cfg(feature = "std")] 1536 fn decode_vault_master_key( 1537 encoded: &str, 1538 ) -> Result<[u8; RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES], RadrootsSimplexAgentStoreError> 1539 { 1540 if encoded.len() != RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES * 2 { 1541 return Err(RadrootsSimplexAgentStoreError::Persistence( 1542 "SimpleX agent protected snapshot master key has invalid length".into(), 1543 )); 1544 } 1545 let mut key = [0_u8; RADROOTS_SIMPLEX_AGENT_STORE_VAULT_MASTER_KEY_BYTES]; 1546 for (index, chunk) in encoded.as_bytes().chunks_exact(2).enumerate() { 1547 key[index] = (decode_ascii_hex_nibble(chunk[0])? << 4) | decode_ascii_hex_nibble(chunk[1])?; 1548 } 1549 Ok(key) 1550 } 1551 1552 #[cfg(feature = "std")] 1553 fn decode_ascii_hex_nibble(value: u8) -> Result<u8, RadrootsSimplexAgentStoreError> { 1554 match value { 1555 b'0'..=b'9' => Ok(value - b'0'), 1556 b'a'..=b'f' => Ok(value - b'a' + 10), 1557 b'A'..=b'F' => Ok(value - b'A' + 10), 1558 _ => Err(RadrootsSimplexAgentStoreError::Persistence( 1559 "SimpleX agent protected snapshot master key is not hex encoded".into(), 1560 )), 1561 } 1562 } 1563 1564 #[cfg(feature = "std")] 1565 fn vault_access_error( 1566 action: &str, 1567 source: RadrootsSecretVaultAccessError, 1568 ) -> RadrootsSimplexAgentStoreError { 1569 RadrootsSimplexAgentStoreError::Persistence(format!( 1570 "failed to {action} SimpleX agent protected snapshot key: {source}" 1571 )) 1572 } 1573 1574 #[cfg(feature = "std")] 1575 impl RadrootsSimplexAgentConnectionSecretsSnapshot { 1576 fn has_secret_material(&self) -> bool { 1577 self.short_link_link_key.is_some() 1578 || self.short_link_private_signature_key.is_some() 1579 || self.local_e2e_private_key.is_some() 1580 || self.local_x3dh_key_1_private_key.is_some() 1581 || self.local_x3dh_key_2_private_key.is_some() 1582 || self.local_pq_private_key.is_some() 1583 || self.shared_secret.is_some() 1584 || self 1585 .queues 1586 .iter() 1587 .any(RadrootsSimplexAgentQueueSecretsSnapshot::has_secret_material) 1588 || self 1589 .ratchet_state 1590 .as_ref() 1591 .is_some_and(RadrootsSimplexAgentRatchetSecretsSnapshot::has_secret_material) 1592 } 1593 } 1594 1595 #[cfg(feature = "std")] 1596 impl RadrootsSimplexAgentQueueSecretsSnapshot { 1597 fn has_secret_material(&self) -> bool { 1598 self.auth_private_key.is_some() 1599 || self.delivery_private_key.is_some() 1600 || self.delivery_shared_secret.is_some() 1601 } 1602 } 1603 1604 #[cfg(feature = "std")] 1605 impl RadrootsSimplexAgentRatchetSecretsSnapshot { 1606 fn has_secret_material(&self) -> bool { 1607 self.current_pq_shared_secret.is_some() 1608 || self.local_pq_private_key.is_some() 1609 || self.local_dh_private_key.is_some() 1610 || self.official_root_key.is_some() 1611 || self.official_sending_chain_key.is_some() 1612 || self.official_receiving_chain_key.is_some() 1613 || self.official_sending_header_key.is_some() 1614 || self.official_receiving_header_key.is_some() 1615 || self.official_next_sending_header_key.is_some() 1616 || self.official_next_receiving_header_key.is_some() 1617 || !self.official_skipped_message_keys.is_empty() 1618 } 1619 } 1620 1621 #[cfg(feature = "std")] 1622 impl RadrootsSimplexAgentPendingCommandSecretsSnapshot { 1623 fn has_secret_material(&self) -> bool { 1624 self.short_invitation_link_key.is_some() 1625 } 1626 } 1627 1628 #[cfg(feature = "std")] 1629 fn protected_secrets_path(path: &Path) -> PathBuf { 1630 sidecar_path(path, RADROOTS_PROTECTED_FILE_SECRET_SUFFIX) 1631 } 1632 1633 #[cfg(feature = "std")] 1634 fn protected_secrets_wrapping_key_path(path: &Path) -> PathBuf { 1635 sidecar_path(path, RADROOTS_PROTECTED_FILE_WRAPPING_KEY_FILE) 1636 } 1637 1638 #[cfg(feature = "std")] 1639 fn protected_secrets_diagnostics( 1640 path: &Path, 1641 ) -> Result<RadrootsSimplexAgentStoreProtectedSecretsDiagnostics, RadrootsSimplexAgentStoreError> { 1642 let store_path = path.to_path_buf(); 1643 let protected_secrets_path = protected_secrets_path(path); 1644 let wrapping_key_path = protected_secrets_wrapping_key_path(path); 1645 let public_snapshot_exists = path.exists(); 1646 let mut protected_secrets_configured = false; 1647 let mut protected_connection_count = 0; 1648 let mut protected_pending_command_count = 0; 1649 let mut protected_generation = None; 1650 let mut protected_envelope_suffix = None; 1651 let mut protected_wrapping_key_suffix = None; 1652 1653 if public_snapshot_exists { 1654 let raw = fs::read(path).map_err(|error| { 1655 RadrootsSimplexAgentStoreError::Persistence(format!( 1656 "failed to read SimpleX agent store snapshot `{}`: {error}", 1657 path.display() 1658 )) 1659 })?; 1660 let snapshot: RadrootsSimplexAgentStoreSnapshot = 1661 serde_json::from_slice(&raw).map_err(|error| { 1662 RadrootsSimplexAgentStoreError::Persistence(format!( 1663 "failed to parse SimpleX agent store snapshot `{}`: {error}", 1664 path.display() 1665 )) 1666 })?; 1667 let protected_configured = snapshot.protected_secrets.is_some(); 1668 validate_public_snapshot_secret_posture(&snapshot, protected_configured)?; 1669 if let Some(protected) = snapshot.protected_secrets.as_ref() { 1670 protected_secrets_configured = true; 1671 let secrets = read_protected_secrets_snapshot(path, &snapshot)?; 1672 protected_connection_count = secrets.connections.len(); 1673 protected_pending_command_count = secrets.pending_commands.len(); 1674 protected_generation = Some(protected.generation.clone()); 1675 protected_envelope_suffix = Some(protected.envelope_suffix.clone()); 1676 protected_wrapping_key_suffix = Some(protected.wrapping_key_suffix.clone()); 1677 } 1678 } 1679 1680 Ok(RadrootsSimplexAgentStoreProtectedSecretsDiagnostics { 1681 store_path, 1682 protected_secrets_path: protected_secrets_path.clone(), 1683 wrapping_key_path: wrapping_key_path.clone(), 1684 public_snapshot_exists, 1685 protected_secrets_configured, 1686 protected_secrets_exists: protected_secrets_path.exists(), 1687 wrapping_key_exists: wrapping_key_path.exists(), 1688 protected_connection_count, 1689 protected_pending_command_count, 1690 protected_generation, 1691 protected_envelope_suffix, 1692 protected_wrapping_key_suffix, 1693 }) 1694 } 1695 1696 #[cfg(feature = "std")] 1697 fn redact_snapshot_secrets( 1698 snapshot: &mut RadrootsSimplexAgentStoreSnapshot, 1699 ) -> Result<RadrootsSimplexAgentStoreSecretsSnapshot, RadrootsSimplexAgentStoreError> { 1700 let connections = snapshot 1701 .connections 1702 .iter_mut() 1703 .map(redact_connection_secrets) 1704 .collect::<Result<Vec<_>, _>>()?; 1705 let pending_commands = snapshot 1706 .pending_commands 1707 .iter_mut() 1708 .map(redact_pending_command_secrets) 1709 .filter(RadrootsSimplexAgentPendingCommandSecretsSnapshot::has_secret_material) 1710 .collect::<Vec<_>>(); 1711 Ok(RadrootsSimplexAgentStoreSecretsSnapshot { 1712 version: RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_VERSION, 1713 generation: String::new(), 1714 connections, 1715 pending_commands, 1716 }) 1717 } 1718 1719 #[cfg(feature = "std")] 1720 fn redact_connection_secrets( 1721 connection: &mut RadrootsSimplexAgentConnectionSnapshot, 1722 ) -> Result<RadrootsSimplexAgentConnectionSecretsSnapshot, RadrootsSimplexAgentStoreError> { 1723 let queues = connection 1724 .queues 1725 .iter_mut() 1726 .map(redact_queue_secrets) 1727 .collect::<Result<Vec<_>, _>>()?; 1728 Ok(RadrootsSimplexAgentConnectionSecretsSnapshot { 1729 id: connection.id.clone(), 1730 short_link_link_key: connection 1731 .short_link 1732 .as_mut() 1733 .and_then(|short_link| take_non_empty_vec(&mut short_link.link_key)), 1734 short_link_private_signature_key: connection 1735 .short_link 1736 .as_mut() 1737 .and_then(|short_link| take_non_empty_vec(&mut short_link.link_private_signature_key)), 1738 queues, 1739 ratchet_state: connection 1740 .ratchet_state 1741 .as_mut() 1742 .map(redact_ratchet_secrets), 1743 local_e2e_private_key: connection.local_e2e_private_key.take(), 1744 local_x3dh_key_1_private_key: redact_x3dh_keypair_private(&mut connection.local_x3dh_key_1), 1745 local_x3dh_key_2_private_key: redact_x3dh_keypair_private(&mut connection.local_x3dh_key_2), 1746 local_pq_private_key: redact_pq_keypair_private(&mut connection.local_pq_keypair), 1747 shared_secret: connection.shared_secret.take(), 1748 }) 1749 } 1750 1751 #[cfg(feature = "std")] 1752 fn redact_queue_secrets( 1753 queue: &mut RadrootsSimplexAgentQueueRecordSnapshot, 1754 ) -> Result<RadrootsSimplexAgentQueueSecretsSnapshot, RadrootsSimplexAgentStoreError> { 1755 let descriptor = queue_descriptor_from_snapshot(queue.descriptor.clone())?; 1756 Ok(RadrootsSimplexAgentQueueSecretsSnapshot { 1757 entity_id: queue.entity_id.clone(), 1758 role: queue.role.clone(), 1759 queue_address: Some(queue_address_to_snapshot(descriptor.queue_address())), 1760 auth_private_key: queue 1761 .auth_state 1762 .as_mut() 1763 .and_then(|auth| take_non_empty_vec(&mut auth.private_key)), 1764 delivery_private_key: queue.delivery_private_key.take(), 1765 delivery_shared_secret: queue.delivery_shared_secret.take(), 1766 }) 1767 } 1768 1769 #[cfg(feature = "std")] 1770 fn redact_ratchet_secrets( 1771 ratchet: &mut RadrootsSimplexAgentRatchetStateSnapshot, 1772 ) -> RadrootsSimplexAgentRatchetSecretsSnapshot { 1773 RadrootsSimplexAgentRatchetSecretsSnapshot { 1774 current_pq_shared_secret: ratchet.current_pq_shared_secret.take(), 1775 local_pq_private_key: ratchet.local_pq_private_key.take(), 1776 local_dh_private_key: ratchet.local_dh_private_key.take(), 1777 official_root_key: ratchet.official_root_key.take(), 1778 official_sending_chain_key: ratchet.official_sending_chain_key.take(), 1779 official_receiving_chain_key: ratchet.official_receiving_chain_key.take(), 1780 official_sending_header_key: ratchet.official_sending_header_key.take(), 1781 official_receiving_header_key: ratchet.official_receiving_header_key.take(), 1782 official_next_sending_header_key: ratchet.official_next_sending_header_key.take(), 1783 official_next_receiving_header_key: ratchet.official_next_receiving_header_key.take(), 1784 official_skipped_message_keys: core::mem::take(&mut ratchet.official_skipped_message_keys), 1785 } 1786 } 1787 1788 #[cfg(feature = "std")] 1789 fn redact_pending_command_secrets( 1790 command: &mut RadrootsSimplexAgentPendingCommandSnapshot, 1791 ) -> RadrootsSimplexAgentPendingCommandSecretsSnapshot { 1792 RadrootsSimplexAgentPendingCommandSecretsSnapshot { 1793 id: command.id, 1794 connection_id: command.connection_id.clone(), 1795 short_invitation_link_key: redact_pending_command_short_invitation_link_key( 1796 &mut command.kind, 1797 ), 1798 short_invitation_sender_auth_private_key: 1799 redact_pending_command_short_invitation_sender_auth_private_key(&mut command.kind), 1800 } 1801 } 1802 1803 #[cfg(feature = "std")] 1804 fn redact_pending_command_short_invitation_link_key( 1805 kind: &mut RadrootsSimplexAgentPendingCommandKindSnapshot, 1806 ) -> Option<Vec<u8>> { 1807 match kind { 1808 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 1809 invitation, 1810 .. 1811 } 1812 | RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueLinkData { invitation, .. } => { 1813 take_non_empty_vec(&mut invitation.link_key) 1814 } 1815 _ => None, 1816 } 1817 } 1818 1819 #[cfg(feature = "std")] 1820 fn redact_pending_command_short_invitation_sender_auth_private_key( 1821 kind: &mut RadrootsSimplexAgentPendingCommandKindSnapshot, 1822 ) -> Option<Vec<u8>> { 1823 match kind { 1824 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 1825 sender_auth_private_key, 1826 .. 1827 } => take_non_empty_vec(sender_auth_private_key), 1828 _ => None, 1829 } 1830 } 1831 1832 #[cfg(feature = "std")] 1833 fn redact_x3dh_keypair_private( 1834 keypair: &mut Option<RadrootsSimplexAgentX3dhKeypair>, 1835 ) -> Option<Vec<u8>> { 1836 keypair 1837 .as_mut() 1838 .and_then(|keypair| take_non_empty_vec(&mut keypair.private_key)) 1839 } 1840 1841 #[cfg(feature = "std")] 1842 fn redact_pq_keypair_private( 1843 keypair: &mut Option<RadrootsSimplexAgentPqKeypair>, 1844 ) -> Option<Vec<u8>> { 1845 keypair 1846 .as_mut() 1847 .and_then(|keypair| take_non_empty_vec(&mut keypair.private_key)) 1848 } 1849 1850 #[cfg(feature = "std")] 1851 fn take_non_empty_vec(value: &mut Vec<u8>) -> Option<Vec<u8>> { 1852 if value.is_empty() { 1853 None 1854 } else { 1855 Some(core::mem::take(value)) 1856 } 1857 } 1858 1859 #[cfg(feature = "std")] 1860 fn compute_protected_generation( 1861 snapshot: &RadrootsSimplexAgentStoreSnapshot, 1862 secrets: &RadrootsSimplexAgentStoreSecretsSnapshot, 1863 ) -> Result<String, RadrootsSimplexAgentStoreError> { 1864 let mut public_snapshot = snapshot.clone(); 1865 public_snapshot.protected_secrets = None; 1866 let mut secrets_snapshot = secrets.clone(); 1867 secrets_snapshot.generation.clear(); 1868 let public_encoded = serde_json::to_vec(&public_snapshot).map_err(|error| { 1869 RadrootsSimplexAgentStoreError::Persistence(format!( 1870 "failed to encode SimpleX agent public generation input: {error}" 1871 )) 1872 })?; 1873 let secrets_encoded = serde_json::to_vec(&secrets_snapshot).map_err(|error| { 1874 RadrootsSimplexAgentStoreError::Persistence(format!( 1875 "failed to encode SimpleX agent protected generation input: {error}" 1876 )) 1877 })?; 1878 let mut hasher = Sha256::new(); 1879 hasher.update(public_encoded); 1880 hasher.update(b"\n"); 1881 hasher.update(secrets_encoded); 1882 Ok(encode_digest_hex(hasher.finalize().as_slice())) 1883 } 1884 1885 #[cfg(feature = "std")] 1886 fn encode_digest_hex(bytes: &[u8]) -> String { 1887 const HEX: &[u8; 16] = b"0123456789abcdef"; 1888 let mut output = String::with_capacity(bytes.len() * 2); 1889 for byte in bytes { 1890 output.push(HEX[(byte >> 4) as usize] as char); 1891 output.push(HEX[(byte & 0x0f) as usize] as char); 1892 } 1893 output 1894 } 1895 1896 #[cfg(feature = "std")] 1897 fn atomic_write_public_snapshot( 1898 path: &Path, 1899 snapshot: &RadrootsSimplexAgentStoreSnapshot, 1900 ) -> Result<(), RadrootsSimplexAgentStoreError> { 1901 let mut encoded = serde_json::to_vec_pretty(snapshot).map_err(|error| { 1902 RadrootsSimplexAgentStoreError::Persistence(format!( 1903 "failed to serialize SimpleX agent store snapshot `{}`: {error}", 1904 path.display() 1905 )) 1906 })?; 1907 encoded.push(b'\n'); 1908 atomic_write_bytes(path, encoded.as_slice(), false) 1909 } 1910 1911 #[cfg(feature = "std")] 1912 fn write_protected_snapshot( 1913 path: &Path, 1914 key_source: &RadrootsSimplexAgentStoreVaultKeySource, 1915 snapshot: &RadrootsSimplexAgentStoreSnapshot, 1916 ) -> Result<(), RadrootsSimplexAgentStoreError> { 1917 let mut protected_snapshot = snapshot.clone(); 1918 protected_snapshot.protected_secrets = None; 1919 let plaintext = serde_json::to_vec(&protected_snapshot).map_err(|error| { 1920 RadrootsSimplexAgentStoreError::Persistence(format!( 1921 "failed to serialize SimpleX agent protected snapshot `{}`: {error}", 1922 path.display() 1923 )) 1924 })?; 1925 let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( 1926 key_source, 1927 RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SNAPSHOT_KEY_SLOT, 1928 &plaintext, 1929 ) 1930 .map_err(|error| { 1931 RadrootsSimplexAgentStoreError::Persistence(format!( 1932 "failed to seal SimpleX agent protected snapshot `{}`: {error}", 1933 path.display() 1934 )) 1935 })?; 1936 let encoded = envelope.encode_json().map_err(|error| { 1937 RadrootsSimplexAgentStoreError::Persistence(format!( 1938 "failed to encode SimpleX agent protected snapshot `{}`: {error}", 1939 path.display() 1940 )) 1941 })?; 1942 atomic_write_bytes(path, encoded.as_slice(), true) 1943 } 1944 1945 #[cfg(feature = "std")] 1946 fn read_protected_snapshot( 1947 path: &Path, 1948 key_source: &RadrootsSimplexAgentStoreVaultKeySource, 1949 ) -> Result<RadrootsSimplexAgentStoreSnapshot, RadrootsSimplexAgentStoreError> { 1950 let encoded = fs::read(path).map_err(|error| { 1951 RadrootsSimplexAgentStoreError::Persistence(format!( 1952 "failed to read SimpleX agent protected snapshot `{}`: {error}", 1953 path.display() 1954 )) 1955 })?; 1956 let envelope = RadrootsProtectedStoreEnvelope::decode_json(&encoded).map_err(|error| { 1957 RadrootsSimplexAgentStoreError::Persistence(format!( 1958 "failed to decode SimpleX agent protected snapshot `{}`: {error}", 1959 path.display() 1960 )) 1961 })?; 1962 if envelope.header.key_slot != RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SNAPSHOT_KEY_SLOT { 1963 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 1964 "SimpleX agent protected snapshot `{}` uses key slot `{}`", 1965 path.display(), 1966 envelope.header.key_slot 1967 ))); 1968 } 1969 let plaintext = envelope 1970 .open_with_wrapped_key(key_source) 1971 .map_err(|error| { 1972 RadrootsSimplexAgentStoreError::Persistence(format!( 1973 "failed to open SimpleX agent protected snapshot `{}`: {error}", 1974 path.display() 1975 )) 1976 })?; 1977 let snapshot: RadrootsSimplexAgentStoreSnapshot = 1978 serde_json::from_slice(&plaintext).map_err(|error| { 1979 RadrootsSimplexAgentStoreError::Persistence(format!( 1980 "failed to parse SimpleX agent protected snapshot `{}`: {error}", 1981 path.display() 1982 )) 1983 })?; 1984 if snapshot.protected_secrets.is_some() { 1985 return Err(RadrootsSimplexAgentStoreError::Persistence( 1986 "SimpleX agent protected snapshot must not reference protected sidecar secrets".into(), 1987 )); 1988 } 1989 Ok(snapshot) 1990 } 1991 1992 #[cfg(feature = "std")] 1993 fn ensure_parent_dir(path: &Path) -> Result<(), RadrootsSimplexAgentStoreError> { 1994 if let Some(parent) = path.parent() 1995 && !parent.as_os_str().is_empty() 1996 { 1997 fs::create_dir_all(parent).map_err(|error| { 1998 RadrootsSimplexAgentStoreError::Persistence(format!( 1999 "failed to create SimpleX agent store directory `{}`: {error}", 2000 parent.display() 2001 )) 2002 })?; 2003 } 2004 Ok(()) 2005 } 2006 2007 #[cfg(feature = "std")] 2008 fn atomic_write_bytes( 2009 path: &Path, 2010 bytes: &[u8], 2011 secret_permissions: bool, 2012 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2013 let temp_path = temp_sibling_path(path); 2014 let result = atomic_write_bytes_inner(path, &temp_path, bytes, secret_permissions); 2015 if result.is_err() { 2016 let _ = fs::remove_file(&temp_path); 2017 } 2018 result 2019 } 2020 2021 #[cfg(feature = "std")] 2022 fn atomic_write_bytes_inner( 2023 path: &Path, 2024 temp_path: &Path, 2025 bytes: &[u8], 2026 secret_permissions: bool, 2027 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2028 remove_file_if_exists(temp_path)?; 2029 let mut file = fs::OpenOptions::new() 2030 .write(true) 2031 .create_new(true) 2032 .open(temp_path) 2033 .map_err(|error| { 2034 RadrootsSimplexAgentStoreError::Persistence(format!( 2035 "failed to create SimpleX agent store temp file `{}`: {error}", 2036 temp_path.display() 2037 )) 2038 })?; 2039 file.write_all(bytes).map_err(|error| { 2040 RadrootsSimplexAgentStoreError::Persistence(format!( 2041 "failed to write SimpleX agent store temp file `{}`: {error}", 2042 temp_path.display() 2043 )) 2044 })?; 2045 file.sync_all().map_err(|error| { 2046 RadrootsSimplexAgentStoreError::Persistence(format!( 2047 "failed to sync SimpleX agent store temp file `{}`: {error}", 2048 temp_path.display() 2049 )) 2050 })?; 2051 drop(file); 2052 if secret_permissions { 2053 set_secret_permissions(temp_path)?; 2054 } 2055 fs::rename(temp_path, path).map_err(|error| { 2056 RadrootsSimplexAgentStoreError::Persistence(format!( 2057 "failed to replace SimpleX agent store file `{}` from temp `{}`: {error}", 2058 path.display(), 2059 temp_path.display() 2060 )) 2061 }) 2062 } 2063 2064 #[cfg(feature = "std")] 2065 fn temp_sibling_path(path: &Path) -> PathBuf { 2066 let mut value = OsString::from(path.as_os_str()); 2067 let unique = SystemTime::now() 2068 .duration_since(UNIX_EPOCH) 2069 .map(|duration| duration.as_nanos()) 2070 .unwrap_or_default(); 2071 value.push(format!(".tmp.{}.{}", std::process::id(), unique)); 2072 PathBuf::from(value) 2073 } 2074 2075 #[cfg(feature = "std")] 2076 fn write_protected_secrets_snapshot( 2077 path: &Path, 2078 secrets: &RadrootsSimplexAgentStoreSecretsSnapshot, 2079 generation: String, 2080 ) -> Result<RadrootsSimplexAgentStoreProtectedSecretsRef, RadrootsSimplexAgentStoreError> { 2081 let protected_path = protected_secrets_path(path); 2082 if let Some(parent) = protected_path.parent() 2083 && !parent.as_os_str().is_empty() 2084 { 2085 fs::create_dir_all(parent).map_err(|error| { 2086 RadrootsSimplexAgentStoreError::Persistence(format!( 2087 "failed to create SimpleX agent protected store directory `{}`: {error}", 2088 parent.display() 2089 )) 2090 })?; 2091 } 2092 2093 let payload = serde_json::to_vec(secrets).map_err(|error| { 2094 RadrootsSimplexAgentStoreError::Persistence(format!( 2095 "failed to serialize SimpleX agent protected secrets snapshot `{}`: {error}", 2096 protected_path.display() 2097 )) 2098 })?; 2099 let key_source = RadrootsProtectedFileKeySource::new(protected_secrets_wrapping_key_path(path)); 2100 let envelope = RadrootsProtectedStoreEnvelope::seal_with_wrapped_key( 2101 &key_source, 2102 RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_KEY_SLOT, 2103 &payload, 2104 ) 2105 .map_err(|error| { 2106 RadrootsSimplexAgentStoreError::Persistence(format!( 2107 "failed to seal SimpleX agent protected secrets snapshot `{}`: {error}", 2108 protected_path.display() 2109 )) 2110 })?; 2111 let encoded = envelope.encode_json().map_err(|error| { 2112 RadrootsSimplexAgentStoreError::Persistence(format!( 2113 "failed to encode SimpleX agent protected secrets snapshot `{}`: {error}", 2114 protected_path.display() 2115 )) 2116 })?; 2117 atomic_write_bytes(&protected_path, encoded.as_slice(), true)?; 2118 2119 Ok(RadrootsSimplexAgentStoreProtectedSecretsRef { 2120 version: RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_VERSION, 2121 generation, 2122 envelope_suffix: RADROOTS_PROTECTED_FILE_SECRET_SUFFIX.into(), 2123 wrapping_key_suffix: RADROOTS_PROTECTED_FILE_WRAPPING_KEY_FILE.into(), 2124 key_slot: RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_KEY_SLOT.into(), 2125 connection_count: secrets.connections.len(), 2126 pending_command_count: secrets.pending_commands.len(), 2127 }) 2128 } 2129 2130 #[cfg(feature = "std")] 2131 fn read_protected_secrets_snapshot( 2132 path: &Path, 2133 snapshot: &RadrootsSimplexAgentStoreSnapshot, 2134 ) -> Result<RadrootsSimplexAgentStoreSecretsSnapshot, RadrootsSimplexAgentStoreError> { 2135 let protected_ref = snapshot.protected_secrets.as_ref().ok_or_else(|| { 2136 RadrootsSimplexAgentStoreError::Persistence( 2137 "SimpleX agent store snapshot does not reference protected secrets".into(), 2138 ) 2139 })?; 2140 validate_protected_secrets_ref(protected_ref)?; 2141 2142 let protected_path = protected_secrets_path(path); 2143 let encoded = fs::read(&protected_path).map_err(|error| { 2144 RadrootsSimplexAgentStoreError::Persistence(format!( 2145 "failed to read SimpleX agent protected secrets snapshot `{}`: {error}", 2146 protected_path.display() 2147 )) 2148 })?; 2149 let envelope = RadrootsProtectedStoreEnvelope::decode_json(&encoded).map_err(|error| { 2150 RadrootsSimplexAgentStoreError::Persistence(format!( 2151 "failed to decode SimpleX agent protected secrets snapshot `{}`: {error}", 2152 protected_path.display() 2153 )) 2154 })?; 2155 if envelope.header.key_slot != RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_KEY_SLOT { 2156 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2157 "SimpleX agent protected secrets snapshot `{}` uses key slot `{}`", 2158 protected_path.display(), 2159 envelope.header.key_slot 2160 ))); 2161 } 2162 2163 let key_source = RadrootsProtectedFileKeySource::new(protected_secrets_wrapping_key_path(path)); 2164 let plaintext = envelope 2165 .open_with_wrapped_key(&key_source) 2166 .map_err(|error| { 2167 RadrootsSimplexAgentStoreError::Persistence(format!( 2168 "failed to open SimpleX agent protected secrets snapshot `{}`: {error}", 2169 protected_path.display() 2170 )) 2171 })?; 2172 let secrets: RadrootsSimplexAgentStoreSecretsSnapshot = serde_json::from_slice(&plaintext) 2173 .map_err(|error| { 2174 RadrootsSimplexAgentStoreError::Persistence(format!( 2175 "failed to parse SimpleX agent protected secrets snapshot `{}`: {error}", 2176 protected_path.display() 2177 )) 2178 })?; 2179 if secrets.version != RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_VERSION { 2180 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2181 "unsupported SimpleX agent protected secrets version `{}`", 2182 secrets.version 2183 ))); 2184 } 2185 if secrets.generation != protected_ref.generation { 2186 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2187 "SimpleX agent protected secrets generation `{}` does not match public snapshot generation `{}`", 2188 secrets.generation, protected_ref.generation 2189 ))); 2190 } 2191 if secrets.connections.len() != protected_ref.connection_count { 2192 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2193 "SimpleX agent protected secrets connection count `{}` does not match public snapshot count `{}`", 2194 secrets.connections.len(), 2195 protected_ref.connection_count 2196 ))); 2197 } 2198 if secrets.pending_commands.len() != protected_ref.pending_command_count { 2199 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2200 "SimpleX agent protected secrets pending command count `{}` does not match public snapshot count `{}`", 2201 secrets.pending_commands.len(), 2202 protected_ref.pending_command_count 2203 ))); 2204 } 2205 let expected_generation = compute_protected_generation(snapshot, &secrets)?; 2206 if expected_generation != protected_ref.generation { 2207 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2208 "SimpleX agent protected secrets generation `{}` does not match protected content generation `{expected_generation}`", 2209 protected_ref.generation 2210 ))); 2211 } 2212 Ok(secrets) 2213 } 2214 2215 #[cfg(feature = "std")] 2216 fn validate_protected_secrets_ref( 2217 protected_ref: &RadrootsSimplexAgentStoreProtectedSecretsRef, 2218 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2219 if protected_ref.version != RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_VERSION { 2220 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2221 "unsupported SimpleX agent protected secrets reference version `{}`", 2222 protected_ref.version 2223 ))); 2224 } 2225 if protected_ref.generation.len() != 64 2226 || !protected_ref 2227 .generation 2228 .bytes() 2229 .all(|byte| byte.is_ascii_hexdigit()) 2230 { 2231 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2232 "invalid SimpleX agent protected secrets generation `{}`", 2233 protected_ref.generation 2234 ))); 2235 } 2236 if protected_ref.envelope_suffix != RADROOTS_PROTECTED_FILE_SECRET_SUFFIX { 2237 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2238 "unsupported SimpleX agent protected secrets envelope suffix `{}`", 2239 protected_ref.envelope_suffix 2240 ))); 2241 } 2242 if protected_ref.wrapping_key_suffix != RADROOTS_PROTECTED_FILE_WRAPPING_KEY_FILE { 2243 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2244 "unsupported SimpleX agent protected secrets wrapping key suffix `{}`", 2245 protected_ref.wrapping_key_suffix 2246 ))); 2247 } 2248 if protected_ref.key_slot != RADROOTS_SIMPLEX_AGENT_STORE_PROTECTED_SECRETS_KEY_SLOT { 2249 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2250 "unsupported SimpleX agent protected secrets key slot `{}`", 2251 protected_ref.key_slot 2252 ))); 2253 } 2254 Ok(()) 2255 } 2256 2257 #[cfg(feature = "std")] 2258 fn validate_public_snapshot_secret_posture( 2259 snapshot: &RadrootsSimplexAgentStoreSnapshot, 2260 protected_secrets_configured: bool, 2261 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2262 for connection in &snapshot.connections { 2263 validate_public_connection_secret_posture(connection, protected_secrets_configured)?; 2264 } 2265 for command in &snapshot.pending_commands { 2266 validate_public_pending_command_secret_posture(command, protected_secrets_configured)?; 2267 } 2268 Ok(()) 2269 } 2270 2271 #[cfg(feature = "std")] 2272 fn validate_public_connection_secret_posture( 2273 connection: &RadrootsSimplexAgentConnectionSnapshot, 2274 protected_secrets_configured: bool, 2275 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2276 reject_public_secret_option( 2277 connection.local_e2e_private_key.as_ref(), 2278 protected_secrets_configured, 2279 "local e2e private key", 2280 &connection.id, 2281 )?; 2282 reject_public_keypair_private( 2283 connection.local_x3dh_key_1.as_ref(), 2284 protected_secrets_configured, 2285 "first X3DH private key", 2286 &connection.id, 2287 )?; 2288 reject_public_keypair_private( 2289 connection.local_x3dh_key_2.as_ref(), 2290 protected_secrets_configured, 2291 "second X3DH private key", 2292 &connection.id, 2293 )?; 2294 reject_public_pq_private( 2295 connection.local_pq_keypair.as_ref(), 2296 protected_secrets_configured, 2297 "PQ private key", 2298 &connection.id, 2299 )?; 2300 reject_public_secret_option( 2301 connection.shared_secret.as_ref(), 2302 protected_secrets_configured, 2303 "connection shared secret", 2304 &connection.id, 2305 )?; 2306 if let Some(short_link) = connection.short_link.as_ref() { 2307 reject_public_secret_vec( 2308 short_link.link_key.as_slice(), 2309 protected_secrets_configured, 2310 "short-link link key", 2311 &connection.id, 2312 )?; 2313 reject_public_secret_vec( 2314 short_link.link_private_signature_key.as_slice(), 2315 protected_secrets_configured, 2316 "short-link private signature key", 2317 &connection.id, 2318 )?; 2319 } 2320 for queue in &connection.queues { 2321 reject_public_queue_secret_posture(queue, protected_secrets_configured, &connection.id)?; 2322 } 2323 if let Some(ratchet) = connection.ratchet_state.as_ref() { 2324 reject_public_ratchet_secret_posture( 2325 ratchet, 2326 protected_secrets_configured, 2327 &connection.id, 2328 )?; 2329 } 2330 Ok(()) 2331 } 2332 2333 #[cfg(feature = "std")] 2334 fn reject_public_queue_secret_posture( 2335 queue: &RadrootsSimplexAgentQueueRecordSnapshot, 2336 protected_secrets_configured: bool, 2337 connection_id: &str, 2338 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2339 if let Some(auth) = queue.auth_state.as_ref() { 2340 reject_public_secret_vec( 2341 auth.private_key.as_slice(), 2342 protected_secrets_configured, 2343 "queue auth private key", 2344 connection_id, 2345 )?; 2346 } 2347 reject_public_secret_option( 2348 queue.delivery_private_key.as_ref(), 2349 protected_secrets_configured, 2350 "delivery private key", 2351 connection_id, 2352 )?; 2353 reject_public_secret_option( 2354 queue.delivery_shared_secret.as_ref(), 2355 protected_secrets_configured, 2356 "delivery shared secret", 2357 connection_id, 2358 ) 2359 } 2360 2361 #[cfg(feature = "std")] 2362 fn reject_public_ratchet_secret_posture( 2363 ratchet: &RadrootsSimplexAgentRatchetStateSnapshot, 2364 protected_secrets_configured: bool, 2365 connection_id: &str, 2366 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2367 for (label, value) in [ 2368 ( 2369 "current PQ shared secret", 2370 ratchet.current_pq_shared_secret.as_ref(), 2371 ), 2372 ( 2373 "local PQ private key", 2374 ratchet.local_pq_private_key.as_ref(), 2375 ), 2376 ( 2377 "local DH private key", 2378 ratchet.local_dh_private_key.as_ref(), 2379 ), 2380 ("official root key", ratchet.official_root_key.as_ref()), 2381 ( 2382 "official sending chain key", 2383 ratchet.official_sending_chain_key.as_ref(), 2384 ), 2385 ( 2386 "official receiving chain key", 2387 ratchet.official_receiving_chain_key.as_ref(), 2388 ), 2389 ( 2390 "official sending header key", 2391 ratchet.official_sending_header_key.as_ref(), 2392 ), 2393 ( 2394 "official receiving header key", 2395 ratchet.official_receiving_header_key.as_ref(), 2396 ), 2397 ( 2398 "official next sending header key", 2399 ratchet.official_next_sending_header_key.as_ref(), 2400 ), 2401 ( 2402 "official next receiving header key", 2403 ratchet.official_next_receiving_header_key.as_ref(), 2404 ), 2405 ] { 2406 reject_public_secret_option(value, protected_secrets_configured, label, connection_id)?; 2407 } 2408 if !ratchet.official_skipped_message_keys.is_empty() { 2409 return Err(public_secret_error( 2410 protected_secrets_configured, 2411 "skipped message keys", 2412 connection_id, 2413 )); 2414 } 2415 Ok(()) 2416 } 2417 2418 #[cfg(feature = "std")] 2419 fn validate_public_pending_command_secret_posture( 2420 command: &RadrootsSimplexAgentPendingCommandSnapshot, 2421 protected_secrets_configured: bool, 2422 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2423 match &command.kind { 2424 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 2425 invitation, 2426 sender_auth_private_key, 2427 .. 2428 } => { 2429 reject_public_secret_vec( 2430 invitation.link_key.as_slice(), 2431 protected_secrets_configured, 2432 "pending short-link invitation link key", 2433 &command.connection_id, 2434 )?; 2435 reject_public_secret_vec( 2436 sender_auth_private_key.as_slice(), 2437 protected_secrets_configured, 2438 "pending short-link sender auth private key", 2439 &command.connection_id, 2440 ) 2441 } 2442 RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueLinkData { invitation, .. } => { 2443 reject_public_secret_vec( 2444 invitation.link_key.as_slice(), 2445 protected_secrets_configured, 2446 "pending short-link invitation link key", 2447 &command.connection_id, 2448 ) 2449 } 2450 _ => Ok(()), 2451 } 2452 } 2453 2454 #[cfg(feature = "std")] 2455 fn reject_public_keypair_private( 2456 keypair: Option<&RadrootsSimplexAgentX3dhKeypair>, 2457 protected_secrets_configured: bool, 2458 label: &str, 2459 connection_id: &str, 2460 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2461 if let Some(keypair) = keypair { 2462 reject_public_secret_vec( 2463 keypair.private_key.as_slice(), 2464 protected_secrets_configured, 2465 label, 2466 connection_id, 2467 )?; 2468 } 2469 Ok(()) 2470 } 2471 2472 #[cfg(feature = "std")] 2473 fn reject_public_pq_private( 2474 keypair: Option<&RadrootsSimplexAgentPqKeypair>, 2475 protected_secrets_configured: bool, 2476 label: &str, 2477 connection_id: &str, 2478 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2479 if let Some(keypair) = keypair { 2480 reject_public_secret_vec( 2481 keypair.private_key.as_slice(), 2482 protected_secrets_configured, 2483 label, 2484 connection_id, 2485 )?; 2486 } 2487 Ok(()) 2488 } 2489 2490 #[cfg(feature = "std")] 2491 fn reject_public_secret_option( 2492 value: Option<&Vec<u8>>, 2493 protected_secrets_configured: bool, 2494 label: &str, 2495 connection_id: &str, 2496 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2497 if let Some(value) = value { 2498 reject_public_secret_vec( 2499 value.as_slice(), 2500 protected_secrets_configured, 2501 label, 2502 connection_id, 2503 )?; 2504 } 2505 Ok(()) 2506 } 2507 2508 #[cfg(feature = "std")] 2509 fn reject_public_secret_vec( 2510 value: &[u8], 2511 protected_secrets_configured: bool, 2512 label: &str, 2513 connection_id: &str, 2514 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2515 if !value.is_empty() || !protected_secrets_configured { 2516 return Err(public_secret_error( 2517 protected_secrets_configured, 2518 label, 2519 connection_id, 2520 )); 2521 } 2522 Ok(()) 2523 } 2524 2525 #[cfg(feature = "std")] 2526 fn public_secret_error( 2527 protected_secrets_configured: bool, 2528 label: &str, 2529 connection_id: &str, 2530 ) -> RadrootsSimplexAgentStoreError { 2531 let posture = if protected_secrets_configured { 2532 "plaintext secret material" 2533 } else { 2534 "secret material or redacted secret markers without protected metadata" 2535 }; 2536 RadrootsSimplexAgentStoreError::Persistence(format!( 2537 "SimpleX agent public snapshot contains {posture} for {label} on `{connection_id}`" 2538 )) 2539 } 2540 2541 #[cfg(feature = "std")] 2542 fn merge_protected_secrets( 2543 snapshot: &mut RadrootsSimplexAgentStoreSnapshot, 2544 secrets: RadrootsSimplexAgentStoreSecretsSnapshot, 2545 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2546 for secret_connection in secrets.connections { 2547 let connection = snapshot 2548 .connections 2549 .iter_mut() 2550 .find(|connection| connection.id == secret_connection.id) 2551 .ok_or_else(|| { 2552 RadrootsSimplexAgentStoreError::Persistence(format!( 2553 "SimpleX agent protected secrets reference unknown connection `{}`", 2554 secret_connection.id 2555 )) 2556 })?; 2557 merge_connection_secrets(connection, secret_connection)?; 2558 } 2559 for command_secrets in secrets.pending_commands { 2560 merge_pending_command_secrets(snapshot, command_secrets)?; 2561 } 2562 Ok(()) 2563 } 2564 2565 #[cfg(feature = "std")] 2566 fn merge_connection_secrets( 2567 connection: &mut RadrootsSimplexAgentConnectionSnapshot, 2568 secrets: RadrootsSimplexAgentConnectionSecretsSnapshot, 2569 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2570 for queue_secrets in secrets.queues { 2571 let queue_index = protected_queue_secret_match_index(connection, &queue_secrets)?; 2572 let queue = &mut connection.queues[queue_index]; 2573 merge_queue_secrets(queue, queue_secrets, &connection.id)?; 2574 } 2575 2576 if let Some(ratchet_secrets) = secrets.ratchet_state { 2577 let ratchet = connection.ratchet_state.as_mut().ok_or_else(|| { 2578 RadrootsSimplexAgentStoreError::Persistence(format!( 2579 "SimpleX agent protected secrets reference missing ratchet state on `{}`", 2580 connection.id 2581 )) 2582 })?; 2583 merge_ratchet_secrets(ratchet, ratchet_secrets); 2584 } 2585 2586 connection.local_e2e_private_key = secrets.local_e2e_private_key; 2587 if let Some(private_key) = secrets.local_x3dh_key_1_private_key { 2588 let keypair = connection.local_x3dh_key_1.as_mut().ok_or_else(|| { 2589 RadrootsSimplexAgentStoreError::Persistence(format!( 2590 "SimpleX agent protected secrets reference missing first X3DH keypair on `{}`", 2591 connection.id 2592 )) 2593 })?; 2594 keypair.private_key = private_key; 2595 } 2596 if let Some(private_key) = secrets.local_x3dh_key_2_private_key { 2597 let keypair = connection.local_x3dh_key_2.as_mut().ok_or_else(|| { 2598 RadrootsSimplexAgentStoreError::Persistence(format!( 2599 "SimpleX agent protected secrets reference missing second X3DH keypair on `{}`", 2600 connection.id 2601 )) 2602 })?; 2603 keypair.private_key = private_key; 2604 } 2605 if let Some(private_key) = secrets.local_pq_private_key { 2606 let keypair = connection.local_pq_keypair.as_mut().ok_or_else(|| { 2607 RadrootsSimplexAgentStoreError::Persistence(format!( 2608 "SimpleX agent protected secrets reference missing PQ keypair on `{}`", 2609 connection.id 2610 )) 2611 })?; 2612 keypair.private_key = private_key; 2613 } 2614 connection.shared_secret = secrets.shared_secret; 2615 if secrets.short_link_link_key.is_some() || secrets.short_link_private_signature_key.is_some() { 2616 let short_link = connection.short_link.as_mut().ok_or_else(|| { 2617 RadrootsSimplexAgentStoreError::Persistence(format!( 2618 "SimpleX agent protected secrets reference missing short-link credentials on `{}`", 2619 connection.id 2620 )) 2621 })?; 2622 if let Some(link_key) = secrets.short_link_link_key { 2623 short_link.link_key = link_key; 2624 } 2625 if let Some(private_key) = secrets.short_link_private_signature_key { 2626 short_link.link_private_signature_key = private_key; 2627 } 2628 } 2629 Ok(()) 2630 } 2631 2632 #[cfg(feature = "std")] 2633 fn protected_queue_secret_match_index( 2634 connection: &RadrootsSimplexAgentConnectionSnapshot, 2635 secrets: &RadrootsSimplexAgentQueueSecretsSnapshot, 2636 ) -> Result<usize, RadrootsSimplexAgentStoreError> { 2637 let mut matched_index = None; 2638 for (index, queue) in connection.queues.iter().enumerate() { 2639 if !protected_queue_secret_matches(queue, secrets)? { 2640 continue; 2641 } 2642 if matched_index.replace(index).is_some() { 2643 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2644 "SimpleX agent protected secrets reference ambiguous queue on `{}`", 2645 connection.id 2646 ))); 2647 } 2648 } 2649 matched_index.ok_or_else(|| { 2650 RadrootsSimplexAgentStoreError::Persistence(format!( 2651 "SimpleX agent protected secrets reference unknown queue on `{}`", 2652 connection.id 2653 )) 2654 }) 2655 } 2656 2657 #[cfg(feature = "std")] 2658 fn protected_queue_secret_matches( 2659 queue: &RadrootsSimplexAgentQueueRecordSnapshot, 2660 secrets: &RadrootsSimplexAgentQueueSecretsSnapshot, 2661 ) -> Result<bool, RadrootsSimplexAgentStoreError> { 2662 if queue.entity_id != secrets.entity_id || queue.role != secrets.role { 2663 return Ok(false); 2664 } 2665 let Some(address) = secrets.queue_address.as_ref() else { 2666 return Ok(true); 2667 }; 2668 let descriptor = queue_descriptor_from_snapshot(queue.descriptor.clone())?; 2669 Ok(queue_address_to_snapshot(descriptor.queue_address()) == *address) 2670 } 2671 2672 #[cfg(feature = "std")] 2673 fn merge_queue_secrets( 2674 queue: &mut RadrootsSimplexAgentQueueRecordSnapshot, 2675 secrets: RadrootsSimplexAgentQueueSecretsSnapshot, 2676 connection_id: &str, 2677 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2678 if let Some(private_key) = secrets.auth_private_key { 2679 let auth = queue.auth_state.as_mut().ok_or_else(|| { 2680 RadrootsSimplexAgentStoreError::Persistence(format!( 2681 "SimpleX agent protected secrets reference missing queue auth state on `{connection_id}`" 2682 )) 2683 })?; 2684 auth.private_key = private_key; 2685 } 2686 queue.delivery_private_key = secrets.delivery_private_key; 2687 queue.delivery_shared_secret = secrets.delivery_shared_secret; 2688 Ok(()) 2689 } 2690 2691 #[cfg(feature = "std")] 2692 fn merge_ratchet_secrets( 2693 ratchet: &mut RadrootsSimplexAgentRatchetStateSnapshot, 2694 secrets: RadrootsSimplexAgentRatchetSecretsSnapshot, 2695 ) { 2696 ratchet.current_pq_shared_secret = secrets.current_pq_shared_secret; 2697 ratchet.local_pq_private_key = secrets.local_pq_private_key; 2698 ratchet.local_dh_private_key = secrets.local_dh_private_key; 2699 ratchet.official_root_key = secrets.official_root_key; 2700 ratchet.official_sending_chain_key = secrets.official_sending_chain_key; 2701 ratchet.official_receiving_chain_key = secrets.official_receiving_chain_key; 2702 ratchet.official_sending_header_key = secrets.official_sending_header_key; 2703 ratchet.official_receiving_header_key = secrets.official_receiving_header_key; 2704 ratchet.official_next_sending_header_key = secrets.official_next_sending_header_key; 2705 ratchet.official_next_receiving_header_key = secrets.official_next_receiving_header_key; 2706 ratchet.official_skipped_message_keys = secrets.official_skipped_message_keys; 2707 } 2708 2709 #[cfg(feature = "std")] 2710 fn merge_pending_command_secrets( 2711 snapshot: &mut RadrootsSimplexAgentStoreSnapshot, 2712 secrets: RadrootsSimplexAgentPendingCommandSecretsSnapshot, 2713 ) -> Result<(), RadrootsSimplexAgentStoreError> { 2714 let command = snapshot 2715 .pending_commands 2716 .iter_mut() 2717 .find(|command| command.id == secrets.id && command.connection_id == secrets.connection_id) 2718 .ok_or_else(|| { 2719 RadrootsSimplexAgentStoreError::Persistence(format!( 2720 "SimpleX agent protected secrets reference unknown pending command `{}`", 2721 secrets.id 2722 )) 2723 })?; 2724 2725 match &mut command.kind { 2726 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 2727 invitation, 2728 sender_auth_private_key, 2729 .. 2730 } => { 2731 if let Some(link_key) = secrets.short_invitation_link_key { 2732 invitation.link_key = link_key; 2733 } 2734 if let Some(private_key) = secrets.short_invitation_sender_auth_private_key { 2735 *sender_auth_private_key = private_key; 2736 } 2737 Ok(()) 2738 } 2739 RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueLinkData { invitation, .. } => { 2740 if let Some(link_key) = secrets.short_invitation_link_key { 2741 invitation.link_key = link_key; 2742 } 2743 Ok(()) 2744 } 2745 _ if secrets.short_invitation_link_key.is_none() 2746 && secrets.short_invitation_sender_auth_private_key.is_none() => 2747 { 2748 Ok(()) 2749 } 2750 _ => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2751 "SimpleX agent protected secrets reference pending command `{}` without short invitation link data", 2752 secrets.id 2753 ))), 2754 } 2755 } 2756 2757 #[cfg(feature = "std")] 2758 fn remove_protected_secrets_files(path: &Path) -> Result<(), RadrootsSimplexAgentStoreError> { 2759 remove_file_if_exists(&protected_secrets_path(path))?; 2760 remove_file_if_exists(&protected_secrets_wrapping_key_path(path)) 2761 } 2762 2763 #[cfg(feature = "std")] 2764 fn remove_file_if_exists(path: &Path) -> Result<(), RadrootsSimplexAgentStoreError> { 2765 match fs::remove_file(path) { 2766 Ok(()) => Ok(()), 2767 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()), 2768 Err(error) => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 2769 "failed to remove SimpleX agent protected store file `{}`: {error}", 2770 path.display() 2771 ))), 2772 } 2773 } 2774 2775 #[cfg(feature = "std")] 2776 fn set_secret_permissions(path: &Path) -> Result<(), RadrootsSimplexAgentStoreError> { 2777 set_secret_permissions_inner(path).map_err(|error| { 2778 RadrootsSimplexAgentStoreError::Persistence(format!( 2779 "failed to set SimpleX agent protected store permissions `{}`: {error}", 2780 path.display() 2781 )) 2782 }) 2783 } 2784 2785 #[cfg(all(feature = "std", unix))] 2786 fn set_secret_permissions_inner(path: &Path) -> std::io::Result<()> { 2787 use std::os::unix::fs::PermissionsExt; 2788 2789 fs::set_permissions(path, fs::Permissions::from_mode(0o600)) 2790 } 2791 2792 #[cfg(all(feature = "std", not(unix)))] 2793 fn set_secret_permissions_inner(_path: &Path) -> std::io::Result<()> { 2794 Ok(()) 2795 } 2796 2797 #[cfg(feature = "std")] 2798 fn connection_to_snapshot( 2799 record: RadrootsSimplexAgentConnectionRecord, 2800 ) -> Result<RadrootsSimplexAgentConnectionSnapshot, RadrootsSimplexAgentStoreError> { 2801 Ok(RadrootsSimplexAgentConnectionSnapshot { 2802 id: record.id, 2803 mode: encode_connection_mode(record.mode).into(), 2804 status: encode_connection_status(record.status).into(), 2805 invitation: record 2806 .invitation 2807 .as_ref() 2808 .map(encode_connection_link) 2809 .transpose() 2810 .map_err(|error| { 2811 RadrootsSimplexAgentStoreError::Persistence(format!( 2812 "failed to encode SimpleX connection invitation: {error}" 2813 )) 2814 })?, 2815 short_link: record.short_link.map(short_link_to_snapshot), 2816 queues: record 2817 .queues 2818 .into_iter() 2819 .map(queue_record_to_snapshot) 2820 .collect::<Result<Vec<_>, _>>()?, 2821 ratchet_state: record.ratchet_state.map(ratchet_state_to_snapshot), 2822 local_e2e_public_key: record.local_e2e_public_key, 2823 local_e2e_private_key: record.local_e2e_private_key, 2824 local_x3dh_key_1: record.local_x3dh_key_1, 2825 local_x3dh_key_2: record.local_x3dh_key_2, 2826 local_pq_keypair: record.local_pq_keypair, 2827 shared_secret: record.shared_secret, 2828 delivery_cursor: record.delivery_cursor, 2829 last_received_queue: record.last_received_queue.map(queue_address_to_snapshot), 2830 last_received_broker_message_id: record.last_received_broker_message_id, 2831 recent_messages: record.recent_messages, 2832 staged_outbound_message: record.staged_outbound_message, 2833 hello_sent: record.hello_sent, 2834 hello_received: record.hello_received, 2835 }) 2836 } 2837 2838 #[cfg(feature = "std")] 2839 fn connection_from_snapshot( 2840 snapshot: RadrootsSimplexAgentConnectionSnapshot, 2841 ) -> Result<RadrootsSimplexAgentConnectionRecord, RadrootsSimplexAgentStoreError> { 2842 Ok(RadrootsSimplexAgentConnectionRecord { 2843 id: snapshot.id, 2844 mode: decode_connection_mode(&snapshot.mode)?, 2845 status: decode_connection_status(&snapshot.status)?, 2846 invitation: snapshot 2847 .invitation 2848 .as_ref() 2849 .map(|value| { 2850 decode_connection_link(value).map_err(|error| { 2851 RadrootsSimplexAgentStoreError::Persistence(format!( 2852 "failed to decode SimpleX connection invitation: {error}" 2853 )) 2854 }) 2855 }) 2856 .transpose()?, 2857 short_link: snapshot 2858 .short_link 2859 .map(short_link_from_snapshot) 2860 .transpose()?, 2861 queues: snapshot 2862 .queues 2863 .into_iter() 2864 .map(queue_record_from_snapshot) 2865 .collect::<Result<Vec<_>, _>>()?, 2866 ratchet_state: snapshot 2867 .ratchet_state 2868 .map(ratchet_state_from_snapshot) 2869 .transpose()?, 2870 local_e2e_public_key: snapshot.local_e2e_public_key, 2871 local_e2e_private_key: snapshot.local_e2e_private_key, 2872 local_x3dh_key_1: snapshot.local_x3dh_key_1, 2873 local_x3dh_key_2: snapshot.local_x3dh_key_2, 2874 local_pq_keypair: snapshot.local_pq_keypair, 2875 shared_secret: snapshot.shared_secret, 2876 delivery_cursor: snapshot.delivery_cursor, 2877 last_received_queue: snapshot 2878 .last_received_queue 2879 .map(queue_address_from_snapshot) 2880 .transpose()?, 2881 last_received_broker_message_id: snapshot.last_received_broker_message_id, 2882 recent_messages: snapshot.recent_messages, 2883 staged_outbound_message: snapshot.staged_outbound_message, 2884 hello_sent: snapshot.hello_sent, 2885 hello_received: snapshot.hello_received, 2886 }) 2887 } 2888 2889 #[cfg(feature = "std")] 2890 fn queue_record_to_snapshot( 2891 record: RadrootsSimplexAgentQueueRecord, 2892 ) -> Result<RadrootsSimplexAgentQueueRecordSnapshot, RadrootsSimplexAgentStoreError> { 2893 Ok(RadrootsSimplexAgentQueueRecordSnapshot { 2894 descriptor: queue_descriptor_to_snapshot(record.descriptor), 2895 entity_id: record.entity_id, 2896 role: encode_queue_role(record.role).into(), 2897 subscribed: record.subscribed, 2898 primary: record.primary, 2899 tested: record.tested, 2900 auth_state: record.auth_state, 2901 delivery_private_key: record.delivery_private_key, 2902 delivery_shared_secret: record.delivery_shared_secret, 2903 }) 2904 } 2905 2906 #[cfg(feature = "std")] 2907 fn queue_record_from_snapshot( 2908 snapshot: RadrootsSimplexAgentQueueRecordSnapshot, 2909 ) -> Result<RadrootsSimplexAgentQueueRecord, RadrootsSimplexAgentStoreError> { 2910 Ok(RadrootsSimplexAgentQueueRecord { 2911 descriptor: queue_descriptor_from_snapshot(snapshot.descriptor)?, 2912 entity_id: snapshot.entity_id, 2913 role: decode_queue_role(&snapshot.role)?, 2914 subscribed: snapshot.subscribed, 2915 primary: snapshot.primary, 2916 tested: snapshot.tested, 2917 auth_state: snapshot.auth_state, 2918 delivery_private_key: snapshot.delivery_private_key, 2919 delivery_shared_secret: snapshot.delivery_shared_secret, 2920 }) 2921 } 2922 2923 #[cfg(feature = "std")] 2924 fn queue_descriptor_to_snapshot( 2925 descriptor: RadrootsSimplexAgentQueueDescriptor, 2926 ) -> RadrootsSimplexAgentQueueDescriptorSnapshot { 2927 RadrootsSimplexAgentQueueDescriptorSnapshot { 2928 queue_uri: descriptor.queue_uri.to_string(), 2929 replaced_queue: descriptor.replaced_queue.map(queue_address_to_snapshot), 2930 primary: descriptor.primary, 2931 sender_key: descriptor.sender_key, 2932 } 2933 } 2934 2935 #[cfg(feature = "std")] 2936 fn queue_descriptor_from_snapshot( 2937 snapshot: RadrootsSimplexAgentQueueDescriptorSnapshot, 2938 ) -> Result<RadrootsSimplexAgentQueueDescriptor, RadrootsSimplexAgentStoreError> { 2939 Ok(RadrootsSimplexAgentQueueDescriptor { 2940 queue_uri: queue_uri_from_string(&snapshot.queue_uri)?, 2941 replaced_queue: snapshot 2942 .replaced_queue 2943 .map(queue_address_from_snapshot) 2944 .transpose()?, 2945 primary: snapshot.primary, 2946 sender_key: snapshot.sender_key, 2947 }) 2948 } 2949 2950 #[cfg(feature = "std")] 2951 fn queue_uri_from_string( 2952 value: &str, 2953 ) -> Result<RadrootsSimplexSmpQueueUri, RadrootsSimplexAgentStoreError> { 2954 RadrootsSimplexSmpQueueUri::parse(value).map_err(|error| { 2955 RadrootsSimplexAgentStoreError::Persistence(format!( 2956 "failed to parse SimpleX queue uri `{value}`: {error}" 2957 )) 2958 }) 2959 } 2960 2961 #[cfg(feature = "std")] 2962 fn queue_address_to_snapshot( 2963 address: RadrootsSimplexAgentQueueAddress, 2964 ) -> RadrootsSimplexAgentQueueAddressSnapshot { 2965 RadrootsSimplexAgentQueueAddressSnapshot { 2966 server_identity: address.server.server_identity, 2967 hosts: address.server.hosts, 2968 port: address.server.port, 2969 sender_id: address.sender_id, 2970 } 2971 } 2972 2973 #[cfg(feature = "std")] 2974 fn queue_address_from_snapshot( 2975 snapshot: RadrootsSimplexAgentQueueAddressSnapshot, 2976 ) -> Result<RadrootsSimplexAgentQueueAddress, RadrootsSimplexAgentStoreError> { 2977 if snapshot.server_identity.is_empty() || snapshot.hosts.is_empty() { 2978 return Err(RadrootsSimplexAgentStoreError::Persistence( 2979 "invalid SimpleX queue address snapshot".into(), 2980 )); 2981 } 2982 Ok(RadrootsSimplexAgentQueueAddress { 2983 server: RadrootsSimplexSmpServerAddress { 2984 server_identity: snapshot.server_identity, 2985 hosts: snapshot.hosts, 2986 port: snapshot.port, 2987 }, 2988 sender_id: snapshot.sender_id, 2989 }) 2990 } 2991 2992 #[cfg(feature = "std")] 2993 fn short_link_to_snapshot( 2994 credentials: RadrootsSimplexAgentShortLinkCredentials, 2995 ) -> RadrootsSimplexAgentShortLinkCredentialsSnapshot { 2996 RadrootsSimplexAgentShortLinkCredentialsSnapshot { 2997 scheme: encode_short_link_scheme(credentials.scheme).into(), 2998 hosts: credentials.hosts, 2999 port: credentials.port, 3000 server_key_hash: credentials.server_key_hash, 3001 link_id: credentials.link_id, 3002 link_key: credentials.link_key, 3003 link_public_signature_key: credentials.link_public_signature_key, 3004 link_private_signature_key: credentials.link_private_signature_key, 3005 encrypted_fixed_data: credentials.encrypted_fixed_data, 3006 encrypted_user_data: credentials.encrypted_user_data, 3007 } 3008 } 3009 3010 #[cfg(feature = "std")] 3011 fn short_link_from_snapshot( 3012 snapshot: RadrootsSimplexAgentShortLinkCredentialsSnapshot, 3013 ) -> Result<RadrootsSimplexAgentShortLinkCredentials, RadrootsSimplexAgentStoreError> { 3014 Ok(RadrootsSimplexAgentShortLinkCredentials { 3015 scheme: decode_short_link_scheme(&snapshot.scheme)?, 3016 hosts: snapshot.hosts, 3017 port: snapshot.port, 3018 server_key_hash: snapshot.server_key_hash, 3019 link_id: snapshot.link_id, 3020 link_key: snapshot.link_key, 3021 link_public_signature_key: snapshot.link_public_signature_key, 3022 link_private_signature_key: snapshot.link_private_signature_key, 3023 encrypted_fixed_data: snapshot.encrypted_fixed_data, 3024 encrypted_user_data: snapshot.encrypted_user_data, 3025 }) 3026 } 3027 3028 #[cfg(feature = "std")] 3029 fn short_invitation_to_snapshot( 3030 invitation: RadrootsSimplexAgentShortInvitationLink, 3031 ) -> RadrootsSimplexAgentShortInvitationLinkSnapshot { 3032 RadrootsSimplexAgentShortInvitationLinkSnapshot { 3033 scheme: encode_short_link_scheme(invitation.scheme).into(), 3034 hosts: invitation.hosts, 3035 port: invitation.port, 3036 server_key_hash: invitation.server_key_hash, 3037 link_id: invitation.link_id, 3038 link_key: invitation.link_key, 3039 } 3040 } 3041 3042 #[cfg(feature = "std")] 3043 fn short_invitation_from_snapshot( 3044 snapshot: RadrootsSimplexAgentShortInvitationLinkSnapshot, 3045 ) -> Result<RadrootsSimplexAgentShortInvitationLink, RadrootsSimplexAgentStoreError> { 3046 Ok(RadrootsSimplexAgentShortInvitationLink { 3047 scheme: decode_short_link_scheme(&snapshot.scheme)?, 3048 hosts: snapshot.hosts, 3049 port: snapshot.port, 3050 server_key_hash: snapshot.server_key_hash, 3051 link_id: snapshot.link_id, 3052 link_key: snapshot.link_key, 3053 }) 3054 } 3055 3056 #[cfg(feature = "std")] 3057 fn queue_link_data_to_snapshot( 3058 link_data: RadrootsSimplexSmpQueueLinkData, 3059 ) -> RadrootsSimplexAgentQueueLinkDataSnapshot { 3060 RadrootsSimplexAgentQueueLinkDataSnapshot { 3061 fixed_data: link_data.fixed_data, 3062 user_data: link_data.user_data, 3063 } 3064 } 3065 3066 #[cfg(feature = "std")] 3067 fn queue_link_data_from_snapshot( 3068 snapshot: RadrootsSimplexAgentQueueLinkDataSnapshot, 3069 ) -> RadrootsSimplexSmpQueueLinkData { 3070 RadrootsSimplexSmpQueueLinkData { 3071 fixed_data: snapshot.fixed_data, 3072 user_data: snapshot.user_data, 3073 } 3074 } 3075 3076 #[cfg(feature = "std")] 3077 fn ratchet_state_to_snapshot( 3078 state: RadrootsSimplexSmpRatchetState, 3079 ) -> RadrootsSimplexAgentRatchetStateSnapshot { 3080 RadrootsSimplexAgentRatchetStateSnapshot { 3081 role: alloc::format!("{:?}", state.role).to_ascii_lowercase(), 3082 root_epoch: state.root_epoch, 3083 previous_sending_chain_length: state.previous_sending_chain_length, 3084 sending_chain_length: state.sending_chain_length, 3085 receiving_chain_length: state.receiving_chain_length, 3086 local_dh_public_key: state.local_dh_public_key, 3087 remote_dh_public_key: state.remote_dh_public_key, 3088 current_pq_public_key: state.current_pq_public_key, 3089 remote_pq_public_key: state.remote_pq_public_key, 3090 pending_outbound_pq_ciphertext: state.pending_outbound_pq_ciphertext, 3091 pending_inbound_pq_ciphertext: state.pending_inbound_pq_ciphertext, 3092 current_pq_shared_secret: state.current_pq_shared_secret, 3093 local_pq_private_key: state.local_pq_private_key, 3094 local_dh_private_key: state.local_dh_private_key, 3095 official_associated_data: state.official_associated_data, 3096 official_root_key: state.official_root_key, 3097 official_sending_chain_key: state.official_sending_chain_key, 3098 official_receiving_chain_key: state.official_receiving_chain_key, 3099 official_sending_header_key: state.official_sending_header_key, 3100 official_receiving_header_key: state.official_receiving_header_key, 3101 official_next_sending_header_key: state.official_next_sending_header_key, 3102 official_next_receiving_header_key: state.official_next_receiving_header_key, 3103 official_skipped_message_keys: state 3104 .official_skipped_message_keys 3105 .into_iter() 3106 .map(skipped_message_key_to_snapshot) 3107 .collect(), 3108 } 3109 } 3110 3111 #[cfg(feature = "std")] 3112 fn ratchet_state_from_snapshot( 3113 snapshot: RadrootsSimplexAgentRatchetStateSnapshot, 3114 ) -> Result<RadrootsSimplexSmpRatchetState, RadrootsSimplexAgentStoreError> { 3115 let mut state = match snapshot.role.as_str() { 3116 "initiator" => RadrootsSimplexSmpRatchetState::initiator( 3117 snapshot.local_dh_public_key.clone(), 3118 snapshot.remote_dh_public_key.clone(), 3119 snapshot.remote_pq_public_key.clone(), 3120 ) 3121 .map_err(|error| { 3122 RadrootsSimplexAgentStoreError::Persistence(format!( 3123 "failed to restore initiator ratchet state: {error}" 3124 )) 3125 })?, 3126 "responder" => RadrootsSimplexSmpRatchetState::responder( 3127 snapshot.local_dh_public_key.clone(), 3128 snapshot.remote_dh_public_key.clone(), 3129 snapshot.current_pq_public_key.clone(), 3130 ) 3131 .map_err(|error| { 3132 RadrootsSimplexAgentStoreError::Persistence(format!( 3133 "failed to restore responder ratchet state: {error}" 3134 )) 3135 })?, 3136 other => { 3137 return Err(RadrootsSimplexAgentStoreError::Persistence(format!( 3138 "invalid SimpleX ratchet role `{other}`" 3139 ))); 3140 } 3141 }; 3142 state.root_epoch = snapshot.root_epoch; 3143 state.previous_sending_chain_length = snapshot.previous_sending_chain_length; 3144 state.sending_chain_length = snapshot.sending_chain_length; 3145 state.receiving_chain_length = snapshot.receiving_chain_length; 3146 state.current_pq_public_key = snapshot.current_pq_public_key; 3147 state.remote_pq_public_key = snapshot.remote_pq_public_key; 3148 state.pending_outbound_pq_ciphertext = snapshot.pending_outbound_pq_ciphertext; 3149 state.pending_inbound_pq_ciphertext = snapshot.pending_inbound_pq_ciphertext; 3150 state.current_pq_shared_secret = snapshot.current_pq_shared_secret; 3151 state.local_pq_private_key = snapshot.local_pq_private_key; 3152 state.local_dh_private_key = snapshot.local_dh_private_key; 3153 state.official_associated_data = snapshot.official_associated_data; 3154 state.official_root_key = snapshot.official_root_key; 3155 state.official_sending_chain_key = snapshot.official_sending_chain_key; 3156 state.official_receiving_chain_key = snapshot.official_receiving_chain_key; 3157 state.official_sending_header_key = snapshot.official_sending_header_key; 3158 state.official_receiving_header_key = snapshot.official_receiving_header_key; 3159 state.official_next_sending_header_key = snapshot.official_next_sending_header_key; 3160 state.official_next_receiving_header_key = snapshot.official_next_receiving_header_key; 3161 state.official_skipped_message_keys = snapshot 3162 .official_skipped_message_keys 3163 .into_iter() 3164 .map(skipped_message_key_from_snapshot) 3165 .collect::<Result<_, _>>()?; 3166 Ok(state) 3167 } 3168 3169 #[cfg(feature = "std")] 3170 fn skipped_message_key_to_snapshot( 3171 key: RadrootsSimplexSmpSkippedMessageKey, 3172 ) -> RadrootsSimplexAgentSkippedMessageKeySnapshot { 3173 RadrootsSimplexAgentSkippedMessageKeySnapshot { 3174 header_key: key.header_key, 3175 message_number: key.message_number, 3176 message_key: key.message_key, 3177 message_iv: key.message_iv.to_vec(), 3178 } 3179 } 3180 3181 #[cfg(feature = "std")] 3182 fn skipped_message_key_from_snapshot( 3183 snapshot: RadrootsSimplexAgentSkippedMessageKeySnapshot, 3184 ) -> Result<RadrootsSimplexSmpSkippedMessageKey, RadrootsSimplexAgentStoreError> { 3185 let message_iv: [u8; RADROOTS_SIMPLEX_OFFICIAL_AES_IV_LENGTH] = snapshot 3186 .message_iv 3187 .try_into() 3188 .map_err(|message_iv: Vec<u8>| { 3189 RadrootsSimplexAgentStoreError::Persistence(format!( 3190 "invalid SimpleX skipped message IV length {}", 3191 message_iv.len() 3192 )) 3193 })?; 3194 Ok(RadrootsSimplexSmpSkippedMessageKey { 3195 header_key: snapshot.header_key, 3196 message_number: snapshot.message_number, 3197 message_key: snapshot.message_key, 3198 message_iv, 3199 }) 3200 } 3201 3202 #[cfg(feature = "std")] 3203 fn command_to_snapshot( 3204 command: RadrootsSimplexAgentPendingCommand, 3205 ) -> Result<RadrootsSimplexAgentPendingCommandSnapshot, RadrootsSimplexAgentStoreError> { 3206 Ok(RadrootsSimplexAgentPendingCommandSnapshot { 3207 id: command.id, 3208 connection_id: command.connection_id, 3209 kind: command_kind_to_snapshot(command.kind)?, 3210 attempts: command.attempts, 3211 ready_at: command.ready_at, 3212 inflight: command.inflight, 3213 }) 3214 } 3215 3216 #[cfg(feature = "std")] 3217 fn command_from_snapshot( 3218 snapshot: RadrootsSimplexAgentPendingCommandSnapshot, 3219 ) -> Result<RadrootsSimplexAgentPendingCommand, RadrootsSimplexAgentStoreError> { 3220 Ok(RadrootsSimplexAgentPendingCommand { 3221 id: snapshot.id, 3222 connection_id: snapshot.connection_id, 3223 kind: command_kind_from_snapshot(snapshot.kind)?, 3224 attempts: snapshot.attempts, 3225 ready_at: snapshot.ready_at, 3226 inflight: snapshot.inflight, 3227 }) 3228 } 3229 3230 #[cfg(feature = "std")] 3231 fn command_kind_to_snapshot( 3232 kind: RadrootsSimplexAgentPendingCommandKind, 3233 ) -> Result<RadrootsSimplexAgentPendingCommandKindSnapshot, RadrootsSimplexAgentStoreError> { 3234 Ok(match kind { 3235 RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor } => { 3236 RadrootsSimplexAgentPendingCommandKindSnapshot::CreateQueue { 3237 descriptor: queue_descriptor_to_snapshot(descriptor), 3238 } 3239 } 3240 RadrootsSimplexAgentPendingCommandKind::SecureQueue { queue, sender_key } => { 3241 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureQueue { 3242 queue: queue_address_to_snapshot(queue), 3243 sender_key, 3244 } 3245 } 3246 RadrootsSimplexAgentPendingCommandKind::SendEnvelope { 3247 queue, 3248 envelope, 3249 delivery, 3250 } => RadrootsSimplexAgentPendingCommandKindSnapshot::SendEnvelope { 3251 queue: queue_address_to_snapshot(queue), 3252 envelope: encode_envelope(&envelope).map_err(|error| { 3253 RadrootsSimplexAgentStoreError::Persistence(format!( 3254 "failed to encode SimpleX envelope: {error}" 3255 )) 3256 })?, 3257 delivery, 3258 }, 3259 RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { queue } => { 3260 RadrootsSimplexAgentPendingCommandKindSnapshot::SubscribeQueue { 3261 queue: queue_address_to_snapshot(queue), 3262 } 3263 } 3264 RadrootsSimplexAgentPendingCommandKind::GetQueueMessage { queue } => { 3265 RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueMessage { 3266 queue: queue_address_to_snapshot(queue), 3267 } 3268 } 3269 RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { 3270 queue, 3271 broker_message_id, 3272 receipt, 3273 } => RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { 3274 queue: queue_address_to_snapshot(queue), 3275 broker_message_id, 3276 receipt: receipt.map(|receipt| RadrootsSimplexAgentMessageReceiptSnapshot { 3277 message_id: receipt.message_id, 3278 message_hash: receipt.message_hash, 3279 receipt_info: receipt.receipt_info, 3280 }), 3281 }, 3282 RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => { 3283 RadrootsSimplexAgentPendingCommandKindSnapshot::RotateQueues { 3284 descriptors: descriptors 3285 .into_iter() 3286 .map(queue_descriptor_to_snapshot) 3287 .collect(), 3288 } 3289 } 3290 RadrootsSimplexAgentPendingCommandKind::TestQueues { queues } => { 3291 RadrootsSimplexAgentPendingCommandKindSnapshot::TestQueues { 3292 queues: queues.into_iter().map(queue_address_to_snapshot).collect(), 3293 } 3294 } 3295 RadrootsSimplexAgentPendingCommandKind::SetQueueLinkData { 3296 queue, 3297 link_id, 3298 link_data, 3299 } => RadrootsSimplexAgentPendingCommandKindSnapshot::SetQueueLinkData { 3300 queue: queue_address_to_snapshot(queue), 3301 link_id, 3302 link_data: queue_link_data_to_snapshot(link_data), 3303 }, 3304 RadrootsSimplexAgentPendingCommandKind::SecureGetQueueLinkData { 3305 invitation, 3306 reply_queue, 3307 sender_auth_state, 3308 } => RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 3309 invitation: short_invitation_to_snapshot(invitation), 3310 reply_queue: reply_queue.to_string(), 3311 sender_auth_public_key: sender_auth_state.public_key, 3312 sender_auth_private_key: sender_auth_state.private_key, 3313 }, 3314 RadrootsSimplexAgentPendingCommandKind::GetQueueLinkData { 3315 invitation, 3316 reply_queue, 3317 } => RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueLinkData { 3318 invitation: short_invitation_to_snapshot(invitation), 3319 reply_queue: reply_queue.to_string(), 3320 }, 3321 }) 3322 } 3323 3324 #[cfg(feature = "std")] 3325 fn command_kind_from_snapshot( 3326 snapshot: RadrootsSimplexAgentPendingCommandKindSnapshot, 3327 ) -> Result<RadrootsSimplexAgentPendingCommandKind, RadrootsSimplexAgentStoreError> { 3328 Ok(match snapshot { 3329 RadrootsSimplexAgentPendingCommandKindSnapshot::CreateQueue { descriptor } => { 3330 RadrootsSimplexAgentPendingCommandKind::CreateQueue { 3331 descriptor: queue_descriptor_from_snapshot(descriptor)?, 3332 } 3333 } 3334 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureQueue { queue, sender_key } => { 3335 RadrootsSimplexAgentPendingCommandKind::SecureQueue { 3336 queue: queue_address_from_snapshot(queue)?, 3337 sender_key, 3338 } 3339 } 3340 RadrootsSimplexAgentPendingCommandKindSnapshot::SendEnvelope { 3341 queue, 3342 envelope, 3343 delivery, 3344 } => RadrootsSimplexAgentPendingCommandKind::SendEnvelope { 3345 queue: queue_address_from_snapshot(queue)?, 3346 envelope: decode_envelope(&envelope).map_err(|error| { 3347 RadrootsSimplexAgentStoreError::Persistence(format!( 3348 "failed to decode SimpleX envelope: {error}" 3349 )) 3350 })?, 3351 delivery, 3352 }, 3353 RadrootsSimplexAgentPendingCommandKindSnapshot::SubscribeQueue { queue } => { 3354 RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { 3355 queue: queue_address_from_snapshot(queue)?, 3356 } 3357 } 3358 RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueMessage { queue } => { 3359 RadrootsSimplexAgentPendingCommandKind::GetQueueMessage { 3360 queue: queue_address_from_snapshot(queue)?, 3361 } 3362 } 3363 RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { 3364 queue, 3365 broker_message_id, 3366 receipt, 3367 } => RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { 3368 queue: queue_address_from_snapshot(queue)?, 3369 broker_message_id, 3370 receipt: receipt.map(|receipt| RadrootsSimplexAgentMessageReceipt { 3371 message_id: receipt.message_id, 3372 message_hash: receipt.message_hash, 3373 receipt_info: receipt.receipt_info, 3374 }), 3375 }, 3376 RadrootsSimplexAgentPendingCommandKindSnapshot::RotateQueues { descriptors } => { 3377 RadrootsSimplexAgentPendingCommandKind::RotateQueues { 3378 descriptors: descriptors 3379 .into_iter() 3380 .map(queue_descriptor_from_snapshot) 3381 .collect::<Result<Vec<_>, _>>()?, 3382 } 3383 } 3384 RadrootsSimplexAgentPendingCommandKindSnapshot::TestQueues { queues } => { 3385 RadrootsSimplexAgentPendingCommandKind::TestQueues { 3386 queues: queues 3387 .into_iter() 3388 .map(queue_address_from_snapshot) 3389 .collect::<Result<Vec<_>, _>>()?, 3390 } 3391 } 3392 RadrootsSimplexAgentPendingCommandKindSnapshot::SetQueueLinkData { 3393 queue, 3394 link_id, 3395 link_data, 3396 } => RadrootsSimplexAgentPendingCommandKind::SetQueueLinkData { 3397 queue: queue_address_from_snapshot(queue)?, 3398 link_id, 3399 link_data: queue_link_data_from_snapshot(link_data), 3400 }, 3401 RadrootsSimplexAgentPendingCommandKindSnapshot::SecureGetQueueLinkData { 3402 invitation, 3403 reply_queue, 3404 sender_auth_public_key, 3405 sender_auth_private_key, 3406 } => RadrootsSimplexAgentPendingCommandKind::SecureGetQueueLinkData { 3407 invitation: short_invitation_from_snapshot(invitation)?, 3408 reply_queue: queue_uri_from_string(&reply_queue)?, 3409 sender_auth_state: RadrootsSimplexAgentQueueAuthState { 3410 public_key: sender_auth_public_key, 3411 private_key: sender_auth_private_key, 3412 }, 3413 }, 3414 RadrootsSimplexAgentPendingCommandKindSnapshot::GetQueueLinkData { 3415 invitation, 3416 reply_queue, 3417 } => RadrootsSimplexAgentPendingCommandKind::GetQueueLinkData { 3418 invitation: short_invitation_from_snapshot(invitation)?, 3419 reply_queue: queue_uri_from_string(&reply_queue)?, 3420 }, 3421 }) 3422 } 3423 3424 #[cfg(feature = "std")] 3425 fn encode_connection_mode(mode: RadrootsSimplexAgentConnectionMode) -> &'static str { 3426 match mode { 3427 RadrootsSimplexAgentConnectionMode::Direct => "direct", 3428 RadrootsSimplexAgentConnectionMode::ContactAddress => "contact_address", 3429 } 3430 } 3431 3432 #[cfg(feature = "std")] 3433 fn decode_connection_mode( 3434 value: &str, 3435 ) -> Result<RadrootsSimplexAgentConnectionMode, RadrootsSimplexAgentStoreError> { 3436 match value { 3437 "direct" => Ok(RadrootsSimplexAgentConnectionMode::Direct), 3438 "contact_address" => Ok(RadrootsSimplexAgentConnectionMode::ContactAddress), 3439 other => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 3440 "invalid SimpleX connection mode `{other}`" 3441 ))), 3442 } 3443 } 3444 3445 #[cfg(feature = "std")] 3446 fn encode_connection_status(status: RadrootsSimplexAgentConnectionStatus) -> &'static str { 3447 match status { 3448 RadrootsSimplexAgentConnectionStatus::CreatePending => "create_pending", 3449 RadrootsSimplexAgentConnectionStatus::InvitationReady => "invitation_ready", 3450 RadrootsSimplexAgentConnectionStatus::JoinPending => "join_pending", 3451 RadrootsSimplexAgentConnectionStatus::AwaitingApproval => "awaiting_approval", 3452 RadrootsSimplexAgentConnectionStatus::Allowed => "allowed", 3453 RadrootsSimplexAgentConnectionStatus::Connected => "connected", 3454 RadrootsSimplexAgentConnectionStatus::Suspended => "suspended", 3455 RadrootsSimplexAgentConnectionStatus::Rotating => "rotating", 3456 RadrootsSimplexAgentConnectionStatus::Deleted => "deleted", 3457 } 3458 } 3459 3460 #[cfg(feature = "std")] 3461 fn decode_connection_status( 3462 value: &str, 3463 ) -> Result<RadrootsSimplexAgentConnectionStatus, RadrootsSimplexAgentStoreError> { 3464 match value { 3465 "create_pending" => Ok(RadrootsSimplexAgentConnectionStatus::CreatePending), 3466 "invitation_ready" => Ok(RadrootsSimplexAgentConnectionStatus::InvitationReady), 3467 "join_pending" => Ok(RadrootsSimplexAgentConnectionStatus::JoinPending), 3468 "awaiting_approval" => Ok(RadrootsSimplexAgentConnectionStatus::AwaitingApproval), 3469 "allowed" => Ok(RadrootsSimplexAgentConnectionStatus::Allowed), 3470 "connected" => Ok(RadrootsSimplexAgentConnectionStatus::Connected), 3471 "suspended" => Ok(RadrootsSimplexAgentConnectionStatus::Suspended), 3472 "rotating" => Ok(RadrootsSimplexAgentConnectionStatus::Rotating), 3473 "deleted" => Ok(RadrootsSimplexAgentConnectionStatus::Deleted), 3474 other => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 3475 "invalid SimpleX connection status `{other}`" 3476 ))), 3477 } 3478 } 3479 3480 #[cfg(feature = "std")] 3481 fn encode_queue_role(role: RadrootsSimplexAgentQueueRole) -> &'static str { 3482 match role { 3483 RadrootsSimplexAgentQueueRole::Receive => "receive", 3484 RadrootsSimplexAgentQueueRole::Send => "send", 3485 } 3486 } 3487 3488 #[cfg(feature = "std")] 3489 fn decode_queue_role( 3490 value: &str, 3491 ) -> Result<RadrootsSimplexAgentQueueRole, RadrootsSimplexAgentStoreError> { 3492 match value { 3493 "receive" => Ok(RadrootsSimplexAgentQueueRole::Receive), 3494 "send" => Ok(RadrootsSimplexAgentQueueRole::Send), 3495 other => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 3496 "invalid SimpleX queue role `{other}`" 3497 ))), 3498 } 3499 } 3500 3501 #[cfg(feature = "std")] 3502 fn encode_short_link_scheme(scheme: RadrootsSimplexAgentShortLinkScheme) -> &'static str { 3503 match scheme { 3504 RadrootsSimplexAgentShortLinkScheme::Simplex => "simplex", 3505 RadrootsSimplexAgentShortLinkScheme::Https => "https", 3506 } 3507 } 3508 3509 #[cfg(feature = "std")] 3510 fn decode_short_link_scheme( 3511 value: &str, 3512 ) -> Result<RadrootsSimplexAgentShortLinkScheme, RadrootsSimplexAgentStoreError> { 3513 match value { 3514 "simplex" => Ok(RadrootsSimplexAgentShortLinkScheme::Simplex), 3515 "https" => Ok(RadrootsSimplexAgentShortLinkScheme::Https), 3516 other => Err(RadrootsSimplexAgentStoreError::Persistence(format!( 3517 "invalid SimpleX short-link scheme `{other}`" 3518 ))), 3519 } 3520 } 3521 3522 #[cfg(test)] 3523 mod tests { 3524 use super::*; 3525 #[cfg(feature = "std")] 3526 use radroots_secret_vault::{RadrootsSecretVault, RadrootsSecretVaultAccessError}; 3527 use radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueUri; 3528 #[cfg(feature = "std")] 3529 use std::collections::HashMap; 3530 #[cfg(feature = "std")] 3531 use std::path::Path; 3532 #[cfg(feature = "std")] 3533 use std::sync::{Arc, RwLock}; 3534 3535 #[cfg(feature = "std")] 3536 #[derive(Clone, Default)] 3537 struct TestSecretVault { 3538 entries: Arc<RwLock<HashMap<String, String>>>, 3539 } 3540 3541 #[cfg(feature = "std")] 3542 impl TestSecretVault { 3543 fn new() -> Self { 3544 Self::default() 3545 } 3546 } 3547 3548 #[cfg(feature = "std")] 3549 impl RadrootsSecretVault for TestSecretVault { 3550 fn store_secret( 3551 &self, 3552 slot: &str, 3553 secret: &str, 3554 ) -> Result<(), RadrootsSecretVaultAccessError> { 3555 let mut entries = self.entries.write().map_err(|_| { 3556 RadrootsSecretVaultAccessError::Backend("test vault poisoned".into()) 3557 })?; 3558 entries.insert(slot.to_owned(), secret.to_owned()); 3559 Ok(()) 3560 } 3561 3562 fn load_secret( 3563 &self, 3564 slot: &str, 3565 ) -> Result<Option<String>, RadrootsSecretVaultAccessError> { 3566 let entries = self.entries.read().map_err(|_| { 3567 RadrootsSecretVaultAccessError::Backend("test vault poisoned".into()) 3568 })?; 3569 Ok(entries.get(slot).cloned()) 3570 } 3571 3572 fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> { 3573 let mut entries = self.entries.write().map_err(|_| { 3574 RadrootsSecretVaultAccessError::Backend("test vault poisoned".into()) 3575 })?; 3576 entries.remove(slot); 3577 Ok(()) 3578 } 3579 } 3580 3581 fn sample_descriptor(primary: bool) -> RadrootsSimplexAgentQueueDescriptor { 3582 sample_descriptor_with_uri( 3583 "smp://aGVsbG8@relay.example/cXVldWU#/?v=4&dh=Zm9vYmFy&q=m", 3584 primary, 3585 ) 3586 } 3587 3588 fn sample_descriptor_with_uri(uri: &str, primary: bool) -> RadrootsSimplexAgentQueueDescriptor { 3589 RadrootsSimplexAgentQueueDescriptor { 3590 queue_uri: RadrootsSimplexSmpQueueUri::parse(uri).unwrap(), 3591 replaced_queue: None, 3592 primary, 3593 sender_key: Some(b"sender-auth".to_vec()), 3594 } 3595 } 3596 3597 fn sample_auth_state() -> RadrootsSimplexAgentQueueAuthState { 3598 RadrootsSimplexAgentQueueAuthState { 3599 public_key: vec![7_u8; 32], 3600 private_key: vec![9_u8; 32], 3601 } 3602 } 3603 3604 fn sample_short_link_credentials() -> RadrootsSimplexAgentShortLinkCredentials { 3605 RadrootsSimplexAgentShortLinkCredentials { 3606 scheme: RadrootsSimplexAgentShortLinkScheme::Simplex, 3607 hosts: vec!["relay-a.example".to_owned(), "relay-b.example".to_owned()], 3608 port: Some(5223), 3609 server_key_hash: Some(vec![5_u8; 32]), 3610 link_id: vec![6_u8; 24], 3611 link_key: b"short-link-key-must-be-secret!!".to_vec(), 3612 link_public_signature_key: vec![7_u8; 32], 3613 link_private_signature_key: b"short-link-private-signature-key".to_vec(), 3614 encrypted_fixed_data: Some(b"encrypted-fixed-link-data".to_vec()), 3615 encrypted_user_data: Some(b"encrypted-user-link-data".to_vec()), 3616 } 3617 } 3618 3619 fn sample_short_invitation_link(link_id: Vec<u8>) -> RadrootsSimplexAgentShortInvitationLink { 3620 RadrootsSimplexAgentShortInvitationLink { 3621 scheme: RadrootsSimplexAgentShortLinkScheme::Simplex, 3622 hosts: vec!["relay-a.example".to_owned(), "relay-b.example".to_owned()], 3623 port: Some(5223), 3624 server_key_hash: Some(vec![5_u8; 32]), 3625 link_id, 3626 link_key: b"short-link-key-must-be-secret!!".to_vec(), 3627 } 3628 } 3629 3630 #[cfg(feature = "std")] 3631 fn persisted_store_with_secret_material(path: &Path) -> String { 3632 let mut store = RadrootsSimplexAgentStore::open(path).unwrap(); 3633 let connection = store.create_connection( 3634 RadrootsSimplexAgentConnectionMode::Direct, 3635 RadrootsSimplexAgentConnectionStatus::Connected, 3636 None, 3637 None, 3638 ); 3639 store 3640 .add_queue( 3641 &connection.id, 3642 sample_descriptor(true), 3643 RadrootsSimplexAgentQueueRole::Send, 3644 true, 3645 sample_auth_state(), 3646 ) 3647 .unwrap(); 3648 { 3649 let connection = store.connection_mut(&connection.id).unwrap(); 3650 connection.local_e2e_private_key = Some(b"e2e-private".to_vec()); 3651 connection.shared_secret = Some(b"connection-shared-secret".to_vec()); 3652 let queue = connection.queues.first_mut().unwrap(); 3653 queue.auth_state.as_mut().unwrap().private_key = b"queue-auth-private".to_vec(); 3654 queue.delivery_private_key = Some(b"queue-delivery-private".to_vec()); 3655 queue.delivery_shared_secret = Some(b"queue-delivery-shared-secret".to_vec()); 3656 } 3657 store.flush().unwrap(); 3658 connection.id 3659 } 3660 3661 #[cfg(feature = "std")] 3662 fn read_public_snapshot(path: &Path) -> serde_json::Value { 3663 serde_json::from_str(&fs::read_to_string(path).unwrap()).unwrap() 3664 } 3665 3666 #[cfg(feature = "std")] 3667 fn write_public_snapshot(path: &Path, value: &serde_json::Value) { 3668 fs::write( 3669 path, 3670 format!("{}\n", serde_json::to_string_pretty(value).unwrap()), 3671 ) 3672 .unwrap(); 3673 } 3674 3675 #[test] 3676 fn stores_connections_queues_and_retryable_commands() { 3677 let mut store = RadrootsSimplexAgentStore::new(); 3678 let connection = store.create_connection( 3679 RadrootsSimplexAgentConnectionMode::Direct, 3680 RadrootsSimplexAgentConnectionStatus::CreatePending, 3681 None, 3682 None, 3683 ); 3684 store 3685 .add_queue( 3686 &connection.id, 3687 sample_descriptor(true), 3688 RadrootsSimplexAgentQueueRole::Send, 3689 true, 3690 sample_auth_state(), 3691 ) 3692 .unwrap(); 3693 let command = store 3694 .enqueue_command( 3695 &connection.id, 3696 RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { 3697 queue: sample_descriptor(true).queue_address(), 3698 }, 3699 10, 3700 ) 3701 .unwrap(); 3702 let ready = store.take_ready_commands(10, 10); 3703 assert_eq!(ready.len(), 1); 3704 assert_eq!(ready[0].id, command.id); 3705 let retried = store.mark_command_retry(command.id, 20).unwrap(); 3706 assert_eq!(retried.ready_at, 20); 3707 let queue = store.primary_send_queue(&connection.id).unwrap(); 3708 assert_eq!(queue.descriptor, sample_descriptor(true)); 3709 assert!(queue.auth_state.is_some()); 3710 } 3711 3712 #[test] 3713 fn stages_and_confirms_outbound_message_without_consuming_cursor_early() { 3714 let mut store = RadrootsSimplexAgentStore::new(); 3715 let connection = store.create_connection( 3716 RadrootsSimplexAgentConnectionMode::Direct, 3717 RadrootsSimplexAgentConnectionStatus::Connected, 3718 None, 3719 None, 3720 ); 3721 3722 let prepared = store 3723 .prepare_outbound_message(&connection.id, b"ciphertext".to_vec()) 3724 .unwrap(); 3725 assert_eq!(prepared.message_id, 1); 3726 assert!(prepared.previous_message_hash.is_empty()); 3727 assert_eq!( 3728 store 3729 .connection(&connection.id) 3730 .unwrap() 3731 .delivery_cursor 3732 .last_sent_message_id, 3733 None 3734 ); 3735 3736 let error = store 3737 .prepare_outbound_message(&connection.id, b"next".to_vec()) 3738 .unwrap_err(); 3739 assert_eq!( 3740 error, 3741 RadrootsSimplexAgentStoreError::PendingOutboundMessage(connection.id.clone()) 3742 ); 3743 3744 store 3745 .confirm_outbound_message(&connection.id, prepared.message_id) 3746 .unwrap(); 3747 let cursor = &store.connection(&connection.id).unwrap().delivery_cursor; 3748 assert_eq!(cursor.last_sent_message_id, Some(1)); 3749 assert_eq!(cursor.last_sent_message_hash, Some(b"ciphertext".to_vec())); 3750 assert_eq!( 3751 store 3752 .outbound_message_hash(&connection.id, prepared.message_id) 3753 .unwrap(), 3754 Some(b"ciphertext".to_vec()) 3755 ); 3756 } 3757 3758 #[test] 3759 fn inbound_ack_target_uses_frame_specific_queue_after_cursor_moves() { 3760 let mut store = RadrootsSimplexAgentStore::new(); 3761 let connection = store.create_connection( 3762 RadrootsSimplexAgentConnectionMode::Direct, 3763 RadrootsSimplexAgentConnectionStatus::Connected, 3764 None, 3765 None, 3766 ); 3767 let first_queue = sample_descriptor(true).queue_address(); 3768 let second_queue = sample_descriptor_with_uri( 3769 "smp://aGVsbG8@relay.example/c2Vjb25k#/?v=4&dh=Zm9vYmFy&q=m", 3770 true, 3771 ) 3772 .queue_address(); 3773 3774 store 3775 .record_inbound_message( 3776 &connection.id, 3777 first_queue.clone(), 3778 b"first-broker-message".to_vec(), 3779 7, 3780 b"first-message-hash".to_vec(), 3781 ) 3782 .unwrap(); 3783 store 3784 .record_inbound_message( 3785 &connection.id, 3786 second_queue.clone(), 3787 b"second-broker-message".to_vec(), 3788 8, 3789 b"second-message-hash".to_vec(), 3790 ) 3791 .unwrap(); 3792 3793 assert_eq!( 3794 store 3795 .connection(&connection.id) 3796 .unwrap() 3797 .last_received_queue, 3798 Some(second_queue.clone()) 3799 ); 3800 assert_eq!( 3801 store 3802 .inbound_ack_target(&connection.id, 7, b"first-message-hash") 3803 .unwrap(), 3804 Some((first_queue, b"first-broker-message".to_vec())) 3805 ); 3806 assert_eq!( 3807 store 3808 .inbound_ack_target(&connection.id, 8, b"second-message-hash") 3809 .unwrap(), 3810 Some((second_queue, b"second-broker-message".to_vec())) 3811 ); 3812 } 3813 3814 #[cfg(feature = "std")] 3815 #[test] 3816 fn flush_and_reopen_persisted_store_state() { 3817 let tempdir = tempfile::tempdir().unwrap(); 3818 let path = tempdir.path().join("agent-store.json"); 3819 3820 let mut store = RadrootsSimplexAgentStore::open(&path).unwrap(); 3821 let connection = store.create_connection( 3822 RadrootsSimplexAgentConnectionMode::Direct, 3823 RadrootsSimplexAgentConnectionStatus::Connected, 3824 None, 3825 None, 3826 ); 3827 store 3828 .add_queue( 3829 &connection.id, 3830 sample_descriptor(true), 3831 RadrootsSimplexAgentQueueRole::Send, 3832 true, 3833 sample_auth_state(), 3834 ) 3835 .unwrap(); 3836 let prepared = store 3837 .prepare_outbound_message(&connection.id, b"persisted".to_vec()) 3838 .unwrap(); 3839 let queue = sample_descriptor(true).queue_address(); 3840 let short_reply_queue = sample_descriptor_with_uri( 3841 "smp://aGVsbG8@relay.example/cmVwbHk#/?v=4&dh=cmVwbHkta2V5&q=m", 3842 true, 3843 ) 3844 .queue_uri; 3845 let secure_short_invitation = sample_short_invitation_link(vec![2_u8; 24]); 3846 let get_short_invitation = sample_short_invitation_link(vec![3_u8; 24]); 3847 store 3848 .enqueue_command( 3849 &connection.id, 3850 RadrootsSimplexAgentPendingCommandKind::SendEnvelope { 3851 queue: queue.clone(), 3852 envelope: RadrootsSimplexAgentEnvelope::Invitation { 3853 request: b"req".to_vec(), 3854 connection_info: b"info".to_vec(), 3855 }, 3856 delivery: Some(RadrootsSimplexAgentOutboundMessage { 3857 message_id: prepared.message_id, 3858 message_hash: prepared.message_hash.clone(), 3859 }), 3860 }, 3861 10, 3862 ) 3863 .unwrap(); 3864 store 3865 .enqueue_command( 3866 &connection.id, 3867 RadrootsSimplexAgentPendingCommandKind::GetQueueMessage { 3868 queue: queue.clone(), 3869 }, 3870 11, 3871 ) 3872 .unwrap(); 3873 store 3874 .enqueue_command( 3875 &connection.id, 3876 RadrootsSimplexAgentPendingCommandKind::SetQueueLinkData { 3877 queue: queue.clone(), 3878 link_id: vec![1_u8; 24], 3879 link_data: RadrootsSimplexSmpQueueLinkData { 3880 fixed_data: b"fixed-link-data".to_vec(), 3881 user_data: b"user-link-data".to_vec(), 3882 }, 3883 }, 3884 12, 3885 ) 3886 .unwrap(); 3887 store 3888 .enqueue_command( 3889 &connection.id, 3890 RadrootsSimplexAgentPendingCommandKind::SecureGetQueueLinkData { 3891 invitation: secure_short_invitation.clone(), 3892 reply_queue: short_reply_queue.clone(), 3893 sender_auth_state: sample_auth_state(), 3894 }, 3895 13, 3896 ) 3897 .unwrap(); 3898 store 3899 .enqueue_command( 3900 &connection.id, 3901 RadrootsSimplexAgentPendingCommandKind::GetQueueLinkData { 3902 invitation: get_short_invitation.clone(), 3903 reply_queue: short_reply_queue.clone(), 3904 }, 3905 14, 3906 ) 3907 .unwrap(); 3908 { 3909 let connection = store.connection_mut(&connection.id).unwrap(); 3910 connection.hello_sent = true; 3911 connection.hello_received = true; 3912 connection.short_link = Some(sample_short_link_credentials()); 3913 connection.local_e2e_public_key = Some(b"e2e-public".to_vec()); 3914 connection.local_e2e_private_key = Some(b"e2e-private".to_vec()); 3915 connection.shared_secret = Some(b"connection-shared-secret".to_vec()); 3916 let queue = connection.queues.first_mut().unwrap(); 3917 queue.auth_state.as_mut().unwrap().private_key = b"queue-auth-private".to_vec(); 3918 queue.delivery_private_key = Some(b"queue-delivery-private".to_vec()); 3919 queue.delivery_shared_secret = Some(b"queue-delivery-shared-secret".to_vec()); 3920 let mut ratchet = 3921 RadrootsSimplexSmpRatchetState::initiator(vec![1_u8; 56], vec![2_u8; 56], None) 3922 .unwrap(); 3923 ratchet.current_pq_public_key = Some(b"ratchet-pq-public".to_vec()); 3924 ratchet.local_pq_private_key = Some(b"ratchet-pq-private".to_vec()); 3925 ratchet.local_dh_private_key = Some(b"official-private".to_vec()); 3926 ratchet.official_associated_data = Some(b"official-ad".to_vec()); 3927 ratchet.official_root_key = Some(b"official-root".to_vec()); 3928 ratchet.official_sending_chain_key = Some(b"official-send-chain".to_vec()); 3929 ratchet.official_receiving_chain_key = Some(b"official-recv-chain".to_vec()); 3930 ratchet.official_sending_header_key = Some(b"official-send-header".to_vec()); 3931 ratchet.official_receiving_header_key = Some(b"official-recv-header".to_vec()); 3932 ratchet.official_next_sending_header_key = Some(b"official-next-send-header".to_vec()); 3933 ratchet.official_next_receiving_header_key = 3934 Some(b"official-next-recv-header".to_vec()); 3935 ratchet 3936 .official_skipped_message_keys 3937 .push(RadrootsSimplexSmpSkippedMessageKey { 3938 header_key: b"official-skipped-header".to_vec(), 3939 message_number: 7, 3940 message_key: b"official-skipped-message".to_vec(), 3941 message_iv: [3_u8; RADROOTS_SIMPLEX_OFFICIAL_AES_IV_LENGTH], 3942 }); 3943 connection.ratchet_state = Some(ratchet); 3944 connection.local_x3dh_key_1 = Some(RadrootsSimplexAgentX3dhKeypair { 3945 public_key: b"x3dh-public-1".to_vec(), 3946 private_key: b"x3dh-private-1".to_vec(), 3947 }); 3948 connection.local_x3dh_key_2 = Some(RadrootsSimplexAgentX3dhKeypair { 3949 public_key: b"x3dh-public-2".to_vec(), 3950 private_key: b"x3dh-private-2".to_vec(), 3951 }); 3952 connection.local_pq_keypair = Some(RadrootsSimplexAgentPqKeypair { 3953 public_key: b"pq-public".to_vec(), 3954 private_key: b"pq-private".to_vec(), 3955 }); 3956 } 3957 store.flush().unwrap(); 3958 let raw_public = fs::read_to_string(&path).unwrap(); 3959 let public_json: serde_json::Value = serde_json::from_str(&raw_public).unwrap(); 3960 let public_connection = &public_json["connections"][0]; 3961 assert!(public_connection["local_e2e_public_key"].is_array()); 3962 assert!(public_connection["local_e2e_private_key"].is_null()); 3963 assert!(public_connection["shared_secret"].is_null()); 3964 let public_short_link = &public_connection["short_link"]; 3965 assert!(public_short_link["link_id"].is_array()); 3966 assert_eq!(public_short_link["link_key"].as_array().unwrap().len(), 0); 3967 assert!(public_short_link["link_public_signature_key"].is_array()); 3968 assert_eq!( 3969 public_short_link["link_private_signature_key"] 3970 .as_array() 3971 .unwrap() 3972 .len(), 3973 0 3974 ); 3975 assert!(public_connection["local_x3dh_key_1"]["public_key"].is_array()); 3976 assert_eq!( 3977 public_connection["local_x3dh_key_1"]["private_key"] 3978 .as_array() 3979 .unwrap() 3980 .len(), 3981 0 3982 ); 3983 assert_eq!( 3984 public_connection["local_x3dh_key_2"]["private_key"] 3985 .as_array() 3986 .unwrap() 3987 .len(), 3988 0 3989 ); 3990 assert!(public_connection["local_pq_keypair"]["public_key"].is_array()); 3991 assert_eq!( 3992 public_connection["local_pq_keypair"]["private_key"] 3993 .as_array() 3994 .unwrap() 3995 .len(), 3996 0 3997 ); 3998 let public_queue = &public_connection["queues"][0]; 3999 assert_eq!( 4000 public_queue["auth_state"]["private_key"] 4001 .as_array() 4002 .unwrap() 4003 .len(), 4004 0 4005 ); 4006 assert!(public_queue["delivery_private_key"].is_null()); 4007 assert!(public_queue["delivery_shared_secret"].is_null()); 4008 let public_ratchet = &public_connection["ratchet_state"]; 4009 for field in [ 4010 "current_pq_shared_secret", 4011 "local_pq_private_key", 4012 "local_dh_private_key", 4013 "official_root_key", 4014 "official_sending_chain_key", 4015 "official_receiving_chain_key", 4016 "official_sending_header_key", 4017 "official_receiving_header_key", 4018 "official_next_sending_header_key", 4019 "official_next_receiving_header_key", 4020 ] { 4021 assert!( 4022 public_ratchet[field].is_null(), 4023 "public ratchet leaked {field}" 4024 ); 4025 } 4026 assert_eq!( 4027 public_ratchet["official_skipped_message_keys"] 4028 .as_array() 4029 .unwrap() 4030 .len(), 4031 0 4032 ); 4033 assert!(raw_public.contains("protected_secrets")); 4034 let public_pending_commands = public_json["pending_commands"].as_array().unwrap(); 4035 let redacted_pending_short_links = public_pending_commands 4036 .iter() 4037 .filter(|command| { 4038 matches!( 4039 command["kind"]["kind"].as_str(), 4040 Some("secure_get_queue_link_data") | Some("get_queue_link_data") 4041 ) 4042 }) 4043 .collect::<Vec<_>>(); 4044 assert_eq!(redacted_pending_short_links.len(), 2); 4045 for command in redacted_pending_short_links { 4046 assert_eq!( 4047 command["kind"]["invitation"]["link_key"] 4048 .as_array() 4049 .unwrap() 4050 .len(), 4051 0 4052 ); 4053 } 4054 let protected_path = RadrootsSimplexAgentStore::protected_secrets_path(&path); 4055 let protected_raw = fs::read_to_string(&protected_path).unwrap(); 4056 for secret in [ 4057 "e2e-private", 4058 "queue-auth-private", 4059 "connection-shared-secret", 4060 "short-link-key-must-be-secret", 4061 "short-link-private-signature-key", 4062 "short-link-key-must-be-secret!!", 4063 "official-root", 4064 "x3dh-private-1", 4065 "pq-private", 4066 ] { 4067 assert!( 4068 !protected_raw.contains(secret), 4069 "protected envelope leaked {secret}" 4070 ); 4071 } 4072 assert!(RadrootsSimplexAgentStore::protected_secrets_wrapping_key_path(&path).is_file()); 4073 let diagnostics = RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap(); 4074 assert!(diagnostics.public_snapshot_exists); 4075 assert!(diagnostics.protected_secrets_configured); 4076 assert!(diagnostics.protected_secrets_exists); 4077 assert!(diagnostics.wrapping_key_exists); 4078 assert_eq!(diagnostics.protected_connection_count, 1); 4079 assert_eq!(diagnostics.protected_pending_command_count, 2); 4080 4081 let loaded = RadrootsSimplexAgentStore::open(&path).unwrap(); 4082 let loaded_connection = loaded.connection(&connection.id).unwrap(); 4083 assert_eq!( 4084 loaded_connection.staged_outbound_message, 4085 Some(RadrootsSimplexAgentOutboundMessage { 4086 message_id: 1, 4087 message_hash: b"persisted".to_vec(), 4088 }) 4089 ); 4090 assert!(loaded_connection.hello_sent); 4091 assert!(loaded_connection.hello_received); 4092 assert_eq!( 4093 loaded_connection.short_link.as_ref(), 4094 Some(&sample_short_link_credentials()) 4095 ); 4096 assert_eq!( 4097 loaded_connection 4098 .short_link 4099 .as_ref() 4100 .map(|short_link| short_link.invitation_link().link_key), 4101 Some(b"short-link-key-must-be-secret!!".to_vec()) 4102 ); 4103 assert_eq!( 4104 loaded_connection.local_e2e_private_key.as_deref(), 4105 Some(&b"e2e-private"[..]) 4106 ); 4107 assert_eq!( 4108 loaded_connection.shared_secret.as_deref(), 4109 Some(&b"connection-shared-secret"[..]) 4110 ); 4111 let loaded_queue = loaded.primary_send_queue(&connection.id).unwrap(); 4112 assert_eq!( 4113 loaded_queue 4114 .auth_state 4115 .as_ref() 4116 .map(|auth| auth.private_key.as_slice()), 4117 Some(&b"queue-auth-private"[..]) 4118 ); 4119 assert_eq!( 4120 loaded_queue.delivery_private_key.as_deref(), 4121 Some(&b"queue-delivery-private"[..]) 4122 ); 4123 assert_eq!( 4124 loaded_queue.delivery_shared_secret.as_deref(), 4125 Some(&b"queue-delivery-shared-secret"[..]) 4126 ); 4127 let loaded_ratchet = loaded_connection.ratchet_state.as_ref().unwrap(); 4128 assert_eq!( 4129 loaded_ratchet.official_associated_data.as_deref(), 4130 Some(&b"official-ad"[..]) 4131 ); 4132 assert_eq!( 4133 loaded_ratchet.official_sending_chain_key.as_deref(), 4134 Some(&b"official-send-chain"[..]) 4135 ); 4136 assert_eq!( 4137 loaded_ratchet.official_next_receiving_header_key.as_deref(), 4138 Some(&b"official-next-recv-header"[..]) 4139 ); 4140 assert_eq!( 4141 loaded_ratchet.official_skipped_message_keys, 4142 vec![RadrootsSimplexSmpSkippedMessageKey { 4143 header_key: b"official-skipped-header".to_vec(), 4144 message_number: 7, 4145 message_key: b"official-skipped-message".to_vec(), 4146 message_iv: [3_u8; RADROOTS_SIMPLEX_OFFICIAL_AES_IV_LENGTH], 4147 }] 4148 ); 4149 assert_eq!( 4150 loaded_ratchet.local_pq_private_key.as_deref(), 4151 Some(&b"ratchet-pq-private"[..]) 4152 ); 4153 assert_eq!( 4154 loaded_connection 4155 .local_x3dh_key_1 4156 .as_ref() 4157 .map(|key| (key.public_key.as_slice(), key.private_key.as_slice())), 4158 Some((&b"x3dh-public-1"[..], &b"x3dh-private-1"[..])) 4159 ); 4160 assert_eq!( 4161 loaded_connection 4162 .local_x3dh_key_2 4163 .as_ref() 4164 .map(|key| (key.public_key.as_slice(), key.private_key.as_slice())), 4165 Some((&b"x3dh-public-2"[..], &b"x3dh-private-2"[..])) 4166 ); 4167 assert_eq!( 4168 loaded_connection 4169 .local_pq_keypair 4170 .as_ref() 4171 .map(|key| (key.public_key.as_slice(), key.private_key.as_slice())), 4172 Some((&b"pq-public"[..], &b"pq-private"[..])) 4173 ); 4174 assert_eq!(loaded.pending_commands.len(), 5); 4175 assert!(loaded.pending_commands.values().any(|command| matches!( 4176 &command.kind, 4177 RadrootsSimplexAgentPendingCommandKind::GetQueueMessage { queue: persisted_queue } 4178 if persisted_queue == &queue 4179 ))); 4180 assert!(loaded.pending_commands.values().any(|command| matches!( 4181 &command.kind, 4182 RadrootsSimplexAgentPendingCommandKind::SetQueueLinkData { 4183 queue: persisted_queue, 4184 link_id, 4185 link_data 4186 } if persisted_queue == &queue 4187 && link_id.as_slice() == &[1_u8; 24] 4188 && link_data.fixed_data.as_slice() == b"fixed-link-data" 4189 && link_data.user_data.as_slice() == b"user-link-data" 4190 ))); 4191 assert!(loaded.pending_commands.values().any(|command| matches!( 4192 &command.kind, 4193 RadrootsSimplexAgentPendingCommandKind::SecureGetQueueLinkData { 4194 invitation, 4195 reply_queue, 4196 .. 4197 } if invitation == &secure_short_invitation 4198 && reply_queue == &short_reply_queue 4199 ))); 4200 assert!(loaded.pending_commands.values().any(|command| matches!( 4201 &command.kind, 4202 RadrootsSimplexAgentPendingCommandKind::GetQueueLinkData { 4203 invitation, 4204 reply_queue 4205 } if invitation == &get_short_invitation 4206 && reply_queue == &short_reply_queue 4207 ))); 4208 assert!( 4209 loaded 4210 .primary_send_queue(&connection.id) 4211 .unwrap() 4212 .auth_state 4213 .is_some() 4214 ); 4215 } 4216 4217 #[cfg(feature = "std")] 4218 #[test] 4219 fn protected_snapshot_persists_full_agent_state_without_plaintext_json() { 4220 let tempdir = tempfile::tempdir().unwrap(); 4221 let path = tempdir.path().join("agent-store.protected.json"); 4222 let vault = Arc::new(TestSecretVault::new()); 4223 let mut store = RadrootsSimplexAgentStore::open_protected_with_vault( 4224 &path, 4225 vault.clone(), 4226 "test-agent-master", 4227 ) 4228 .unwrap(); 4229 let connection = store.create_connection( 4230 RadrootsSimplexAgentConnectionMode::Direct, 4231 RadrootsSimplexAgentConnectionStatus::Connected, 4232 None, 4233 None, 4234 ); 4235 store 4236 .add_queue( 4237 &connection.id, 4238 sample_descriptor(true), 4239 RadrootsSimplexAgentQueueRole::Send, 4240 true, 4241 sample_auth_state(), 4242 ) 4243 .unwrap(); 4244 store.connection_mut(&connection.id).unwrap().shared_secret = 4245 Some(b"connection-shared-secret".to_vec()); 4246 store.flush().unwrap(); 4247 4248 let raw = fs::read_to_string(&path).unwrap(); 4249 assert!(!raw.contains("connections")); 4250 assert!(!raw.contains("relay.example")); 4251 assert!(!raw.contains("connection-shared-secret")); 4252 assert!(serde_json::from_str::<RadrootsSimplexAgentStoreSnapshot>(&raw).is_err()); 4253 assert!(!RadrootsSimplexAgentStore::protected_secrets_path(&path).exists()); 4254 assert!(!RadrootsSimplexAgentStore::protected_secrets_wrapping_key_path(&path).exists()); 4255 4256 let loaded = RadrootsSimplexAgentStore::open_protected_with_vault( 4257 &path, 4258 vault.clone(), 4259 "test-agent-master", 4260 ) 4261 .unwrap(); 4262 let loaded_connection = loaded.connection(&connection.id).unwrap(); 4263 assert_eq!( 4264 loaded_connection.shared_secret.as_deref(), 4265 Some(&b"connection-shared-secret"[..]) 4266 ); 4267 assert!( 4268 loaded 4269 .primary_send_queue(&connection.id) 4270 .unwrap() 4271 .auth_state 4272 .is_some() 4273 ); 4274 } 4275 4276 #[cfg(feature = "std")] 4277 #[test] 4278 fn protected_snapshot_wrong_vault_fails_open() { 4279 let tempdir = tempfile::tempdir().unwrap(); 4280 let path = tempdir.path().join("agent-store.protected.json"); 4281 let vault = Arc::new(TestSecretVault::new()); 4282 let mut store = RadrootsSimplexAgentStore::open_protected_with_vault( 4283 &path, 4284 vault.clone(), 4285 "test-agent-master", 4286 ) 4287 .unwrap(); 4288 store.create_connection( 4289 RadrootsSimplexAgentConnectionMode::Direct, 4290 RadrootsSimplexAgentConnectionStatus::Connected, 4291 None, 4292 None, 4293 ); 4294 store.flush().unwrap(); 4295 4296 let error = RadrootsSimplexAgentStore::open_protected_with_vault( 4297 &path, 4298 Arc::new(TestSecretVault::new()), 4299 "test-agent-master", 4300 ) 4301 .unwrap_err(); 4302 4303 assert!(error.to_string().contains("failed to open")); 4304 } 4305 4306 #[cfg(feature = "std")] 4307 #[test] 4308 fn corrupt_protected_snapshot_fails_open() { 4309 let tempdir = tempfile::tempdir().unwrap(); 4310 let path = tempdir.path().join("agent-store.protected.json"); 4311 let vault = Arc::new(TestSecretVault::new()); 4312 fs::write(&path, b"not-json").unwrap(); 4313 4314 let error = RadrootsSimplexAgentStore::open_protected_with_vault( 4315 &path, 4316 vault.clone(), 4317 "test-agent-master", 4318 ) 4319 .unwrap_err(); 4320 4321 assert!(error.to_string().contains("failed to decode")); 4322 } 4323 4324 #[cfg(feature = "std")] 4325 #[test] 4326 fn corrupt_protected_sidecar_fails_open_and_diagnostics() { 4327 let tempdir = tempfile::tempdir().unwrap(); 4328 let path = tempdir.path().join("agent-store.json"); 4329 persisted_store_with_secret_material(&path); 4330 fs::write( 4331 RadrootsSimplexAgentStore::protected_secrets_path(&path), 4332 b"not-json", 4333 ) 4334 .unwrap(); 4335 4336 let open_error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4337 assert!(open_error.to_string().contains("failed to decode")); 4338 let diagnostics_error = 4339 RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap_err(); 4340 assert!(diagnostics_error.to_string().contains("failed to decode")); 4341 } 4342 4343 #[cfg(feature = "std")] 4344 #[test] 4345 fn missing_wrapping_key_fails_open_and_diagnostics() { 4346 let tempdir = tempfile::tempdir().unwrap(); 4347 let path = tempdir.path().join("agent-store.json"); 4348 persisted_store_with_secret_material(&path); 4349 fs::remove_file(RadrootsSimplexAgentStore::protected_secrets_wrapping_key_path(&path)) 4350 .unwrap(); 4351 4352 let open_error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4353 assert!(open_error.to_string().contains("failed to open")); 4354 let diagnostics_error = 4355 RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap_err(); 4356 assert!(diagnostics_error.to_string().contains("failed to open")); 4357 } 4358 4359 #[cfg(feature = "std")] 4360 #[test] 4361 fn missing_protected_sidecar_fails_open_and_diagnostics() { 4362 let tempdir = tempfile::tempdir().unwrap(); 4363 let path = tempdir.path().join("agent-store.json"); 4364 persisted_store_with_secret_material(&path); 4365 fs::remove_file(RadrootsSimplexAgentStore::protected_secrets_path(&path)).unwrap(); 4366 4367 let open_error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4368 assert!(open_error.to_string().contains("failed to read")); 4369 let diagnostics_error = 4370 RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap_err(); 4371 assert!(diagnostics_error.to_string().contains("failed to read")); 4372 } 4373 4374 #[cfg(feature = "std")] 4375 #[test] 4376 fn stale_protected_generation_fails_open_and_diagnostics() { 4377 let tempdir = tempfile::tempdir().unwrap(); 4378 let path = tempdir.path().join("agent-store.json"); 4379 persisted_store_with_secret_material(&path); 4380 let mut public_json = read_public_snapshot(&path); 4381 public_json["protected_secrets"]["generation"] = serde_json::Value::String("0".repeat(64)); 4382 write_public_snapshot(&path, &public_json); 4383 4384 let open_error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4385 assert!(open_error.to_string().contains("does not match")); 4386 let diagnostics_error = 4387 RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap_err(); 4388 assert!(diagnostics_error.to_string().contains("does not match")); 4389 } 4390 4391 #[cfg(feature = "std")] 4392 #[test] 4393 fn public_snapshot_and_protected_sidecar_skew_is_rejected() { 4394 let tempdir = tempfile::tempdir().unwrap(); 4395 let path = tempdir.path().join("agent-store.json"); 4396 persisted_store_with_secret_material(&path); 4397 let old_public_json = read_public_snapshot(&path); 4398 let mut store = RadrootsSimplexAgentStore::open(&path).unwrap(); 4399 let second_connection = store.create_connection( 4400 RadrootsSimplexAgentConnectionMode::Direct, 4401 RadrootsSimplexAgentConnectionStatus::Connected, 4402 None, 4403 None, 4404 ); 4405 store 4406 .add_queue( 4407 &second_connection.id, 4408 sample_descriptor_with_uri( 4409 "smp://aGVsbG8@relay-second.example/cXVldWU#/?v=4&dh=Zm9vYmFy&q=m", 4410 true, 4411 ), 4412 RadrootsSimplexAgentQueueRole::Send, 4413 true, 4414 sample_auth_state(), 4415 ) 4416 .unwrap(); 4417 store 4418 .connection_mut(&second_connection.id) 4419 .unwrap() 4420 .shared_secret = Some(b"second-secret".to_vec()); 4421 store.flush().unwrap(); 4422 write_public_snapshot(&path, &old_public_json); 4423 4424 let open_error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4425 assert!(open_error.to_string().contains("does not match")); 4426 let diagnostics_error = 4427 RadrootsSimplexAgentStore::protected_secrets_diagnostics(&path).unwrap_err(); 4428 assert!(diagnostics_error.to_string().contains("does not match")); 4429 } 4430 4431 #[cfg(feature = "std")] 4432 #[test] 4433 fn plaintext_snapshot_without_protected_metadata_is_rejected() { 4434 let tempdir = tempfile::tempdir().unwrap(); 4435 let path = tempdir.path().join("agent-store.json"); 4436 let mut store = RadrootsSimplexAgentStore::new(); 4437 let connection = store.create_connection( 4438 RadrootsSimplexAgentConnectionMode::Direct, 4439 RadrootsSimplexAgentConnectionStatus::Connected, 4440 None, 4441 None, 4442 ); 4443 store 4444 .add_queue( 4445 &connection.id, 4446 sample_descriptor(true), 4447 RadrootsSimplexAgentQueueRole::Send, 4448 true, 4449 sample_auth_state(), 4450 ) 4451 .unwrap(); 4452 store.connection_mut(&connection.id).unwrap().shared_secret = 4453 Some(b"plaintext-secret".to_vec()); 4454 let snapshot = store.snapshot().unwrap(); 4455 fs::write( 4456 &path, 4457 format!("{}\n", serde_json::to_string_pretty(&snapshot).unwrap()), 4458 ) 4459 .unwrap(); 4460 4461 let error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4462 assert!(error.to_string().contains("without protected metadata")); 4463 } 4464 4465 #[cfg(feature = "std")] 4466 #[test] 4467 fn pending_short_invitation_link_key_without_protected_metadata_is_rejected() { 4468 let tempdir = tempfile::tempdir().unwrap(); 4469 let path = tempdir.path().join("agent-store.json"); 4470 let mut store = RadrootsSimplexAgentStore::new(); 4471 let connection = store.create_connection( 4472 RadrootsSimplexAgentConnectionMode::Direct, 4473 RadrootsSimplexAgentConnectionStatus::JoinPending, 4474 None, 4475 None, 4476 ); 4477 store 4478 .enqueue_command( 4479 &connection.id, 4480 RadrootsSimplexAgentPendingCommandKind::GetQueueLinkData { 4481 invitation: sample_short_invitation_link(vec![4_u8; 24]), 4482 reply_queue: sample_descriptor(true).queue_uri, 4483 }, 4484 10, 4485 ) 4486 .unwrap(); 4487 let snapshot = store.snapshot().unwrap(); 4488 fs::write( 4489 &path, 4490 format!("{}\n", serde_json::to_string_pretty(&snapshot).unwrap()), 4491 ) 4492 .unwrap(); 4493 4494 let error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4495 assert!(error.to_string().contains("without protected metadata")); 4496 } 4497 4498 #[cfg(feature = "std")] 4499 #[test] 4500 fn pending_short_invitation_plaintext_link_key_with_protected_metadata_is_rejected() { 4501 let tempdir = tempfile::tempdir().unwrap(); 4502 let path = tempdir.path().join("agent-store.json"); 4503 let mut store = RadrootsSimplexAgentStore::open(&path).unwrap(); 4504 let connection = store.create_connection( 4505 RadrootsSimplexAgentConnectionMode::Direct, 4506 RadrootsSimplexAgentConnectionStatus::JoinPending, 4507 None, 4508 None, 4509 ); 4510 store 4511 .enqueue_command( 4512 &connection.id, 4513 RadrootsSimplexAgentPendingCommandKind::SecureGetQueueLinkData { 4514 invitation: sample_short_invitation_link(vec![5_u8; 24]), 4515 reply_queue: sample_descriptor(true).queue_uri, 4516 sender_auth_state: sample_auth_state(), 4517 }, 4518 10, 4519 ) 4520 .unwrap(); 4521 store.flush().unwrap(); 4522 let mut public_json = read_public_snapshot(&path); 4523 public_json["pending_commands"][0]["kind"]["invitation"]["link_key"] = 4524 serde_json::Value::Array(vec![serde_json::Value::from(7)]); 4525 write_public_snapshot(&path, &public_json); 4526 4527 let error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4528 assert!(error.to_string().contains("plaintext secret material")); 4529 } 4530 4531 #[cfg(feature = "std")] 4532 #[test] 4533 fn redacted_markers_without_protected_metadata_are_rejected() { 4534 let tempdir = tempfile::tempdir().unwrap(); 4535 let path = tempdir.path().join("agent-store.json"); 4536 persisted_store_with_secret_material(&path); 4537 let mut public_json = read_public_snapshot(&path); 4538 public_json 4539 .as_object_mut() 4540 .unwrap() 4541 .remove("protected_secrets"); 4542 write_public_snapshot(&path, &public_json); 4543 4544 let error = RadrootsSimplexAgentStore::open(&path).unwrap_err(); 4545 assert!(error.to_string().contains("without protected metadata")); 4546 } 4547 4548 #[cfg(feature = "std")] 4549 #[test] 4550 fn ambiguous_queue_secret_merge_is_rejected() { 4551 let mut store = RadrootsSimplexAgentStore::new(); 4552 let connection = store.create_connection( 4553 RadrootsSimplexAgentConnectionMode::Direct, 4554 RadrootsSimplexAgentConnectionStatus::Connected, 4555 None, 4556 None, 4557 ); 4558 store 4559 .add_queue( 4560 &connection.id, 4561 sample_descriptor_with_uri( 4562 "smp://aGVsbG8@relay-a.example/cXVldWU#/?v=4&dh=Zm9vYmFy&q=m", 4563 true, 4564 ), 4565 RadrootsSimplexAgentQueueRole::Send, 4566 true, 4567 sample_auth_state(), 4568 ) 4569 .unwrap(); 4570 store 4571 .add_queue( 4572 &connection.id, 4573 sample_descriptor_with_uri( 4574 "smp://aGVsbG8@relay-b.example/cXVldWU#/?v=4&dh=Zm9vYmFy&q=m", 4575 false, 4576 ), 4577 RadrootsSimplexAgentQueueRole::Send, 4578 false, 4579 sample_auth_state(), 4580 ) 4581 .unwrap(); 4582 let mut snapshot = 4583 connection_to_snapshot(store.connection(&connection.id).unwrap().clone()) 4584 .expect("snapshot"); 4585 let entity_id = snapshot.queues[0].entity_id.clone(); 4586 let secrets = RadrootsSimplexAgentConnectionSecretsSnapshot { 4587 id: connection.id, 4588 short_link_link_key: None, 4589 short_link_private_signature_key: None, 4590 queues: vec![RadrootsSimplexAgentQueueSecretsSnapshot { 4591 entity_id, 4592 role: "send".to_owned(), 4593 queue_address: None, 4594 auth_private_key: Some(b"secret".to_vec()), 4595 delivery_private_key: None, 4596 delivery_shared_secret: None, 4597 }], 4598 ratchet_state: None, 4599 local_e2e_private_key: None, 4600 local_x3dh_key_1_private_key: None, 4601 local_x3dh_key_2_private_key: None, 4602 local_pq_private_key: None, 4603 shared_secret: None, 4604 }; 4605 4606 let error = merge_connection_secrets(&mut snapshot, secrets).unwrap_err(); 4607 assert!(error.to_string().contains("ambiguous queue")); 4608 } 4609 4610 #[cfg(feature = "std")] 4611 #[test] 4612 fn flush_without_secrets_removes_stale_protected_sidecars() { 4613 let tempdir = tempfile::tempdir().unwrap(); 4614 let path = tempdir.path().join("agent-store.json"); 4615 fs::write( 4616 RadrootsSimplexAgentStore::protected_secrets_path(&path), 4617 b"stale", 4618 ) 4619 .unwrap(); 4620 fs::write( 4621 RadrootsSimplexAgentStore::protected_secrets_wrapping_key_path(&path), 4622 b"stale", 4623 ) 4624 .unwrap(); 4625 4626 let mut store = RadrootsSimplexAgentStore::open(&path).unwrap(); 4627 store.create_connection( 4628 RadrootsSimplexAgentConnectionMode::Direct, 4629 RadrootsSimplexAgentConnectionStatus::Connected, 4630 None, 4631 None, 4632 ); 4633 store.flush().unwrap(); 4634 4635 let raw_public = fs::read_to_string(&path).unwrap(); 4636 assert!(!raw_public.contains("protected_secrets")); 4637 assert!(!RadrootsSimplexAgentStore::protected_secrets_path(&path).exists()); 4638 assert!(!RadrootsSimplexAgentStore::protected_secrets_wrapping_key_path(&path).exists()); 4639 } 4640 }