Skip to main content

archimedes_kernel/persistence/
mod.rs

1use std::fs;
2use std::io;
3use std::path::Path;
4
5use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8
9use crate::{Reality, RealitySnapshot};
10
11pub const PERSISTENCE_VERSION: u8 = 2;
12
13const REALITY_DOMAIN: &[u8] = b"ARCHIMEDES-KERNEL-SIGNED-REALITY-V2";
14const SNAPSHOT_DOMAIN: &[u8] = b"ARCHIMEDES-KERNEL-SIGNED-SNAPSHOT-V2";
15
16#[derive(Debug)]
17pub enum PersistenceError {
18    Io(io::Error),
19    Serialization(postcard::Error),
20    IntegrityFailure(&'static str),
21    Signature(&'static str),
22    UnsupportedVersion { expected: u8, found: u8 },
23    PublicKeyMismatch,
24}
25
26impl std::fmt::Display for PersistenceError {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            PersistenceError::Io(e) => write!(f, "IO error: {e}"),
30            PersistenceError::Serialization(e) => {
31                write!(f, "Serialization error: {e}")
32            }
33            PersistenceError::IntegrityFailure(msg) => {
34                write!(f, "Integrity failure: {msg}")
35            }
36            PersistenceError::Signature(msg) => {
37                write!(f, "Signature failure: {msg}")
38            }
39            PersistenceError::UnsupportedVersion { expected, found } => {
40                write!(
41                    f,
42                    "Unsupported persistence version: expected {expected}, found {found}"
43                )
44            }
45            PersistenceError::PublicKeyMismatch => {
46                write!(
47                    f,
48                    "Embedded public key does not match expected authority key"
49                )
50            }
51        }
52    }
53}
54
55impl std::error::Error for PersistenceError {
56    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
57        match self {
58            PersistenceError::Io(e) => Some(e),
59            PersistenceError::Serialization(_)
60            | PersistenceError::IntegrityFailure(_)
61            | PersistenceError::Signature(_)
62            | PersistenceError::UnsupportedVersion { .. }
63            | PersistenceError::PublicKeyMismatch => None,
64        }
65    }
66}
67
68fn write_postcard<T: Serialize>(path: &Path, value: &T) -> Result<(), PersistenceError> {
69    let bytes = postcard::to_allocvec(value).map_err(PersistenceError::Serialization)?;
70    fs::write(path, bytes).map_err(PersistenceError::Io)
71}
72
73fn read_postcard<T: DeserializeOwned>(path: &Path) -> Result<T, PersistenceError> {
74    let bytes = fs::read(path).map_err(PersistenceError::Io)?;
75    postcard::from_bytes(&bytes).map_err(PersistenceError::Serialization)
76}
77
78fn check_version(found: u8) -> Result<(), PersistenceError> {
79    if found != PERSISTENCE_VERSION {
80        return Err(PersistenceError::UnsupportedVersion {
81            expected: PERSISTENCE_VERSION,
82            found,
83        });
84    }
85
86    Ok(())
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90struct PersistedReality {
91    persistence_version: u8,
92    reality: Reality,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
96struct PersistedSnapshot {
97    persistence_version: u8,
98    snapshot: RealitySnapshot,
99}
100
101pub fn save_reality(reality: &Reality, path: &Path) -> Result<(), PersistenceError> {
102    let persisted = PersistedReality {
103        persistence_version: PERSISTENCE_VERSION,
104        reality: reality.clone(),
105    };
106
107    write_postcard(path, &persisted)
108}
109
110pub fn load_reality(path: &Path) -> Result<Reality, PersistenceError> {
111    let persisted: PersistedReality = read_postcard(path)?;
112
113    check_version(persisted.persistence_version)?;
114
115    let reality = persisted.reality;
116
117    if !reality.memory_integrity() {
118        return Err(PersistenceError::IntegrityFailure(
119            "movement memory integrity failed",
120        ));
121    }
122
123    if reality.drift_check().hidden_drift_required {
124        return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
125    }
126
127    Ok(reality)
128}
129
130pub fn save_snapshot(snapshot: &RealitySnapshot, path: &Path) -> Result<(), PersistenceError> {
131    let persisted = PersistedSnapshot {
132        persistence_version: PERSISTENCE_VERSION,
133        snapshot: snapshot.clone(),
134    };
135
136    write_postcard(path, &persisted)
137}
138
139pub fn load_snapshot(path: &Path) -> Result<RealitySnapshot, PersistenceError> {
140    let persisted: PersistedSnapshot = read_postcard(path)?;
141
142    check_version(persisted.persistence_version)?;
143
144    Ok(persisted.snapshot)
145}
146
147#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148pub struct SignedReality {
149    pub protocol_version: u8,
150    pub reality: Reality,
151    pub public_key: [u8; 32],
152    pub signature: Vec<u8>,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156pub struct SignedSnapshot {
157    pub protocol_version: u8,
158    pub snapshot: RealitySnapshot,
159    pub public_key: [u8; 32],
160    pub signature: Vec<u8>,
161}
162
163#[derive(Serialize)]
164struct SignableReality<'a> {
165    domain: &'a [u8],
166    protocol_version: u8,
167    reality: &'a Reality,
168    public_key: [u8; 32],
169}
170
171#[derive(Serialize)]
172struct SignableSnapshot<'a> {
173    domain: &'a [u8],
174    protocol_version: u8,
175    snapshot: &'a RealitySnapshot,
176    public_key: [u8; 32],
177}
178
179fn sign_bytes(message: &[u8], signing_key: &SigningKey) -> ([u8; 32], [u8; 64]) {
180    let signature: Signature = signing_key.sign(message);
181
182    (signing_key.verifying_key().to_bytes(), signature.to_bytes())
183}
184
185fn verify_bytes(
186    message: &[u8],
187    public_key: &[u8; 32],
188    signature: &[u8],
189) -> Result<(), PersistenceError> {
190    let sig_bytes: [u8; 64] = signature
191        .try_into()
192        .map_err(|_| PersistenceError::Signature("invalid signature length"))?;
193
194    let verifying_key = VerifyingKey::from_bytes(public_key)
195        .map_err(|_| PersistenceError::Signature("invalid public key"))?;
196
197    let signature = Signature::from_bytes(&sig_bytes);
198
199    verifying_key
200        .verify_strict(message, &signature)
201        .map_err(|_| PersistenceError::Signature("invalid signature"))
202}
203
204pub fn sign_reality(
205    reality: &Reality,
206    signing_key: &SigningKey,
207) -> Result<SignedReality, PersistenceError> {
208    let public_key = signing_key.verifying_key().to_bytes();
209
210    let signable = SignableReality {
211        domain: REALITY_DOMAIN,
212        protocol_version: PERSISTENCE_VERSION,
213        reality,
214        public_key,
215    };
216
217    let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
218
219    let (_, signature) = sign_bytes(&message, signing_key);
220
221    Ok(SignedReality {
222        protocol_version: PERSISTENCE_VERSION,
223        reality: reality.clone(),
224        public_key,
225        signature: signature.to_vec(),
226    })
227}
228
229pub fn save_signed_reality(signed: &SignedReality, path: &Path) -> Result<(), PersistenceError> {
230    write_postcard(path, signed)
231}
232
233pub fn load_signed_reality(
234    path: &Path,
235    expected_public_key: &[u8; 32],
236) -> Result<SignedReality, PersistenceError> {
237    let signed: SignedReality = read_postcard(path)?;
238
239    check_version(signed.protocol_version)?;
240
241    if signed.public_key != *expected_public_key {
242        return Err(PersistenceError::PublicKeyMismatch);
243    }
244
245    let signable = SignableReality {
246        domain: REALITY_DOMAIN,
247        protocol_version: signed.protocol_version,
248        reality: &signed.reality,
249        public_key: signed.public_key,
250    };
251
252    let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
253
254    verify_bytes(&message, expected_public_key, &signed.signature)?;
255
256    if !signed.reality.memory_integrity() {
257        return Err(PersistenceError::IntegrityFailure(
258            "movement memory integrity failed",
259        ));
260    }
261
262    if signed.reality.drift_check().hidden_drift_required {
263        return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
264    }
265
266    Ok(signed)
267}
268
269pub fn sign_snapshot(
270    snapshot: &RealitySnapshot,
271    signing_key: &SigningKey,
272) -> Result<SignedSnapshot, PersistenceError> {
273    let public_key = signing_key.verifying_key().to_bytes();
274
275    let signable = SignableSnapshot {
276        domain: SNAPSHOT_DOMAIN,
277        protocol_version: PERSISTENCE_VERSION,
278        snapshot,
279        public_key,
280    };
281
282    let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
283
284    let (_, signature) = sign_bytes(&message, signing_key);
285
286    Ok(SignedSnapshot {
287        protocol_version: PERSISTENCE_VERSION,
288        snapshot: snapshot.clone(),
289        public_key,
290        signature: signature.to_vec(),
291    })
292}
293
294pub fn save_signed_snapshot(signed: &SignedSnapshot, path: &Path) -> Result<(), PersistenceError> {
295    write_postcard(path, signed)
296}
297
298pub fn load_signed_snapshot(
299    path: &Path,
300    expected_public_key: &[u8; 32],
301) -> Result<SignedSnapshot, PersistenceError> {
302    let signed: SignedSnapshot = read_postcard(path)?;
303
304    check_version(signed.protocol_version)?;
305
306    if signed.public_key != *expected_public_key {
307        return Err(PersistenceError::PublicKeyMismatch);
308    }
309
310    let signable = SignableSnapshot {
311        domain: SNAPSHOT_DOMAIN,
312        protocol_version: signed.protocol_version,
313        snapshot: &signed.snapshot,
314        public_key: signed.public_key,
315    };
316
317    let message = postcard::to_allocvec(&signable).map_err(PersistenceError::Serialization)?;
318
319    verify_bytes(&message, expected_public_key, &signed.signature)?;
320
321    Ok(signed)
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327    use crate::{perform_movement_sequence, Boundary, Event, Identity, Law, State};
328    use rand::rngs::OsRng;
329
330    fn test_reality() -> Reality {
331        Reality::new(
332            Identity("persist-test".to_string()),
333            Boundary {
334                allowed_values: vec![
335                    "before".to_string(),
336                    "after".to_string(),
337                    "done".to_string(),
338                ],
339            },
340            Law {
341                allowed_transitions: vec![
342                    ("before".to_string(), "after".to_string()),
343                    ("after".to_string(), "done".to_string()),
344                ],
345            },
346            State {
347                field: "before".to_string(),
348            },
349        )
350    }
351
352    fn moved_reality() -> Reality {
353        let mut reality = test_reality();
354
355        let events = vec![
356            Event {
357                proposed_field: "after".to_string(),
358            },
359            Event {
360                proposed_field: "done".to_string(),
361            },
362        ];
363
364        perform_movement_sequence(&mut reality, events).unwrap();
365
366        reality
367    }
368
369    #[test]
370    fn reality_roundtrip_preserves_integrity() {
371        let reality = moved_reality();
372
373        let path = std::env::temp_dir().join("archimedes-v2-reality.bin");
374
375        save_reality(&reality, &path).unwrap();
376
377        let loaded = load_reality(&path).unwrap();
378
379        assert_eq!(reality, loaded);
380        assert!(loaded.memory_integrity());
381        assert!(!loaded.drift_check().hidden_drift_required);
382
383        let _ = std::fs::remove_file(&path);
384    }
385
386    #[test]
387    fn snapshot_roundtrip_matches_original() {
388        let reality = test_reality();
389        let snapshot = reality.snapshot();
390
391        let path = std::env::temp_dir().join("archimedes-v2-snapshot.bin");
392
393        save_snapshot(&snapshot, &path).unwrap();
394
395        let loaded = load_snapshot(&path).unwrap();
396
397        assert_eq!(snapshot, loaded);
398
399        let _ = std::fs::remove_file(&path);
400    }
401
402    #[test]
403    fn tampered_reality_file_is_rejected() {
404        let reality = moved_reality();
405
406        let path = std::env::temp_dir().join("archimedes-v2-tampered.bin");
407
408        save_reality(&reality, &path).unwrap();
409
410        let mut bytes = std::fs::read(&path).unwrap();
411        let mid = bytes.len() / 2;
412        bytes[mid] ^= 0xFF;
413        std::fs::write(&path, &bytes).unwrap();
414
415        assert!(load_reality(&path).is_err());
416
417        let _ = std::fs::remove_file(&path);
418    }
419
420    #[test]
421    fn unsupported_unsigned_version_is_rejected() {
422        let persisted = PersistedReality {
423            persistence_version: 99,
424            reality: test_reality(),
425        };
426
427        let path = std::env::temp_dir().join("archimedes-v2-version.bin");
428
429        write_postcard(&path, &persisted).unwrap();
430
431        let result = load_reality(&path);
432
433        assert!(matches!(
434            result,
435            Err(PersistenceError::UnsupportedVersion { .. })
436        ));
437
438        let _ = std::fs::remove_file(&path);
439    }
440
441    #[test]
442    fn signed_reality_roundtrip_works() {
443        let reality = moved_reality();
444
445        let mut csprng = OsRng;
446        let signing_key = SigningKey::generate(&mut csprng);
447        let public_key = signing_key.verifying_key().to_bytes();
448
449        let signed = sign_reality(&reality, &signing_key).unwrap();
450
451        let path = std::env::temp_dir().join("archimedes-v2-signed-reality.bin");
452
453        save_signed_reality(&signed, &path).unwrap();
454
455        let loaded = load_signed_reality(&path, &public_key).unwrap();
456
457        assert_eq!(loaded.protocol_version, PERSISTENCE_VERSION);
458        assert_eq!(loaded.reality, reality);
459        assert_eq!(loaded.public_key, public_key);
460
461        let _ = std::fs::remove_file(&path);
462    }
463
464    #[test]
465    fn signed_reality_rejects_wrong_key() {
466        let reality = moved_reality();
467
468        let mut csprng = OsRng;
469        let signing_key = SigningKey::generate(&mut csprng);
470        let wrong_key = SigningKey::generate(&mut csprng).verifying_key().to_bytes();
471
472        let signed = sign_reality(&reality, &signing_key).unwrap();
473
474        let path = std::env::temp_dir().join("archimedes-v2-wrong-key.bin");
475
476        save_signed_reality(&signed, &path).unwrap();
477
478        assert!(load_signed_reality(&path, &wrong_key).is_err());
479
480        let _ = std::fs::remove_file(&path);
481    }
482
483    #[test]
484    fn signed_reality_rejects_tampered_embedded_key() {
485        let reality = moved_reality();
486
487        let mut csprng = OsRng;
488        let signing_key = SigningKey::generate(&mut csprng);
489        let public_key = signing_key.verifying_key().to_bytes();
490
491        let mut signed = sign_reality(&reality, &signing_key).unwrap();
492        signed.public_key[0] ^= 0x01;
493
494        let path = std::env::temp_dir().join("archimedes-v2-key-tamper.bin");
495
496        save_signed_reality(&signed, &path).unwrap();
497
498        let result = load_signed_reality(&path, &public_key);
499
500        assert!(matches!(result, Err(PersistenceError::PublicKeyMismatch)));
501
502        let _ = std::fs::remove_file(&path);
503    }
504
505    #[test]
506    fn signed_reality_rejects_tampered_payload() {
507        let reality = moved_reality();
508
509        let mut csprng = OsRng;
510        let signing_key = SigningKey::generate(&mut csprng);
511        let public_key = signing_key.verifying_key().to_bytes();
512
513        let mut signed = sign_reality(&reality, &signing_key).unwrap();
514        signed.reality.state.field = "tampered".to_string();
515
516        let path = std::env::temp_dir().join("archimedes-v2-payload-tamper.bin");
517
518        save_signed_reality(&signed, &path).unwrap();
519
520        let result = load_signed_reality(&path, &public_key);
521
522        assert!(matches!(result, Err(PersistenceError::Signature(_))));
523
524        let _ = std::fs::remove_file(&path);
525    }
526
527    #[test]
528    fn unsupported_signed_version_is_rejected() {
529        let reality = moved_reality();
530
531        let mut csprng = OsRng;
532        let signing_key = SigningKey::generate(&mut csprng);
533        let public_key = signing_key.verifying_key().to_bytes();
534
535        let mut signed = sign_reality(&reality, &signing_key).unwrap();
536        signed.protocol_version = 99;
537
538        let path = std::env::temp_dir().join("archimedes-v2-protocol-version.bin");
539
540        save_signed_reality(&signed, &path).unwrap();
541
542        let result = load_signed_reality(&path, &public_key);
543
544        assert!(matches!(
545            result,
546            Err(PersistenceError::UnsupportedVersion { .. })
547        ));
548
549        let _ = std::fs::remove_file(&path);
550    }
551
552    #[test]
553    fn signed_snapshot_roundtrip_works() {
554        let reality = test_reality();
555        let snapshot = reality.snapshot();
556
557        let mut csprng = OsRng;
558        let signing_key = SigningKey::generate(&mut csprng);
559        let public_key = signing_key.verifying_key().to_bytes();
560
561        let signed = sign_snapshot(&snapshot, &signing_key).unwrap();
562
563        let path = std::env::temp_dir().join("archimedes-v2-signed-snapshot.bin");
564
565        save_signed_snapshot(&signed, &path).unwrap();
566
567        let loaded = load_signed_snapshot(&path, &public_key).unwrap();
568
569        assert_eq!(loaded.protocol_version, PERSISTENCE_VERSION);
570        assert_eq!(loaded.snapshot, snapshot);
571        assert_eq!(loaded.public_key, public_key);
572
573        let _ = std::fs::remove_file(&path);
574    }
575
576    #[test]
577    fn signed_snapshot_rejects_tampered_embedded_key() {
578        let reality = test_reality();
579        let snapshot = reality.snapshot();
580
581        let mut csprng = OsRng;
582        let signing_key = SigningKey::generate(&mut csprng);
583        let public_key = signing_key.verifying_key().to_bytes();
584
585        let mut signed = sign_snapshot(&snapshot, &signing_key).unwrap();
586        signed.public_key[0] ^= 0x01;
587
588        let path = std::env::temp_dir().join("archimedes-v2-snapshot-key-tamper.bin");
589
590        save_signed_snapshot(&signed, &path).unwrap();
591
592        let result = load_signed_snapshot(&path, &public_key);
593
594        assert!(matches!(result, Err(PersistenceError::PublicKeyMismatch)));
595
596        let _ = std::fs::remove_file(&path);
597    }
598
599    #[test]
600    fn signed_snapshot_rejects_tampered_payload() {
601        let reality = test_reality();
602        let snapshot = reality.snapshot();
603
604        let mut csprng = OsRng;
605        let signing_key = SigningKey::generate(&mut csprng);
606        let public_key = signing_key.verifying_key().to_bytes();
607
608        let mut signed = sign_snapshot(&snapshot, &signing_key).unwrap();
609        signed.snapshot.state.field = "tampered".to_string();
610
611        let path = std::env::temp_dir().join("archimedes-v2-snapshot-tamper.bin");
612
613        save_signed_snapshot(&signed, &path).unwrap();
614
615        let result = load_signed_snapshot(&path, &public_key);
616
617        assert!(matches!(result, Err(PersistenceError::Signature(_))));
618
619        let _ = std::fs::remove_file(&path);
620    }
621}