1use std::collections::{HashMap, HashSet};
9use std::fs;
10use std::io::{self, Write};
11use std::path::{Path, PathBuf};
12use std::sync::Mutex;
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use rns_crypto::identity::Identity;
16use rns_crypto::OsRng;
17
18#[derive(Debug, Clone)]
20pub struct StoragePaths {
21 pub config_dir: PathBuf,
22 pub storage: PathBuf,
23 pub cache: PathBuf,
24 pub identities: PathBuf,
25 pub ratchets: PathBuf,
26 pub discovered_interfaces: PathBuf,
28}
29
30#[derive(Debug, Clone)]
32pub struct KnownDestination {
33 pub identity_hash: [u8; 16],
34 pub public_key: [u8; 64],
35 pub app_data: Option<Vec<u8>>,
36 pub hops: u8,
37 pub received_at: f64,
38 pub receiving_interface: u64,
39 pub was_used: bool,
40 pub last_used_at: Option<f64>,
41 pub retained: bool,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub struct RatchetEntry {
46 pub ratchet: [u8; 32],
47 pub received_at: f64,
48}
49
50#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
51pub struct RatchetCleanupStats {
52 pub processed: usize,
53 pub not_known: usize,
54 pub removed: usize,
55}
56
57pub trait RatchetStore: Send + Sync {
58 fn remember(&self, dest_hash: [u8; 16], entry: RatchetEntry) -> io::Result<()>;
59 fn current(
60 &self,
61 dest_hash: &[u8; 16],
62 now: f64,
63 expiry_secs: f64,
64 ) -> io::Result<Option<RatchetEntry>>;
65 fn cleanup(
66 &self,
67 known_destinations: &HashSet<[u8; 16]>,
68 now: f64,
69 expiry_secs: f64,
70 ) -> io::Result<RatchetCleanupStats>;
71}
72
73#[derive(Debug)]
74pub struct FsRatchetStore {
75 dir: PathBuf,
76 cache: Mutex<HashMap<[u8; 16], RatchetEntry>>,
77}
78
79impl FsRatchetStore {
80 pub fn new(dir: PathBuf) -> Self {
81 Self {
82 dir,
83 cache: Mutex::new(HashMap::new()),
84 }
85 }
86
87 fn path_for(&self, dest_hash: &[u8; 16]) -> PathBuf {
88 self.dir.join(hex_lower(dest_hash))
89 }
90
91 fn read_entry(path: &Path) -> io::Result<RatchetEntry> {
92 use rns_core::msgpack;
93
94 let data = fs::read(path)?;
95 let (value, _) = msgpack::unpack(&data).map_err(|e| {
96 io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e))
97 })?;
98 let ratchet = value
99 .map_get("ratchet")
100 .and_then(|v| v.as_bin())
101 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing ratchet"))?;
102 if ratchet.len() != 32 {
103 return Err(io::Error::new(
104 io::ErrorKind::InvalidData,
105 format!("ratchet must be 32 bytes, got {}", ratchet.len()),
106 ));
107 }
108 let mut ratchet_bytes = [0u8; 32];
109 ratchet_bytes.copy_from_slice(ratchet);
110 let received_at = value
111 .map_get("received")
112 .and_then(|v| v.as_number())
113 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "missing received"))?;
114
115 Ok(RatchetEntry {
116 ratchet: ratchet_bytes,
117 received_at,
118 })
119 }
120
121 fn write_entry(&self, path: &Path, entry: RatchetEntry) -> io::Result<()> {
122 use rns_core::msgpack::{self, Value};
123
124 fs::create_dir_all(&self.dir)?;
125 let value = Value::Map(vec![
126 (
127 Value::Str("ratchet".into()),
128 Value::Bin(entry.ratchet.to_vec()),
129 ),
130 (
131 Value::Str("received".into()),
132 Value::Float(entry.received_at),
133 ),
134 ]);
135 let packed = msgpack::pack(&value);
136 let tmp = path.with_extension("out");
137 fs::write(&tmp, packed)?;
138 fs::rename(tmp, path)
139 }
140}
141
142impl RatchetStore for FsRatchetStore {
143 fn remember(&self, dest_hash: [u8; 16], entry: RatchetEntry) -> io::Result<()> {
144 if self
145 .cache
146 .lock()
147 .map(|cache| cache.get(&dest_hash).copied() == Some(entry))
148 .unwrap_or(false)
149 {
150 return Ok(());
151 }
152
153 let path = self.path_for(&dest_hash);
154 self.write_entry(&path, entry)?;
155 if let Ok(mut cache) = self.cache.lock() {
156 cache.insert(dest_hash, entry);
157 }
158 Ok(())
159 }
160
161 fn current(
162 &self,
163 dest_hash: &[u8; 16],
164 now: f64,
165 expiry_secs: f64,
166 ) -> io::Result<Option<RatchetEntry>> {
167 if let Ok(cache) = self.cache.lock() {
168 if let Some(entry) = cache.get(dest_hash).copied() {
169 if now <= entry.received_at + expiry_secs {
170 return Ok(Some(entry));
171 }
172 }
173 }
174
175 let path = self.path_for(dest_hash);
176 if !path.is_file() {
177 return Ok(None);
178 }
179
180 let entry = Self::read_entry(&path)?;
181 if now > entry.received_at + expiry_secs {
182 let _ = fs::remove_file(path);
183 if let Ok(mut cache) = self.cache.lock() {
184 cache.remove(dest_hash);
185 }
186 return Ok(None);
187 }
188
189 if let Ok(mut cache) = self.cache.lock() {
190 cache.insert(*dest_hash, entry);
191 }
192 Ok(Some(entry))
193 }
194
195 fn cleanup(
196 &self,
197 known_destinations: &HashSet<[u8; 16]>,
198 now: f64,
199 expiry_secs: f64,
200 ) -> io::Result<RatchetCleanupStats> {
201 let mut stats = RatchetCleanupStats::default();
202 if !self.dir.is_dir() {
203 return Ok(stats);
204 }
205
206 for entry in fs::read_dir(&self.dir)? {
207 let entry = entry?;
208 if !entry.file_type()?.is_file() {
209 continue;
210 }
211 stats.processed += 1;
212 let path = entry.path();
213 let Some(filename) = path.file_name().and_then(|name| name.to_str()) else {
214 let _ = fs::remove_file(&path);
215 stats.removed += 1;
216 continue;
217 };
218
219 let Some(dest_hash) = parse_dest_hash_hex(filename) else {
220 let _ = fs::remove_file(&path);
221 stats.removed += 1;
222 continue;
223 };
224
225 let unknown = !known_destinations.contains(&dest_hash);
226 if unknown {
227 stats.not_known += 1;
228 }
229
230 let expired_or_corrupt = match Self::read_entry(&path) {
231 Ok(entry) => now > entry.received_at + expiry_secs,
232 Err(_) => true,
233 };
234
235 if unknown || expired_or_corrupt {
236 let _ = fs::remove_file(&path);
237 stats.removed += 1;
238 if let Ok(mut cache) = self.cache.lock() {
239 cache.remove(&dest_hash);
240 }
241 }
242 }
243
244 Ok(stats)
245 }
246}
247
248pub fn ensure_storage_dirs(config_dir: &Path) -> io::Result<StoragePaths> {
250 let storage = config_dir.join("storage");
251 let cache = config_dir.join("cache");
252 let identities = storage.join("identities");
253 let ratchets = storage.join("ratchets");
254 let announces = cache.join("announces");
255 let discovered_interfaces = storage.join("discovery").join("interfaces");
256
257 fs::create_dir_all(&storage)?;
258 fs::create_dir_all(&cache)?;
259 fs::create_dir_all(&identities)?;
260 fs::create_dir_all(&ratchets)?;
261 fs::create_dir_all(&announces)?;
262 fs::create_dir_all(&discovered_interfaces)?;
263
264 Ok(StoragePaths {
265 config_dir: config_dir.to_path_buf(),
266 storage,
267 cache,
268 identities,
269 ratchets,
270 discovered_interfaces,
271 })
272}
273
274fn hex_lower(bytes: &[u8]) -> String {
275 let mut out = String::with_capacity(bytes.len() * 2);
276 for byte in bytes {
277 use std::fmt::Write;
278 let _ = write!(&mut out, "{:02x}", byte);
279 }
280 out
281}
282
283fn parse_dest_hash_hex(s: &str) -> Option<[u8; 16]> {
284 if s.len() != 32 {
285 return None;
286 }
287 let mut out = [0u8; 16];
288 for i in 0..16 {
289 out[i] = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()?;
290 }
291 Some(out)
292}
293
294pub fn save_identity(identity: &Identity, path: &Path) -> io::Result<()> {
296 let private_key = identity
297 .get_private_key()
298 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Identity has no private key"))?;
299 let mut options = fs::OpenOptions::new();
300 options.write(true).create(true).truncate(true);
301 #[cfg(unix)]
302 {
303 use std::os::unix::fs::OpenOptionsExt;
304 options.mode(0o600);
305 }
306 options.open(path)?.write_all(&private_key)
307}
308
309pub fn load_identity(path: &Path) -> io::Result<Identity> {
311 let data = fs::read(path)?;
312 if data.len() != 64 {
313 return Err(io::Error::new(
314 io::ErrorKind::InvalidData,
315 format!("Identity file must be 64 bytes, got {}", data.len()),
316 ));
317 }
318 let mut key = [0u8; 64];
319 key.copy_from_slice(&data);
320 Ok(Identity::from_private_key(&key))
321}
322
323pub fn save_known_destinations(
330 destinations: &HashMap<[u8; 16], KnownDestination>,
331 path: &Path,
332) -> io::Result<()> {
333 use rns_core::msgpack::{self, Value};
334
335 let entries: Vec<(Value, Value)> = destinations
336 .iter()
337 .map(|(hash, dest)| {
338 let key = Value::Bin(hash.to_vec());
339 let app_data = match &dest.app_data {
340 Some(d) => Value::Bin(d.clone()),
341 None => Value::Nil,
342 };
343 let value = Value::Array(vec![
344 Value::UInt(dest.received_at as u64),
345 Value::Bin(dest.public_key.to_vec()),
346 app_data,
347 Value::Bin(dest.identity_hash.to_vec()),
348 Value::UInt(dest.hops as u64),
349 Value::UInt(dest.receiving_interface),
350 Value::Bool(dest.was_used),
351 match dest.last_used_at {
352 Some(last_used_at) => Value::UInt(last_used_at as u64),
353 None => Value::Nil,
354 },
355 Value::Bool(dest.retained),
356 ]);
357 (key, value)
358 })
359 .collect();
360
361 let packed = msgpack::pack(&Value::Map(entries));
362 atomic_write(path, &packed)
363}
364
365fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
366 let parent = path.parent().unwrap_or_else(|| Path::new("."));
367 let file_name = path
368 .file_name()
369 .and_then(|name| name.to_str())
370 .unwrap_or("known_destinations");
371 let nonce = SystemTime::now()
372 .duration_since(UNIX_EPOCH)
373 .map(|duration| duration.as_nanos())
374 .unwrap_or_default();
375 let temp_path = parent.join(format!(".{file_name}.tmp.{}.{}", std::process::id(), nonce));
376 match fs::write(&temp_path, data).and_then(|_| replace_file(&temp_path, path)) {
377 Ok(()) => Ok(()),
378 Err(err) => {
379 let _ = fs::remove_file(&temp_path);
380 Err(err)
381 }
382 }
383}
384
385fn replace_file(temp_path: &Path, path: &Path) -> io::Result<()> {
386 #[cfg(windows)]
387 if path.exists() {
388 fs::remove_file(path)?;
389 }
390 fs::rename(temp_path, path)
391}
392
393pub fn load_known_destinations(path: &Path) -> io::Result<HashMap<[u8; 16], KnownDestination>> {
395 use rns_core::msgpack;
396
397 let data = fs::read(path)?;
398 if data.is_empty() {
399 return Ok(HashMap::new());
400 }
401
402 let (value, _) = msgpack::unpack(&data)
403 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("msgpack error: {}", e)))?;
404
405 let map = value
406 .as_map()
407 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected msgpack map"))?;
408
409 let mut result = HashMap::new();
410
411 for (k, v) in map {
412 let hash_bytes = k
413 .as_bin()
414 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin key"))?;
415
416 if hash_bytes.len() != 16 {
417 continue; }
419
420 let mut dest_hash = [0u8; 16];
421 dest_hash.copy_from_slice(hash_bytes);
422
423 let arr = v
424 .as_array()
425 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected array value"))?;
426
427 if arr.len() < 3 {
428 continue;
429 }
430
431 let received_at = arr[0].as_uint().unwrap_or(0) as f64;
432
433 let pub_key_bytes = arr[1]
434 .as_bin()
435 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Expected bin public_key"))?;
436 if pub_key_bytes.len() != 64 {
437 continue;
438 }
439 let mut public_key = [0u8; 64];
440 public_key.copy_from_slice(pub_key_bytes);
441
442 let app_data = if arr.len() > 2 {
443 arr[2].as_bin().map(|b| b.to_vec())
444 } else {
445 None
446 };
447
448 let identity_hash = if arr.len() > 3 {
449 let hash_bytes = arr[3]
450 .as_bin()
451 .filter(|bytes| bytes.len() == 16)
452 .map(|bytes| {
453 let mut hash = [0u8; 16];
454 hash.copy_from_slice(bytes);
455 hash
456 });
457 hash_bytes.unwrap_or_else(|| rns_core::hash::truncated_hash(&public_key))
458 } else {
459 rns_core::hash::truncated_hash(&public_key)
460 };
461 let hops = arr.get(4).and_then(|value| value.as_uint()).unwrap_or(0) as u8;
462 let receiving_interface = arr.get(5).and_then(|value| value.as_uint()).unwrap_or(0);
463 let was_used = arr
464 .get(6)
465 .and_then(|value| value.as_bool())
466 .unwrap_or(false);
467 let last_used_at = arr
468 .get(7)
469 .and_then(|value| value.as_uint())
470 .map(|value| value as f64);
471 let retained = arr
472 .get(8)
473 .and_then(|value| value.as_bool())
474 .unwrap_or(false);
475
476 result.insert(
477 dest_hash,
478 KnownDestination {
479 identity_hash,
480 public_key,
481 app_data,
482 hops,
483 received_at,
484 receiving_interface,
485 was_used,
486 last_used_at,
487 retained,
488 },
489 );
490 }
491
492 Ok(result)
493}
494
495pub fn save_transport_state(
498 snapshot: &rns_core::transport::persistence::TransportStateSnapshot,
499 storage_dir: &Path,
500 low_priority: bool,
501) -> io::Result<()> {
502 let mut round_started = std::time::Instant::now();
503 save_transport_state_with_checkpoint(snapshot, storage_dir, || {
504 if low_priority && round_started.elapsed() > std::time::Duration::from_millis(10) {
505 std::thread::sleep(std::time::Duration::from_millis(1));
506 round_started = std::time::Instant::now();
507 }
508 })
509}
510
511fn save_transport_state_with_checkpoint<F>(
512 snapshot: &rns_core::transport::persistence::TransportStateSnapshot,
513 storage_dir: &Path,
514 mut checkpoint: F,
515) -> io::Result<()>
516where
517 F: FnMut(),
518{
519 use rns_core::msgpack::{self, Value};
520
521 fs::create_dir_all(storage_dir)?;
522
523 let mut hashes = Vec::with_capacity(snapshot.packet_hashes.len() * 32);
524 for hash in &snapshot.packet_hashes {
525 checkpoint();
526 hashes.extend_from_slice(hash);
527 }
528 atomic_write(&storage_dir.join("packet_hashlist.raw"), &hashes)?;
529
530 let paths = snapshot
531 .paths
532 .iter()
533 .map(|path| {
534 checkpoint();
535 encode_persisted_path(path)
536 })
537 .collect();
538 atomic_write(
539 &storage_dir.join("destination_table"),
540 &msgpack::pack(&Value::Array(paths)),
541 )?;
542
543 let tunnels = snapshot
544 .tunnels
545 .iter()
546 .map(|tunnel| {
547 checkpoint();
548 let paths = tunnel
549 .paths
550 .iter()
551 .map(|path| {
552 checkpoint();
553 encode_persisted_path(path)
554 })
555 .collect();
556 Value::Array(vec![
557 Value::Bin(tunnel.tunnel_id.to_vec()),
558 optional_hash_value(tunnel.interface_hash),
559 Value::Array(paths),
560 Value::Float(tunnel.expires),
561 ])
562 })
563 .collect();
564 atomic_write(
565 &storage_dir.join("tunnels"),
566 &msgpack::pack(&Value::Array(tunnels)),
567 )
568}
569
570fn encode_persisted_path(
571 path: &rns_core::transport::persistence::PersistedPath,
572) -> rns_core::msgpack::Value {
573 use rns_core::msgpack::Value;
574
575 Value::Array(vec![
576 Value::Bin(path.destination_hash.to_vec()),
577 Value::Float(path.timestamp),
578 Value::Bin(path.received_from.to_vec()),
579 Value::UInt(path.hops as u64),
580 Value::Float(path.expires),
581 Value::Array(
582 path.random_blobs
583 .iter()
584 .map(|blob| Value::Bin(blob.to_vec()))
585 .collect(),
586 ),
587 optional_hash_value(path.interface_hash),
588 Value::Bin(path.packet_hash.to_vec()),
589 ])
590}
591
592fn optional_hash_value(hash: Option<[u8; 32]>) -> rns_core::msgpack::Value {
593 match hash {
594 Some(hash) => rns_core::msgpack::Value::Bin(hash.to_vec()),
595 None => rns_core::msgpack::Value::Nil,
596 }
597}
598
599pub fn load_transport_state(
602 storage_dir: &Path,
603) -> io::Result<rns_core::transport::persistence::TransportStateSnapshot> {
604 use rns_core::transport::persistence::TransportStateSnapshot;
605
606 let packet_hashes = load_packet_hashlist(&storage_dir.join("packet_hashlist.raw"))?;
607 let paths = load_path_table(&storage_dir.join("destination_table"))?;
608 let tunnels = load_tunnel_table(&storage_dir.join("tunnels"))?;
609 Ok(TransportStateSnapshot {
610 packet_hashes,
611 paths,
612 tunnels,
613 })
614}
615
616fn load_packet_hashlist(path: &Path) -> io::Result<Vec<[u8; 32]>> {
617 let data = match fs::read(path) {
618 Ok(data) => data,
619 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
620 Err(err) => return Err(err),
621 };
622 Ok(data
623 .chunks_exact(32)
624 .map(|chunk| {
625 let mut hash = [0u8; 32];
626 hash.copy_from_slice(chunk);
627 hash
628 })
629 .collect())
630}
631
632fn load_path_table(
633 path: &Path,
634) -> io::Result<Vec<rns_core::transport::persistence::PersistedPath>> {
635 let Some(value) = load_optional_msgpack(path)? else {
636 return Ok(Vec::new());
637 };
638 value
639 .as_array()
640 .ok_or_else(|| invalid_transport_data("destination table must be an array"))?
641 .iter()
642 .map(decode_persisted_path)
643 .collect()
644}
645
646fn load_tunnel_table(
647 path: &Path,
648) -> io::Result<Vec<rns_core::transport::persistence::PersistedTunnel>> {
649 use rns_core::transport::persistence::PersistedTunnel;
650
651 let Some(value) = load_optional_msgpack(path)? else {
652 return Ok(Vec::new());
653 };
654 value
655 .as_array()
656 .ok_or_else(|| invalid_transport_data("tunnel table must be an array"))?
657 .iter()
658 .map(|value| {
659 let fields = value
660 .as_array()
661 .filter(|fields| fields.len() == 4)
662 .ok_or_else(|| invalid_transport_data("tunnel entry must contain 4 fields"))?;
663 let tunnel_id = fixed_bin::<32>(&fields[0], "tunnel id")?;
664 let interface_hash = optional_fixed_bin::<32>(&fields[1], "tunnel interface hash")?;
665 let paths = fields[2]
666 .as_array()
667 .ok_or_else(|| invalid_transport_data("tunnel paths must be an array"))?
668 .iter()
669 .map(decode_persisted_path)
670 .collect::<io::Result<Vec<_>>>()?;
671 let expires = fields[3]
672 .as_number()
673 .ok_or_else(|| invalid_transport_data("tunnel expiry must be numeric"))?;
674 Ok(PersistedTunnel {
675 tunnel_id,
676 interface_hash,
677 paths,
678 expires,
679 })
680 })
681 .collect()
682}
683
684fn decode_persisted_path(
685 value: &rns_core::msgpack::Value,
686) -> io::Result<rns_core::transport::persistence::PersistedPath> {
687 use rns_core::transport::persistence::PersistedPath;
688
689 let fields = value
690 .as_array()
691 .filter(|fields| fields.len() == 8)
692 .ok_or_else(|| invalid_transport_data("path entry must contain 8 fields"))?;
693 let random_blobs = fields[5]
694 .as_array()
695 .ok_or_else(|| invalid_transport_data("random blobs must be an array"))?
696 .iter()
697 .map(|value| fixed_bin::<10>(value, "random blob"))
698 .collect::<io::Result<Vec<_>>>()?;
699 let hops = fields[3]
700 .as_uint()
701 .and_then(|value| u8::try_from(value).ok())
702 .ok_or_else(|| invalid_transport_data("hop count must fit in a byte"))?;
703 Ok(PersistedPath {
704 destination_hash: fixed_bin::<16>(&fields[0], "destination hash")?,
705 timestamp: fields[1]
706 .as_number()
707 .ok_or_else(|| invalid_transport_data("path timestamp must be numeric"))?,
708 received_from: fixed_bin::<16>(&fields[2], "received-from hash")?,
709 hops,
710 expires: fields[4]
711 .as_number()
712 .ok_or_else(|| invalid_transport_data("path expiry must be numeric"))?,
713 random_blobs,
714 interface_hash: optional_fixed_bin::<32>(&fields[6], "interface hash")?,
715 packet_hash: fixed_bin::<32>(&fields[7], "packet hash")?,
716 })
717}
718
719fn load_optional_msgpack(path: &Path) -> io::Result<Option<rns_core::msgpack::Value>> {
720 let data = match fs::read(path) {
721 Ok(data) => data,
722 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
723 Err(err) => return Err(err),
724 };
725 rns_core::msgpack::unpack_exact(&data)
726 .map(Some)
727 .map_err(|err| invalid_transport_data(&format!("msgpack error: {err}")))
728}
729
730fn fixed_bin<const N: usize>(value: &rns_core::msgpack::Value, field: &str) -> io::Result<[u8; N]> {
731 let bytes = value
732 .as_bin()
733 .filter(|bytes| bytes.len() == N)
734 .ok_or_else(|| invalid_transport_data(&format!("{field} must be {N} bytes")))?;
735 let mut result = [0u8; N];
736 result.copy_from_slice(bytes);
737 Ok(result)
738}
739
740fn optional_fixed_bin<const N: usize>(
741 value: &rns_core::msgpack::Value,
742 field: &str,
743) -> io::Result<Option<[u8; N]>> {
744 if matches!(value, rns_core::msgpack::Value::Nil) {
745 Ok(None)
746 } else {
747 fixed_bin(value, field).map(Some)
748 }
749}
750
751fn invalid_transport_data(message: &str) -> io::Error {
752 io::Error::new(io::ErrorKind::InvalidData, message)
753}
754
755pub fn resolve_config_dir(explicit: Option<&Path>) -> PathBuf {
758 if let Some(p) = explicit {
759 p.to_path_buf()
760 } else {
761 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".into());
762 PathBuf::from(home).join(".reticulum")
763 }
764}
765
766pub fn load_or_create_identity(identities_dir: &Path) -> io::Result<Identity> {
768 let id_path = identities_dir.join("identity");
769 if id_path.exists() {
770 load_identity(&id_path)
771 } else {
772 let identity = Identity::new(&mut OsRng);
773 save_identity(&identity, &id_path)?;
774 Ok(identity)
775 }
776}
777
778pub fn transport_identity_path(paths: &StoragePaths) -> PathBuf {
782 paths.storage.join("transport_identity")
783}
784
785pub fn load_rpc_identity(paths: &StoragePaths) -> io::Result<Identity> {
792 let path = transport_identity_path(paths);
793 if path.exists() {
794 load_identity(&path)
795 } else {
796 load_or_create_identity(&paths.identities)
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 use std::sync::atomic::{AtomicU64, Ordering};
805 static TEST_COUNTER: AtomicU64 = AtomicU64::new(0);
806
807 fn temp_dir() -> PathBuf {
808 let id = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
809 let dir = std::env::temp_dir().join(format!("rns-test-{}-{}", std::process::id(), id));
810 let _ = fs::remove_dir_all(&dir);
811 fs::create_dir_all(&dir).unwrap();
812 dir
813 }
814
815 #[test]
816 fn save_load_identity_roundtrip() {
817 let dir = temp_dir();
818 let path = dir.join("test_identity");
819
820 let identity = Identity::new(&mut OsRng);
821 let original_hash = *identity.hash();
822
823 save_identity(&identity, &path).unwrap();
824 #[cfg(unix)]
825 {
826 use std::os::unix::fs::PermissionsExt;
827 assert_eq!(
828 fs::metadata(&path).unwrap().permissions().mode() & 0o777,
829 0o600
830 );
831 }
832 let loaded = load_identity(&path).unwrap();
833
834 assert_eq!(*loaded.hash(), original_hash);
835
836 let _ = fs::remove_dir_all(&dir);
837 }
838
839 #[test]
840 fn identity_file_format() {
841 let dir = temp_dir();
842 let path = dir.join("test_identity_fmt");
843
844 let identity = Identity::new(&mut OsRng);
845 save_identity(&identity, &path).unwrap();
846
847 let data = fs::read(&path).unwrap();
848 assert_eq!(data.len(), 64, "Identity file must be exactly 64 bytes");
849
850 let private_key = identity.get_private_key();
853 let private_key = private_key.unwrap();
854 assert_eq!(&data[..], &private_key[..]);
855
856 let _ = fs::remove_dir_all(&dir);
857 }
858
859 #[test]
860 fn save_load_known_destinations_empty() {
861 let dir = temp_dir();
862 let path = dir.join("known_destinations");
863
864 let empty: HashMap<[u8; 16], KnownDestination> = HashMap::new();
865 save_known_destinations(&empty, &path).unwrap();
866
867 let loaded = load_known_destinations(&path).unwrap();
868 assert!(loaded.is_empty());
869
870 let _ = fs::remove_dir_all(&dir);
871 }
872
873 #[test]
874 fn save_load_known_destinations_roundtrip() {
875 let dir = temp_dir();
876 let path = dir.join("known_destinations");
877
878 let mut dests = HashMap::new();
879 dests.insert(
880 [0x01u8; 16],
881 KnownDestination {
882 identity_hash: [0x11u8; 16],
883 public_key: [0xABu8; 64],
884 app_data: Some(vec![0x01, 0x02, 0x03]),
885 hops: 2,
886 received_at: 1700000000.0,
887 receiving_interface: 7,
888 was_used: true,
889 last_used_at: Some(1700000010.0),
890 retained: true,
891 },
892 );
893 dests.insert(
894 [0x02u8; 16],
895 KnownDestination {
896 identity_hash: [0x22u8; 16],
897 public_key: [0xCDu8; 64],
898 app_data: None,
899 hops: 1,
900 received_at: 1700000001.0,
901 receiving_interface: 0,
902 was_used: false,
903 last_used_at: None,
904 retained: false,
905 },
906 );
907
908 save_known_destinations(&dests, &path).unwrap();
909 let loaded = load_known_destinations(&path).unwrap();
910
911 assert_eq!(loaded.len(), 2);
912
913 let d1 = &loaded[&[0x01u8; 16]];
914 assert_eq!(d1.identity_hash, [0x11u8; 16]);
915 assert_eq!(d1.public_key, [0xABu8; 64]);
916 assert_eq!(d1.app_data, Some(vec![0x01, 0x02, 0x03]));
917 assert_eq!(d1.hops, 2);
918 assert_eq!(d1.received_at as u64, 1700000000);
919 assert_eq!(d1.receiving_interface, 7);
920 assert!(d1.was_used);
921 assert_eq!(d1.last_used_at, Some(1700000010.0));
922 assert!(d1.retained);
923
924 let d2 = &loaded[&[0x02u8; 16]];
925 assert_eq!(d2.app_data, None);
926 assert!(!d2.was_used);
927 assert_eq!(d2.last_used_at, None);
928 assert!(!d2.retained);
929
930 let _ = fs::remove_dir_all(&dir);
931 }
932
933 #[test]
934 fn save_known_destinations_replaces_existing_file_atomically() {
935 let dir = temp_dir();
936 let path = dir.join("known_destinations");
937 fs::write(&path, b"old").unwrap();
938
939 let mut dests = HashMap::new();
940 dests.insert(
941 [0x03u8; 16],
942 KnownDestination {
943 identity_hash: [0x33u8; 16],
944 public_key: [0xEFu8; 64],
945 app_data: None,
946 hops: 3,
947 received_at: 1700000002.0,
948 receiving_interface: 9,
949 was_used: false,
950 last_used_at: None,
951 retained: false,
952 },
953 );
954
955 save_known_destinations(&dests, &path).unwrap();
956
957 let loaded = load_known_destinations(&path).unwrap();
958 assert_eq!(loaded.len(), 1);
959 assert!(loaded.contains_key(&[0x03u8; 16]));
960 let leftover_temp = fs::read_dir(&dir)
961 .unwrap()
962 .filter_map(Result::ok)
963 .any(|entry| entry.file_name().to_string_lossy().contains(".tmp."));
964 assert!(!leftover_temp);
965
966 let _ = fs::remove_dir_all(&dir);
967 }
968
969 #[test]
970 fn ratchet_store_roundtrip() {
971 let dir = temp_dir();
972 let store = FsRatchetStore::new(dir.join("ratchets"));
973 let dest = [0xAA; 16];
974 let entry = RatchetEntry {
975 ratchet: [0xBB; 32],
976 received_at: 1700000000.25,
977 };
978
979 store.remember(dest, entry).unwrap();
980 let loaded = store.current(&dest, 1700000001.0, 60.0).unwrap();
981
982 assert_eq!(loaded, Some(entry));
983 assert!(dir.join("ratchets").join(hex_lower(&dest)).exists());
984
985 let _ = fs::remove_dir_all(&dir);
986 }
987
988 #[test]
989 fn ratchet_cleanup_removes_expired_corrupt_unknown_and_temp() {
990 let dir = temp_dir();
991 let ratchets = dir.join("ratchets");
992 fs::create_dir_all(&ratchets).unwrap();
993 let store = FsRatchetStore::new(ratchets.clone());
994
995 let known_live = [0x01; 16];
996 let known_expired = [0x02; 16];
997 let unknown = [0x03; 16];
998 store
999 .remember(
1000 known_live,
1001 RatchetEntry {
1002 ratchet: [0x11; 32],
1003 received_at: 1000.0,
1004 },
1005 )
1006 .unwrap();
1007 store
1008 .remember(
1009 known_expired,
1010 RatchetEntry {
1011 ratchet: [0x22; 32],
1012 received_at: 100.0,
1013 },
1014 )
1015 .unwrap();
1016 store
1017 .remember(
1018 unknown,
1019 RatchetEntry {
1020 ratchet: [0x33; 32],
1021 received_at: 1000.0,
1022 },
1023 )
1024 .unwrap();
1025 fs::write(ratchets.join(hex_lower(&[0x04; 16])), b"not msgpack").unwrap();
1026 fs::write(ratchets.join("0102.out"), b"temp").unwrap();
1027
1028 let known = HashSet::from([known_live, known_expired, [0x04; 16]]);
1029 let stats = store.cleanup(&known, 1000.0, 300.0).unwrap();
1030
1031 assert_eq!(stats.processed, 5);
1032 assert_eq!(stats.not_known, 1);
1033 assert_eq!(stats.removed, 4);
1034 assert!(ratchets.join(hex_lower(&known_live)).exists());
1035 assert!(!ratchets.join(hex_lower(&known_expired)).exists());
1036 assert!(!ratchets.join(hex_lower(&unknown)).exists());
1037 assert!(!ratchets.join(hex_lower(&[0x04; 16])).exists());
1038 assert!(!ratchets.join("0102.out").exists());
1039
1040 let _ = fs::remove_dir_all(&dir);
1041 }
1042
1043 #[test]
1044 fn ensure_dirs_creates() {
1045 let dir = temp_dir().join("new_config");
1046 let _ = fs::remove_dir_all(&dir);
1047
1048 let paths = ensure_storage_dirs(&dir).unwrap();
1049
1050 assert!(paths.storage.exists());
1051 assert!(paths.cache.exists());
1052 assert!(paths.identities.exists());
1053 assert!(paths.ratchets.exists());
1054 assert!(paths.discovered_interfaces.exists());
1055
1056 let _ = fs::remove_dir_all(&dir);
1057 }
1058
1059 #[test]
1060 fn ensure_dirs_existing() {
1061 let dir = temp_dir().join("existing_config");
1062 fs::create_dir_all(dir.join("storage")).unwrap();
1063 fs::create_dir_all(dir.join("cache")).unwrap();
1064
1065 let paths = ensure_storage_dirs(&dir).unwrap();
1066 assert!(paths.storage.exists());
1067 assert!(paths.identities.exists());
1068
1069 let _ = fs::remove_dir_all(&dir);
1070 }
1071
1072 #[test]
1073 fn load_or_create_identity_new() {
1074 let dir = temp_dir().join("load_or_create");
1075 fs::create_dir_all(&dir).unwrap();
1076
1077 let identity = load_or_create_identity(&dir).unwrap();
1078 let id_path = dir.join("identity");
1079 assert!(id_path.exists());
1080
1081 let loaded = load_or_create_identity(&dir).unwrap();
1083 assert_eq!(*identity.hash(), *loaded.hash());
1084
1085 let _ = fs::remove_dir_all(&dir);
1086 }
1087
1088 fn transport_snapshot() -> rns_core::transport::persistence::TransportStateSnapshot {
1089 use rns_core::transport::persistence::{
1090 PersistedPath, PersistedTunnel, TransportStateSnapshot,
1091 };
1092
1093 let path = PersistedPath {
1094 destination_hash: [0x11; 16],
1095 timestamp: 10.25,
1096 received_from: [0x22; 16],
1097 hops: 3,
1098 expires: 20.5,
1099 random_blobs: vec![[0x33; 10], [0x44; 10]],
1100 interface_hash: Some([0x55; 32]),
1101 packet_hash: [0x66; 32],
1102 };
1103 TransportStateSnapshot {
1104 packet_hashes: vec![[1; 32], [2; 32]],
1105 paths: vec![path.clone()],
1106 tunnels: vec![PersistedTunnel {
1107 tunnel_id: [0x77; 32],
1108 interface_hash: None,
1109 paths: vec![PersistedPath {
1110 destination_hash: [0x88; 16],
1111 interface_hash: None,
1112 ..path
1113 }],
1114 expires: 30.75,
1115 }],
1116 }
1117 }
1118
1119 #[test]
1120 fn packet_hashlist_raw_is_fixed_width_fifo_and_ignores_partial_tail() {
1121 let dir = temp_dir();
1122 let snapshot = transport_snapshot();
1123 save_transport_state(&snapshot, &dir, false).unwrap();
1124
1125 let path = dir.join("packet_hashlist.raw");
1126 let mut raw = fs::read(&path).unwrap();
1127 assert_eq!(raw.len(), 64);
1128 assert_eq!(&raw[..32], &[1; 32]);
1129 assert_eq!(&raw[32..], &[2; 32]);
1130 raw.extend_from_slice(&[0xFF; 17]);
1131 fs::write(&path, raw).unwrap();
1132
1133 assert_eq!(load_packet_hashlist(&path).unwrap(), vec![[1; 32], [2; 32]]);
1134 let _ = fs::remove_dir_all(&dir);
1135 }
1136
1137 #[test]
1138 fn path_and_tunnel_tables_round_trip_python_compatible_fields() {
1139 let dir = temp_dir();
1140 let expected = transport_snapshot();
1141 save_transport_state(&expected, &dir, false).unwrap();
1142 let loaded = load_transport_state(&dir).unwrap();
1143 assert_eq!(loaded, expected);
1144
1145 let packed = fs::read(dir.join("destination_table")).unwrap();
1146 let value = rns_core::msgpack::unpack_exact(&packed).unwrap();
1147 let entry = &value.as_array().unwrap()[0];
1148 assert_eq!(entry.as_array().unwrap().len(), 8);
1149 assert_eq!(entry.as_array().unwrap()[0].as_bin().unwrap().len(), 16);
1150 assert_eq!(entry.as_array().unwrap()[6].as_bin().unwrap().len(), 32);
1151
1152 let packed = fs::read(dir.join("tunnels")).unwrap();
1153 let value = rns_core::msgpack::unpack_exact(&packed).unwrap();
1154 let tunnel = &value.as_array().unwrap()[0];
1155 assert_eq!(tunnel.as_array().unwrap().len(), 4);
1156 assert!(matches!(
1157 tunnel.as_array().unwrap()[1],
1158 rns_core::msgpack::Value::Nil
1159 ));
1160 let _ = fs::remove_dir_all(&dir);
1161 }
1162
1163 #[test]
1164 fn missing_transport_state_files_load_as_empty_tables() {
1165 let dir = temp_dir();
1166 assert_eq!(
1167 load_transport_state(&dir).unwrap(),
1168 rns_core::transport::persistence::TransportStateSnapshot::default()
1169 );
1170 let _ = fs::remove_dir_all(&dir);
1171 }
1172
1173 #[test]
1174 fn low_priority_save_checkpoints_each_collection_without_temp_leaks() {
1175 let dir = temp_dir();
1176 let snapshot = transport_snapshot();
1177 let mut checkpoints = 0;
1178 save_transport_state_with_checkpoint(&snapshot, &dir, || checkpoints += 1).unwrap();
1179
1180 assert_eq!(checkpoints, 5);
1182 assert!(!fs::read_dir(&dir)
1183 .unwrap()
1184 .filter_map(Result::ok)
1185 .any(|entry| entry.file_name().to_string_lossy().contains(".tmp.")));
1186 let _ = fs::remove_dir_all(&dir);
1187 }
1188
1189 #[test]
1190 fn malformed_transport_table_reports_invalid_data_without_panicking() {
1191 let dir = temp_dir();
1192 fs::write(dir.join("destination_table"), b"not-msgpack").unwrap();
1193 let error = load_transport_state(&dir).unwrap_err();
1194 assert_eq!(error.kind(), io::ErrorKind::InvalidData);
1195 let _ = fs::remove_dir_all(&dir);
1196 }
1197
1198 #[test]
1199 fn load_rpc_identity_prefers_transport_identity() {
1200 let dir = temp_dir();
1201 let paths = ensure_storage_dirs(&dir).unwrap();
1202
1203 let node = load_rpc_identity(&paths).unwrap();
1205 assert_eq!(
1206 *load_identity(&paths.identities.join("identity"))
1207 .unwrap()
1208 .hash(),
1209 *node.hash()
1210 );
1211
1212 let transport = Identity::new(&mut OsRng);
1214 save_identity(&transport, &transport_identity_path(&paths)).unwrap();
1215 let rpc = load_rpc_identity(&paths).unwrap();
1216 assert_eq!(*rpc.hash(), *transport.hash());
1217 assert_ne!(*rpc.hash(), *node.hash());
1218
1219 let _ = fs::remove_dir_all(&dir);
1220 }
1221}