archimedes_kernel/persistence/
mod.rs1use 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
11#[derive(Debug)]
12pub enum PersistenceError {
13 Io(io::Error),
14 Serialization(bincode::Error),
15 IntegrityFailure(&'static str),
16 Signature(&'static str),
17}
18
19impl std::fmt::Display for PersistenceError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 PersistenceError::Io(e) => write!(f, "IO error: {}", e),
23 PersistenceError::Serialization(e) => write!(f, "Serialization error: {}", e),
24 PersistenceError::IntegrityFailure(msg) => write!(f, "Integrity failure: {}", msg),
25 PersistenceError::Signature(msg) => write!(f, "Signature failure: {}", msg),
26 }
27 }
28}
29
30impl std::error::Error for PersistenceError {
31 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
32 match self {
33 PersistenceError::Io(e) => Some(e),
34 PersistenceError::Serialization(e) => Some(e),
35 PersistenceError::IntegrityFailure(_) => None,
36 PersistenceError::Signature(_) => None,
37 }
38 }
39}
40
41fn write_bincode<T: Serialize>(path: &Path, value: &T) -> Result<(), PersistenceError> {
42 let bytes = bincode::serialize(value).map_err(PersistenceError::Serialization)?;
43 fs::write(path, bytes).map_err(PersistenceError::Io)
44}
45
46fn read_bincode<T: DeserializeOwned>(path: &Path) -> Result<T, PersistenceError> {
47 let bytes = fs::read(path).map_err(PersistenceError::Io)?;
48 bincode::deserialize(&bytes).map_err(PersistenceError::Serialization)
49}
50
51pub fn save_reality(reality: &Reality, path: &Path) -> Result<(), PersistenceError> {
52 write_bincode(path, reality)
53}
54
55pub fn load_reality(path: &Path) -> Result<Reality, PersistenceError> {
56 let reality: Reality = read_bincode(path)?;
57 if !reality.memory_integrity() {
58 return Err(PersistenceError::IntegrityFailure(
59 "movement memory integrity failed",
60 ));
61 }
62 if reality.drift_check().hidden_drift_required {
63 return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
64 }
65 Ok(reality)
66}
67
68pub fn save_snapshot(snapshot: &RealitySnapshot, path: &Path) -> Result<(), PersistenceError> {
69 write_bincode(path, snapshot)
70}
71
72pub fn load_snapshot(path: &Path) -> Result<RealitySnapshot, PersistenceError> {
73 read_bincode(path)
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct SignedReality {
78 pub reality: Reality,
79 pub public_key: [u8; 32],
80 pub signature: Vec<u8>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SignedSnapshot {
85 pub snapshot: RealitySnapshot,
86 pub public_key: [u8; 32],
87 pub signature: Vec<u8>,
88}
89
90fn sign_bytes(message: &[u8], signing_key: &SigningKey) -> ([u8; 32], [u8; 64]) {
91 let signature: Signature = signing_key.sign(message);
92 (signing_key.verifying_key().to_bytes(), signature.to_bytes())
93}
94
95fn verify_bytes(
96 message: &[u8],
97 public_key: &[u8; 32],
98 signature: &[u8],
99) -> Result<(), PersistenceError> {
100 let sig_bytes: [u8; 64] = signature
101 .try_into()
102 .map_err(|_| PersistenceError::Signature("invalid signature length"))?;
103 let verifying_key = VerifyingKey::from_bytes(public_key)
104 .map_err(|_| PersistenceError::Signature("invalid public key"))?;
105 let signature = Signature::from_bytes(&sig_bytes);
106 verifying_key
107 .verify_strict(message, &signature)
108 .map_err(|_| PersistenceError::Signature("invalid signature"))
109}
110
111pub fn sign_reality(
112 reality: &Reality,
113 signing_key: &SigningKey,
114) -> Result<SignedReality, PersistenceError> {
115 let message = bincode::serialize(reality).map_err(PersistenceError::Serialization)?;
116 let (public_key, signature) = sign_bytes(&message, signing_key);
117 Ok(SignedReality {
118 reality: reality.clone(),
119 public_key,
120 signature: signature.to_vec(),
121 })
122}
123
124pub fn save_signed_reality(signed: &SignedReality, path: &Path) -> Result<(), PersistenceError> {
125 write_bincode(path, signed)
126}
127
128pub fn load_signed_reality(
129 path: &Path,
130 expected_public_key: &[u8; 32],
131) -> Result<SignedReality, PersistenceError> {
132 let signed: SignedReality = read_bincode(path)?;
133 let message = bincode::serialize(&signed.reality).map_err(PersistenceError::Serialization)?;
134 verify_bytes(&message, expected_public_key, &signed.signature)?;
135 if !signed.reality.memory_integrity() {
136 return Err(PersistenceError::IntegrityFailure(
137 "movement memory integrity failed",
138 ));
139 }
140 if signed.reality.drift_check().hidden_drift_required {
141 return Err(PersistenceError::IntegrityFailure("hidden drift detected"));
142 }
143 Ok(signed)
144}
145
146pub fn sign_snapshot(
147 snapshot: &RealitySnapshot,
148 signing_key: &SigningKey,
149) -> Result<SignedSnapshot, PersistenceError> {
150 let message = bincode::serialize(snapshot).map_err(PersistenceError::Serialization)?;
151 let (public_key, signature) = sign_bytes(&message, signing_key);
152 Ok(SignedSnapshot {
153 snapshot: snapshot.clone(),
154 public_key,
155 signature: signature.to_vec(),
156 })
157}
158
159pub fn save_signed_snapshot(signed: &SignedSnapshot, path: &Path) -> Result<(), PersistenceError> {
160 write_bincode(path, signed)
161}
162
163pub fn load_signed_snapshot(
164 path: &Path,
165 expected_public_key: &[u8; 32],
166) -> Result<SignedSnapshot, PersistenceError> {
167 let signed: SignedSnapshot = read_bincode(path)?;
168 let message = bincode::serialize(&signed.snapshot).map_err(PersistenceError::Serialization)?;
169 verify_bytes(&message, expected_public_key, &signed.signature)?;
170 Ok(signed)
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::{perform_movement_sequence, Boundary, Event, Identity, Law, State};
177 use rand::rngs::OsRng;
178
179 fn test_reality() -> Reality {
180 Reality::new(
181 Identity("persist-test".to_string()),
182 Boundary {
183 allowed_values: vec![
184 "before".to_string(),
185 "after".to_string(),
186 "done".to_string(),
187 ],
188 },
189 Law {
190 allowed_transitions: vec![
191 ("before".to_string(), "after".to_string()),
192 ("after".to_string(), "done".to_string()),
193 ],
194 },
195 State {
196 field: "before".to_string(),
197 },
198 )
199 }
200
201 #[test]
202 fn reality_roundtrip_preserves_integrity() {
203 let mut reality = test_reality();
204 let events = vec![
205 Event {
206 proposed_field: "after".to_string(),
207 },
208 Event {
209 proposed_field: "done".to_string(),
210 },
211 ];
212 let _ = perform_movement_sequence(&mut reality, events).unwrap();
213
214 let path = std::env::temp_dir().join("archimedes-test-reality.bin");
215 save_reality(&reality, &path).unwrap();
216 let loaded = load_reality(&path).unwrap();
217 assert_eq!(reality, loaded);
218 assert!(loaded.memory_integrity());
219 assert!(!loaded.drift_check().hidden_drift_required);
220 let _ = std::fs::remove_file(&path);
221 }
222
223 #[test]
224 fn snapshot_roundtrip_matches_original() {
225 let reality = test_reality();
226 let snapshot = reality.snapshot();
227 let path = std::env::temp_dir().join("archimedes-test-snapshot.bin");
228 save_snapshot(&snapshot, &path).unwrap();
229 let loaded = load_snapshot(&path).unwrap();
230 assert_eq!(snapshot, loaded);
231 let _ = std::fs::remove_file(&path);
232 }
233
234 #[test]
235 fn tampered_reality_file_is_rejected() {
236 let mut reality = test_reality();
237 let event = Event {
238 proposed_field: "after".to_string(),
239 };
240 let _ = perform_movement_sequence(&mut reality, vec![event]).unwrap();
241
242 let path = std::env::temp_dir().join("archimedes-test-tampered.bin");
243 save_reality(&reality, &path).unwrap();
244
245 let mut bytes = std::fs::read(&path).unwrap();
246 let mid = bytes.len() / 2;
247 bytes[mid] ^= 0xFF;
248 std::fs::write(&path, &bytes).unwrap();
249
250 let result = load_reality(&path);
251 assert!(result.is_err());
252 let _ = std::fs::remove_file(&path);
253 }
254
255 #[test]
256 fn signed_reality_roundtrip_works() {
257 let mut reality = test_reality();
258 let event = Event {
259 proposed_field: "after".to_string(),
260 };
261 let _ = perform_movement_sequence(&mut reality, vec![event]).unwrap();
262
263 let mut csprng = OsRng;
264 let signing_key = SigningKey::generate(&mut csprng);
265 let public_key = signing_key.verifying_key().to_bytes();
266
267 let signed = sign_reality(&reality, &signing_key).unwrap();
268 let path = std::env::temp_dir().join("archimedes-test-signed-reality.bin");
269 save_signed_reality(&signed, &path).unwrap();
270
271 let loaded = load_signed_reality(&path, &public_key).unwrap();
272 assert_eq!(loaded.reality, reality);
273 assert_eq!(loaded.public_key, public_key);
274 let _ = std::fs::remove_file(&path);
275 }
276
277 #[test]
278 fn signed_reality_rejects_wrong_key() {
279 let mut reality = test_reality();
280 let event = Event {
281 proposed_field: "after".to_string(),
282 };
283 let _ = perform_movement_sequence(&mut reality, vec![event]).unwrap();
284
285 let mut csprng = OsRng;
286 let signing_key = SigningKey::generate(&mut csprng);
287 let wrong_key = SigningKey::generate(&mut csprng).verifying_key().to_bytes();
288
289 let signed = sign_reality(&reality, &signing_key).unwrap();
290 let path = std::env::temp_dir().join("archimedes-test-signed-wrong.bin");
291 save_signed_reality(&signed, &path).unwrap();
292
293 let result = load_signed_reality(&path, &wrong_key);
294 assert!(result.is_err());
295 let _ = std::fs::remove_file(&path);
296 }
297
298 #[test]
299 fn signed_snapshot_roundtrip_works() {
300 let reality = test_reality();
301 let snapshot = reality.snapshot();
302
303 let mut csprng = OsRng;
304 let signing_key = SigningKey::generate(&mut csprng);
305 let public_key = signing_key.verifying_key().to_bytes();
306
307 let signed = sign_snapshot(&snapshot, &signing_key).unwrap();
308 let path = std::env::temp_dir().join("archimedes-test-signed-snapshot.bin");
309 save_signed_snapshot(&signed, &path).unwrap();
310
311 let loaded = load_signed_snapshot(&path, &public_key).unwrap();
312 assert_eq!(loaded.snapshot, snapshot);
313 let _ = std::fs::remove_file(&path);
314 }
315}