amadeus_node/node/
reassembler.rs

1use crate::utils::PublicKey;
2use crate::utils::{bls12_381, misc::get_unix_millis_now, misc::get_unix_nanos_now};
3use crate::{Config, Ver};
4use aes_gcm::aead::{Aead, AeadCore, OsRng};
5use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce};
6use std::collections::HashMap;
7use std::hash::{Hash as StdHash, Hasher};
8use tokio::sync::RwLock;
9
10#[derive(Debug, thiserror::Error, strum_macros::IntoStaticStr)]
11pub enum Error {
12    #[error("AES encryption error")]
13    AesError,
14    #[error(transparent)]
15    Bls(#[from] bls12_381::Error),
16    #[error(transparent)]
17    ReedSolomon(#[from] crate::utils::reed_solomon::Error),
18    #[error(transparent)]
19    Compression(#[from] std::io::Error),
20    #[error("Invalid message format")]
21    InvalidFormat,
22    #[error("Payload too small for nonce")]
23    PayloadTooSmall,
24}
25
26impl crate::utils::misc::Typename for Error {
27    fn typename(&self) -> &'static str {
28        self.into()
29    }
30}
31
32/// Message format with AES-256-GCM encryption and Reed-Solomon sharding
33/// Compatible with Elixir implementation using BLS-compatible shared secrets
34#[derive(Debug, Clone)]
35pub struct Message {
36    pub version: Ver,
37    pub pk: PublicKey,      // Sender's public key
38    pub shard_index: u16,   // Current shard index
39    pub shard_total: u16,   // Total number of shards
40    pub ts_nano: u64,       // Timestamp in nanoseconds
41    pub original_size: u32, // Size of original plaintext
42    pub payload: Vec<u8>,   // Encrypted data (for single shard) or encrypted Reed-Solomon shard
43}
44
45impl Message {
46    /// Calculate Reed-Solomon parameters based on payload size
47    fn calculate_reed_solomon_params(payload_len: usize) -> (usize, usize, u16, usize) {
48        let data_shards = payload_len.div_ceil(1024);
49        let parity_shards = data_shards;
50        let total_shards = (data_shards + parity_shards) as u16;
51        let shards_to_send = data_shards + 1 + (data_shards / 4);
52        (data_shards, parity_shards, total_shards, shards_to_send)
53    }
54    /// Derive AES-256 key using Elixir-compatible method: SHA256(shared_secret + timestamp_in_nanoseconds + iv)
55    /// IMPORTANT: Elixir uses :binary.encode_unsigned which strips leading zeros
56    fn derive_aes_key(shared_secret: &[u8], ts_nano: u64, iv: &[u8]) -> [u8; 32] {
57        use sha2::{Digest, Sha256};
58
59        let mut hasher = Sha256::new();
60        hasher.update(shared_secret);
61        // encode_unsigned strips leading zeros - match Elixir behavior
62        let ts_bytes = ts_nano.to_be_bytes();
63        let first_nonzero = ts_bytes.iter().position(|&b| b != 0).unwrap_or(7);
64        hasher.update(&ts_bytes[first_nonzero..]);
65        hasher.update(iv);
66
67        let result = hasher.finalize();
68        let mut key = [0u8; 32];
69        key.copy_from_slice(&result);
70        key
71    }
72
73    /// Encrypt a message and optionally shard it using Reed-Solomon
74    /// Returns a vector of Message instances (one per shard)
75    pub fn encrypt(
76        sender_pk: &PublicKey,
77        shared_secret: &[u8],
78        plaintext: &[u8],
79        version: Ver,
80    ) -> Result<Vec<Self>, Error> {
81        let ts_nano = get_unix_nanos_now() as u64;
82
83        // Compress first - use zstd for v1.2.3+, zlib for older versions
84        let compressed = if version >= Ver::new(1, 2, 3) {
85            zstd::encode_all(plaintext, 3).map_err(|e| Error::Compression(e.into()))?
86        } else {
87            crate::utils::compression::compress_with_zlib(plaintext)?
88        };
89
90        // AES-256-GCM encryption with Elixir-compatible key derivation
91        let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
92        let key_bytes = Self::derive_aes_key(shared_secret, ts_nano, &nonce);
93        let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
94        let ciphertext_with_tag = cipher.encrypt(&nonce, compressed.as_slice()).map_err(|_| Error::AesError)?;
95
96        // Combine in Elixir format: nonce + tag + ciphertext
97        let (ciphertext, tag) = ciphertext_with_tag.split_at(ciphertext_with_tag.len() - 16);
98        let mut encrypted_payload = Vec::with_capacity(12 + 16 + ciphertext.len());
99        encrypted_payload.extend_from_slice(&nonce);
100        encrypted_payload.extend_from_slice(tag);
101        encrypted_payload.extend_from_slice(ciphertext);
102
103        // Check if we need Reed-Solomon sharding
104        if encrypted_payload.len() < 1300 {
105            // Single shard
106            Ok(vec![Self {
107                version,
108                pk: *sender_pk,
109                shard_index: 0,
110                shard_total: 1,
111                ts_nano,
112                original_size: encrypted_payload.len() as u32,
113                payload: encrypted_payload,
114            }])
115        } else {
116            // Multi-shard with Reed-Solomon
117            let (data_shards, parity_shards, total_shards, shards_to_send) =
118                Self::calculate_reed_solomon_params(encrypted_payload.len());
119
120            let mut rs = crate::utils::reed_solomon::ReedSolomonResource::new(data_shards, parity_shards)?;
121            let encoded_shards = rs.encode_shards(&encrypted_payload)?;
122
123            let limited_shards: Vec<_> = encoded_shards.into_iter().take(shards_to_send).collect();
124
125            let mut messages = Vec::new();
126            for (shard_index, shard_payload) in limited_shards {
127                messages.push(Self {
128                    version,
129                    pk: *sender_pk,
130                    shard_index: shard_index as u16,
131                    shard_total: total_shards,
132                    ts_nano,
133                    original_size: encrypted_payload.len() as u32,
134                    payload: shard_payload,
135                });
136            }
137
138            Ok(messages)
139        }
140    }
141
142    /// Decrypt a single Message (includes decompression for direct use)
143    pub fn decrypt(&self, shared_secret: &[u8]) -> Result<Vec<u8>, Error> {
144        let compressed = self.decrypt_raw(shared_secret)?;
145        // Decompress based on sender version
146        // v1.2.3+ uses zstd, older uses deflate
147        let plaintext = if self.version >= Ver::new(1, 2, 3) {
148            zstd::decode_all(compressed.as_slice()).map_err(|e| Error::Compression(e.into()))?
149        } else {
150            crate::utils::compression::decompress_with_zlib(&compressed)?
151        };
152        Ok(plaintext)
153    }
154
155    /// Raw decryption without decompression (for reassembler use)
156    fn decrypt_raw(&self, shared_secret: &[u8]) -> Result<Vec<u8>, Error> {
157        if self.payload.len() < 28 {
158            // 12 (nonce) + 16 (tag) + minimum ciphertext
159            return Err(Error::PayloadTooSmall);
160        }
161
162        // Extract nonce, tag, and ciphertext in Elixir format: nonce + tag + ciphertext
163        let nonce_bytes = &self.payload[0..12];
164        let tag_bytes = &self.payload[12..28];
165        let ciphertext = &self.payload[28..];
166
167        // Reconstruct ciphertext_with_tag for AES-GCM decryption
168        let mut ciphertext_with_tag = Vec::with_capacity(ciphertext.len() + 16);
169        ciphertext_with_tag.extend_from_slice(ciphertext);
170        ciphertext_with_tag.extend_from_slice(tag_bytes);
171
172        // Decrypt with Elixir-compatible key derivation
173        let key_bytes = Self::derive_aes_key(shared_secret, self.ts_nano, nonce_bytes);
174        let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key_bytes));
175        let nonce = Nonce::from_slice(nonce_bytes);
176
177        cipher.decrypt(nonce, ciphertext_with_tag.as_slice()).map_err(|_e| {
178            tracing::debug!(
179                "AES decrypt failed. pk={} ts_nano={} nonce={} tag={} ciphertext_len={} key={}",
180                hex::encode(&self.pk),
181                self.ts_nano,
182                hex::encode(nonce_bytes),
183                hex::encode(tag_bytes),
184                ciphertext.len(),
185                hex::encode(&key_bytes)
186            );
187            Error::AesError
188        })
189    }
190
191    /// Serialize to binary format
192    pub fn to_bytes(&self) -> Vec<u8> {
193        let ver = self.version.as_bytes();
194        let capacity = 3 + 3 + 1 + 48 + 2 + 2 + 8 + 4 + self.payload.len();
195        let mut out = Vec::with_capacity(capacity);
196
197        // "AMA" (Amadeus Message Authentication)
198        out.extend_from_slice(b"AMA");
199
200        // version_3byte
201        out.extend_from_slice(&ver);
202
203        // reserved byte
204        out.push(0);
205
206        // pk (48 bytes)
207        out.extend_from_slice(self.pk.as_ref());
208
209        // shard_index::16, shard_total::16 (big-endian)
210        out.extend_from_slice(&self.shard_index.to_be_bytes());
211        out.extend_from_slice(&self.shard_total.to_be_bytes());
212
213        // ts_nano::64 (big-endian)
214        out.extend_from_slice(&self.ts_nano.to_be_bytes());
215
216        // original_size::32 (big-endian)
217        out.extend_from_slice(&self.original_size.to_be_bytes());
218
219        // encrypted payload
220        out.extend_from_slice(&self.payload);
221
222        out
223    }
224}
225
226impl TryFrom<&[u8]> for Message {
227    type Error = Error;
228
229    fn try_from(bin: &[u8]) -> Result<Self, Self::Error> {
230        // Minimum header length (including reserved byte)
231        if bin.len() < 3 + 3 + 1 + 48 + 2 + 2 + 8 + 4 {
232            return Err(Error::InvalidFormat);
233        }
234
235        // Check magic
236        if &bin[0..3] != b"AMA" {
237            return Err(Error::InvalidFormat);
238        }
239
240        let version_bytes = &bin[3..6];
241        let version = Ver::new(version_bytes[0], version_bytes[1], version_bytes[2]);
242
243        // Skip reserved byte at position 6
244        let pk_start = 7; // Was 6, now 7 to skip the reserved byte
245        let pk_end = pk_start + 48;
246        let pk = bin[pk_start..pk_end].try_into().expect("pk should be 48 bytes");
247
248        let shard_index = u16::from_be_bytes(bin[pk_end..pk_end + 2].try_into().unwrap());
249        let shard_total = u16::from_be_bytes(bin[pk_end + 2..pk_end + 4].try_into().unwrap());
250
251        let ts_nano = u64::from_be_bytes(bin[pk_end + 4..pk_end + 12].try_into().unwrap());
252        let original_size = u32::from_be_bytes(bin[pk_end + 12..pk_end + 16].try_into().unwrap());
253
254        let payload = bin[pk_end + 16..].to_vec();
255
256        Ok(Self { version, pk, shard_index, shard_total, ts_nano, original_size, payload })
257    }
258}
259
260/// Reassembler for encrypted message shards with Reed-Solomon error correction
261pub struct ReedSolomonReassembler {
262    reorg: RwLock<HashMap<ReassemblyKey, TimedEntryState>>,
263    cache: RwLock<HashMap<PublicKey, TimedSharedSecret>>,
264}
265
266struct TimedSharedSecret {
267    shared_secret: PublicKey,
268    ts_m: u64,
269}
270
271impl TimedSharedSecret {
272    fn new(shared_secret: PublicKey) -> Self {
273        let ts_m = get_unix_millis_now();
274        Self { shared_secret, ts_m }
275    }
276}
277
278#[derive(Clone, Debug, Eq)]
279struct ReassemblyKey {
280    pk: PublicKey,
281    ts_nano: u64,
282    shard_total: u16,
283    original_size: u32,
284    version: Ver,
285}
286
287impl From<&Message> for ReassemblyKey {
288    fn from(msg: &Message) -> Self {
289        Self {
290            pk: msg.pk,
291            ts_nano: msg.ts_nano,
292            shard_total: msg.shard_total,
293            original_size: msg.original_size,
294            version: msg.version,
295        }
296    }
297}
298
299impl PartialEq for ReassemblyKey {
300    fn eq(&self, other: &Self) -> bool {
301        self.pk == other.pk && self.ts_nano == other.ts_nano && self.shard_total == other.shard_total
302    }
303}
304
305impl StdHash for ReassemblyKey {
306    fn hash<H: Hasher>(&self, state: &mut H) {
307        self.pk.hash(state);
308        self.ts_nano.hash(state);
309        self.shard_total.hash(state);
310    }
311}
312
313#[derive(Debug)]
314enum EntryState {
315    Collecting(HashMap<u16, Vec<u8>>), // shard_index -> encrypted shard data
316    Spent,
317}
318
319struct TimedEntryState {
320    ts_m: u64,
321    state: EntryState,
322}
323
324impl TimedEntryState {
325    fn new(state: EntryState) -> Self {
326        let ts_m = get_unix_millis_now();
327        Self { ts_m, state }
328    }
329}
330
331impl Default for ReedSolomonReassembler {
332    fn default() -> Self {
333        Self::new()
334    }
335}
336
337impl ReedSolomonReassembler {
338    pub fn new() -> Self {
339        Self { reorg: RwLock::new(HashMap::new()), cache: RwLock::new(HashMap::new()) }
340    }
341
342    /// Clean up stale incomplete reassembly entries older than `seconds`
343    pub async fn clear_stale(&self) -> usize {
344        let threshold_ms = get_unix_millis_now().saturating_sub(8_000);
345        let mut map = self.reorg.write().await;
346        let size_before = map.len();
347        map.retain(|_k, v| v.ts_m > threshold_ms);
348        let cleared = size_before - map.len();
349        let mut map = self.cache.write().await;
350        map.retain(|_k, v| v.ts_m > threshold_ms);
351
352        cleared
353    }
354
355    /// Add a shard to the reassembly, return complete message if ready
356    /// Takes binary data and parses it as Message
357    pub async fn add_shard(&self, bin: &[u8], config_sk: &[u8]) -> Result<Option<(Vec<u8>, PublicKey)>, Error> {
358        let encrypted_msg = Message::try_from(bin)?;
359        let key = ReassemblyKey::from(&encrypted_msg);
360
361        // Single shard message
362        if key.shard_total == 1 {
363            let shared_secret = bls12_381::get_shared_secret(&key.pk, config_sk)?;
364            // Decrypt and then decompress (reverse of build_shards process)
365            let decrypted_compressed = encrypted_msg.decrypt_raw(shared_secret.as_ref())?;
366            // Decompress based on sender version - must match what Message::encrypt uses
367            let payload = if key.version >= Ver::new(1, 2, 3) {
368                zstd::decode_all(decrypted_compressed.as_slice()).map_err(|e| Error::Compression(e.into()))?
369            } else {
370                crate::utils::compression::decompress_with_zlib(&decrypted_compressed)?
371            };
372            return Ok(Some((payload, key.pk)));
373        }
374
375        let data_shards = (key.shard_total / 2) as usize;
376
377        // Insert or update under lock; if threshold met, collect shards and mark Spent
378        let mut maybe_shards: Option<Vec<(usize, Vec<u8>)>> = None;
379        {
380            let mut map = self.reorg.write().await;
381            use std::collections::hash_map::Entry;
382            match map.entry(key.clone()) {
383                Entry::Vacant(v) => {
384                    let mut state_map = HashMap::new();
385                    state_map.insert(encrypted_msg.shard_index, encrypted_msg.payload.clone());
386                    v.insert(TimedEntryState::new(EntryState::Collecting(state_map)));
387                }
388                Entry::Occupied(mut occ) => {
389                    match occ.get_mut() {
390                        TimedEntryState { state: EntryState::Spent, .. } => {
391                            // nothing to do
392                        }
393                        TimedEntryState { state: EntryState::Collecting(shards_map), .. } => {
394                            shards_map.insert(encrypted_msg.shard_index, encrypted_msg.payload.clone());
395                            if shards_map.len() >= data_shards {
396                                let shards: Vec<(usize, Vec<u8>)> =
397                                    shards_map.iter().map(|(idx, bytes)| (*idx as usize, bytes.clone())).collect();
398                                // Mark as spent to avoid reuse and release memory
399                                *occ.get_mut() = TimedEntryState::new(EntryState::Spent);
400                                maybe_shards = Some(shards);
401                            }
402                        }
403                    }
404                }
405            }
406        }
407
408        if let Some(shards) = maybe_shards {
409            // Decode outside the lock - note: we reconstruct the encrypted payload, not the original
410            let mut rs_res = crate::utils::reed_solomon::ReedSolomonResource::new(data_shards, data_shards)?;
411            // For Message, we reconstruct to get the encrypted payload (nonce + ciphertext)
412            // The original_size in the key refers to the encrypted payload size, not the plaintext size
413            let encrypted_payload =
414                rs_res.decode_shards(shards, key.shard_total as usize, key.original_size as usize)?;
415
416            let shared_secret = bls12_381::get_shared_secret(&key.pk, config_sk)?;
417
418            // Create a temporary Message for decryption
419            let temp_msg = Message {
420                version: key.version,
421                pk: key.pk,
422                shard_index: 0,
423                shard_total: 1,
424                ts_nano: key.ts_nano,
425                original_size: key.original_size,
426                payload: encrypted_payload,
427            };
428
429            // Decrypt and then decompress (reverse of build_shards process)
430            let decrypted_compressed = temp_msg.decrypt_raw(shared_secret.as_ref())?;
431            // Decompress based on sender version - must match what Message::encrypt uses
432            let payload = if key.version >= Ver::new(1, 2, 3) {
433                zstd::decode_all(decrypted_compressed.as_slice()).map_err(|e| Error::Compression(e.into()))?
434            } else {
435                crate::utils::compression::decompress_with_zlib(&decrypted_compressed)?
436            };
437            return Ok(Some((payload, key.pk)));
438        }
439
440        Ok(None)
441    }
442
443    /// Creates encrypted message shards from payload and target public key
444    /// This is the main method for sending encrypted messages to specific recipients
445    pub async fn build_shards(
446        &self,
447        config: &Config,
448        payload: &[u8],
449        target_pk: &PublicKey,
450    ) -> Result<Vec<Vec<u8>>, Error> {
451        let version = config.get_ver();
452        let sender_pk = config.get_pk();
453        let shared_secret = self.get_shared_secret(config, target_pk).await?;
454        let encrypted_messages = Message::encrypt(&sender_pk, shared_secret.as_ref(), payload, version)?;
455
456        let mut shards = Vec::new();
457        for encrypted_msg in encrypted_messages {
458            shards.push(encrypted_msg.to_bytes());
459        }
460
461        Ok(shards)
462    }
463
464    async fn get_shared_secret(&self, config: &Config, pk: &PublicKey) -> Result<PublicKey, Error> {
465        use std::collections::hash_map::Entry;
466
467        let mut map = self.cache.write().await;
468        match map.entry(pk.clone()) {
469            Entry::Vacant(v) => {
470                let shared_secret = bls12_381::get_shared_secret(pk, &config.get_sk())?;
471                v.insert(TimedSharedSecret::new(shared_secret));
472                Ok(shared_secret)
473            }
474            Entry::Occupied(e) => Ok(e.get().shared_secret),
475        }
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::utils::bls12_381;
483
484    #[test]
485    fn test_encrypted_message_round_trip() {
486        // Use valid test keys generated by our key generation function
487        let sk_alice = bls12_381::generate_sk();
488        let sk_bob = bls12_381::generate_sk();
489
490        let pk_alice = bls12_381::get_public_key(&sk_alice).expect("get pk alice");
491        let pk_bob = bls12_381::get_public_key(&sk_bob).expect("get pk bob");
492
493        // Compute shared secrets (should be symmetric)
494        let shared_secret_alice = bls12_381::get_shared_secret(&pk_bob, &sk_alice).expect("shared secret alice");
495        let shared_secret_bob = bls12_381::get_shared_secret(&pk_alice, &sk_bob).expect("shared secret bob");
496
497        assert_eq!(shared_secret_alice, shared_secret_bob, "Shared secrets should be symmetric");
498
499        // Test message
500        let test_message = b"Hello from Alice to Bob via encrypted message!";
501        let version = Ver::new(1, 1, 8);
502
503        // Alice encrypts a message to Bob
504        let encrypted_messages = Message::encrypt(&pk_alice, &*shared_secret_alice, test_message, version)
505            .expect("encryption should succeed");
506
507        assert_eq!(encrypted_messages.len(), 1, "Should create single message for small payload");
508        let encrypted_msg = &encrypted_messages[0];
509
510        // Verify message structure
511        assert_eq!(encrypted_msg.version, version);
512        assert_eq!(*encrypted_msg.pk, *pk_alice);
513        assert_eq!(encrypted_msg.shard_index, 0);
514        assert_eq!(encrypted_msg.shard_total, 1);
515        // original_size is the encrypted payload size (nonce + tag + ciphertext), not plaintext size
516        assert_eq!(encrypted_msg.original_size, encrypted_msg.payload.len() as u32);
517
518        // Bob decrypts the message
519        let decrypted = encrypted_msg.decrypt(&*shared_secret_bob).expect("decryption should succeed");
520
521        assert_eq!(decrypted, test_message, "Decrypted message should match original");
522
523        // Test serialization/deserialization
524        let serialized = encrypted_msg.to_bytes();
525        let deserialized = Message::try_from(serialized.as_slice()).expect("deserialization should succeed");
526
527        assert_eq!(deserialized.version, encrypted_msg.version);
528        assert_eq!(deserialized.pk, encrypted_msg.pk);
529        assert_eq!(deserialized.shard_index, encrypted_msg.shard_index);
530        assert_eq!(deserialized.shard_total, encrypted_msg.shard_total);
531        assert_eq!(deserialized.ts_nano, encrypted_msg.ts_nano);
532        assert_eq!(deserialized.original_size, encrypted_msg.original_size);
533        assert_eq!(deserialized.payload, encrypted_msg.payload);
534
535        // Bob can still decrypt the deserialized message
536        let decrypted2 =
537            deserialized.decrypt(&*shared_secret_bob).expect("decryption of deserialized message should succeed");
538        assert_eq!(decrypted2, test_message, "Decrypted deserialized message should match original");
539    }
540
541    #[test]
542    fn test_elixir_compatible_64_byte_keys() {
543        // Test with 64-byte secret keys like Elixir generates
544        let sk_64_alice = bls12_381::generate_sk();
545        let sk_64_bob = bls12_381::generate_sk();
546
547        let pk_alice = bls12_381::get_public_key(&sk_64_alice).expect("get pk alice 64");
548        let pk_bob = bls12_381::get_public_key(&sk_64_bob).expect("get pk bob 64");
549
550        let shared_secret_alice = bls12_381::get_shared_secret(&pk_bob, &sk_64_alice).expect("shared secret alice 64");
551        let shared_secret_bob = bls12_381::get_shared_secret(&pk_alice, &sk_64_bob).expect("shared secret bob 64");
552
553        assert_eq!(shared_secret_alice, shared_secret_bob, "64-byte shared secrets should be symmetric");
554
555        let test_message = b"64-byte key compatibility test message";
556        let version = Ver::new(1, 1, 7);
557
558        let encrypted_messages = Message::encrypt(&pk_alice, &*shared_secret_alice, test_message, version)
559            .expect("64-byte key encryption should succeed");
560
561        let decrypted =
562            encrypted_messages[0].decrypt(&*shared_secret_bob).expect("64-byte key decryption should succeed");
563
564        assert_eq!(decrypted, test_message, "64-byte key messages should round-trip correctly");
565    }
566
567    #[tokio::test]
568    async fn test_encrypted_message_reassembler() {
569        let sk_alice = bls12_381::generate_sk();
570        let sk_bob = bls12_381::generate_sk();
571
572        let pk_alice = bls12_381::get_public_key(&sk_alice).expect("get pk alice");
573
574        let shared_secret = bls12_381::get_shared_secret(&pk_alice, &sk_bob).expect("shared secret");
575
576        let test_message = b"Test message for reassembler";
577        let version = Ver::new(1, 1, 8);
578
579        let encrypted_messages =
580            Message::encrypt(&pk_alice, &*shared_secret, test_message, version).expect("encryption should succeed");
581
582        let reassembler = ReedSolomonReassembler::new();
583
584        // For single shard, should work immediately
585        if encrypted_messages.len() == 1 {
586            let serialized = encrypted_messages[0].to_bytes();
587            let result = reassembler.add_shard(&serialized, &sk_bob).await.expect("reassembly should succeed");
588            assert_eq!(result.map(|(msg, _)| msg), Some(test_message.to_vec()));
589        }
590    }
591
592    #[tokio::test]
593    async fn test_build_shards() {
594        use crate::config::Config;
595
596        // Create test config
597        let sk = bls12_381::generate_sk();
598
599        let config = Config::new_daemonless(sk);
600
601        // Create target public key (different from sender)
602        let target_sk = bls12_381::generate_sk();
603        let target_pk = bls12_381::get_public_key(&target_sk).expect("get target pk");
604
605        // Test payload
606        let test_payload = b"Test payload for build_shards functionality";
607
608        // Build shards via instance method
609        let reassembler = ReedSolomonReassembler::new();
610        let shards =
611            reassembler.build_shards(&config, test_payload, &target_pk).await.expect("build_shards should succeed");
612
613        assert!(!shards.is_empty(), "Should create at least one shard");
614
615        // Each shard should be valid Message binary
616        for shard in &shards {
617            assert!(shard.len() > 20, "Shard should be large enough to contain header");
618            assert_eq!(&shard[0..3], b"AMA", "Shard should start with AMA magic");
619        }
620    }
621
622    #[tokio::test]
623    async fn test_build_broadcast_shards() {
624        use crate::config::Config;
625
626        // Create test config
627        let sk = bls12_381::generate_sk();
628
629        let config = Config::new_daemonless(sk);
630
631        // Test payload
632        let test_payload = b"Test payload for broadcast build_shards functionality";
633
634        // Test broadcast shards (using own key as target)
635        let sender_pk = config.get_pk();
636        let reassembler = ReedSolomonReassembler::new();
637        let shards =
638            reassembler.build_shards(&config, test_payload, &sender_pk).await.expect("build_shards should succeed");
639
640        assert!(!shards.is_empty(), "Should create at least one shard");
641
642        // Each shard should be valid Message binary
643        for shard in &shards {
644            assert!(shard.len() > 20, "Shard should be large enough to contain header");
645            assert_eq!(&shard[0..3], b"AMA", "Shard should start with AMA magic");
646        }
647    }
648
649    #[test]
650    fn special_compatibility_test() {
651        let src_pk = [
652            169, 28, 174, 71, 198, 45, 103, 77, 154, 232, 203, 244, 17, 34, 237, 129, 66, 93, 94, 78, 141, 226, 51,
653            166, 153, 186, 221, 114, 128, 18, 56, 100, 37, 178, 123, 55, 51, 197, 165, 109, 247, 71, 136, 163, 211,
654            255, 114, 7,
655        ];
656        let src_sk = [
657            9, 150, 210, 55, 28, 239, 9, 161, 68, 62, 249, 195, 10, 127, 86, 17, 19, 41, 143, 189, 9, 205, 85, 30, 245,
658            51, 80, 235, 135, 77, 62, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
659            0, 0, 0, 0, 0, 0,
660        ];
661        let dst_pk = [
662            169, 61, 121, 32, 15, 191, 174, 241, 143, 231, 124, 53, 186, 69, 28, 212, 233, 130, 22, 18, 34, 244, 13,
663            106, 212, 255, 255, 47, 184, 178, 49, 111, 90, 90, 184, 84, 230, 115, 5, 143, 205, 208, 136, 138, 2, 252,
664            27, 222,
665        ];
666        let dst_sk = [
667            97, 100, 58, 216, 121, 14, 255, 149, 44, 165, 1, 88, 100, 35, 75, 192, 138, 138, 67, 9, 134, 210, 6, 88,
668            155, 3, 21, 197, 119, 155, 33, 163, 103, 4, 46, 229, 62, 157, 185, 90, 19, 106, 206, 72, 245, 133, 133,
669            183, 132, 250, 78, 92, 40, 160, 223, 244, 177, 53, 84, 31, 128, 185, 176, 166,
670        ];
671        let expected_shared_secret = [
672            145, 211, 143, 152, 146, 107, 226, 184, 193, 178, 234, 80, 224, 201, 239, 165, 131, 124, 241, 141, 235,
673            118, 201, 148, 206, 156, 92, 207, 137, 41, 12, 197, 10, 84, 128, 170, 183, 98, 125, 37, 158, 197, 73, 174,
674            140, 4, 177, 64,
675        ];
676        let enc_msg_bin = [
677            65, 77, 65, 1, 1, 8, 0, 169, 28, 174, 71, 198, 45, 103, 77, 154, 232, 203, 244, 17, 34, 237, 129, 66, 93,
678            94, 78, 141, 226, 51, 166, 153, 186, 221, 114, 128, 18, 56, 100, 37, 178, 123, 55, 51, 197, 165, 109, 247,
679            71, 136, 163, 211, 255, 114, 7, 0, 0, 0, 1, 24, 102, 118, 222, 246, 28, 196, 24, 0, 0, 0, 29, 174, 153,
680            105, 150, 110, 19, 115, 132, 10, 128, 192, 116, 95, 183, 109, 90, 36, 47, 94, 235, 25, 153, 6, 60, 1, 52,
681            179, 109, 43, 112, 31, 229, 100, 116, 222, 232, 93, 45, 153, 183, 142, 186, 250, 130, 127, 209, 21, 245,
682            77, 243, 34, 160, 38, 105, 188, 253, 167, 218, 80,
683        ];
684
685        // Test 1: Verify shared secret computation (src sending to dst)
686        let computed_shared_secret =
687            bls12_381::get_shared_secret(&dst_pk, &src_sk).expect("Should compute shared secret from src to dst");
688        assert_eq!(
689            *computed_shared_secret, expected_shared_secret,
690            "Computed shared secret should match expected value"
691        );
692
693        // Test 2: Verify symmetric shared secret (dst receiving from src)
694        let symmetric_shared_secret =
695            bls12_381::get_shared_secret(&src_pk, &dst_sk).expect("Should compute shared secret from dst to src");
696        assert_eq!(
697            *symmetric_shared_secret, expected_shared_secret,
698            "Symmetric shared secret should match expected value"
699        );
700
701        // Test 3: Parse the encrypted message
702        let encrypted_msg =
703            Message::try_from(enc_msg_bin.as_slice()).expect("Should parse encrypted message from binary");
704
705        // Verify message structure matches expected format
706        assert_eq!(encrypted_msg.version, Ver::new(1, 1, 8), "Version should be 1.1.8");
707        assert_eq!(*encrypted_msg.pk, src_pk, "Sender public key should match src_pk");
708        assert_eq!(encrypted_msg.shard_index, 0, "Should be single shard (index 0)");
709        assert_eq!(encrypted_msg.shard_total, 1, "Should be single shard (total 1)");
710        assert_eq!(encrypted_msg.original_size, 29, "Original plaintext size should be 37");
711
712        // Test 4: Decrypt the message using dst's secret key
713        let decrypted = encrypted_msg.decrypt(&*computed_shared_secret).expect("Should decrypt message successfully");
714
715        // Verify decrypted content
716        assert_eq!(decrypted.len(), 29, "Decrypted length should match original_size");
717        assert!(!decrypted.is_empty(), "Decrypted message should not be empty");
718    }
719}