lib

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

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 }