Skip to main content

peat_btle/security/
persistence.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! Credential Persistence Layer
17//!
18//! Provides secure storage for mesh credentials, device identity, and security
19//! state across reboots. This is critical for unattended devices (sensors, relays)
20//! that must retain their identity and mesh membership after power cycles.
21//!
22//! # Architecture
23//!
24//! ```text
25//! ┌─────────────────────────────────────────────────────────────┐
26//! │                    PersistedState                           │
27//! │  ┌─────────────┐  ┌─────────────┐  ┌──────────────────┐    │
28//! │  │ DeviceKey   │  │ MeshGenesis │  │ IdentityRegistry │    │
29//! │  │ (Ed25519)   │  │ (mesh seed) │  │ (TOFU cache)     │    │
30//! │  └─────────────┘  └─────────────┘  └──────────────────────┘    │
31//! └─────────────────────────────────────────────────────────────┘
32//!                              │
33//!                              ▼
34//! ┌─────────────────────────────────────────────────────────────┐
35//! │                    SecureStorage Trait                       │
36//! └─────────────────────────────────────────────────────────────┘
37//!          │              │              │              │
38//!          ▼              ▼              ▼              ▼
39//!     ┌────────┐    ┌────────┐    ┌────────┐    ┌────────┐
40//!     │Android │    │  iOS   │    │ Linux  │    │ ESP32  │
41//!     │Keystore│    │Keychain│    │  File  │    │  NVS   │
42//!     └────────┘    └────────┘    └────────┘    └────────┘
43//! ```
44//!
45//! # Example
46//!
47//! ```ignore
48//! use peat_btle::security::{PersistedState, SecureStorage, DeviceIdentity, MeshGenesis};
49//!
50//! // On first boot: create and persist state
51//! let identity = DeviceIdentity::generate();
52//! let genesis = MeshGenesis::create("ALPHA", &identity, MembershipPolicy::Controlled);
53//! let state = PersistedState::new(identity, genesis);
54//! state.save(&storage)?;
55//!
56//! // On subsequent boots: restore state
57//! let state = PersistedState::load(&storage)?;
58//! let mesh = PeatMesh::from_persisted(state, config)?;
59//! ```
60
61#[cfg(not(feature = "std"))]
62use alloc::string::String;
63#[cfg(not(feature = "std"))]
64use alloc::vec::Vec;
65
66use super::{DeviceIdentity, IdentityRegistry, MeshGenesis};
67
68/// Current version of the persisted state format.
69///
70/// Increment when making breaking changes to support migrations.
71pub const PERSISTED_STATE_VERSION: u32 = 1;
72
73/// Magic bytes to identify persisted state files.
74const MAGIC: [u8; 4] = *b"PEAT";
75
76/// Errors that can occur during persistence operations.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum PersistenceError {
79    /// Storage backend error
80    StorageError(String),
81
82    /// Data corruption or invalid format
83    InvalidFormat,
84
85    /// Version mismatch (stored version newer than supported)
86    UnsupportedVersion {
87        /// Version found in the stored data
88        stored: u32,
89        /// Maximum version supported by this code
90        supported: u32,
91    },
92
93    /// Required data not found
94    NotFound,
95
96    /// Cryptographic operation failed
97    CryptoError(String),
98}
99
100#[cfg(feature = "std")]
101impl std::fmt::Display for PersistenceError {
102    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103        match self {
104            Self::StorageError(msg) => write!(f, "storage error: {}", msg),
105            Self::InvalidFormat => write!(f, "invalid format or corrupted data"),
106            Self::UnsupportedVersion { stored, supported } => {
107                write!(
108                    f,
109                    "unsupported version: stored={}, supported={}",
110                    stored, supported
111                )
112            }
113            Self::NotFound => write!(f, "persisted state not found"),
114            Self::CryptoError(msg) => write!(f, "crypto error: {}", msg),
115        }
116    }
117}
118
119#[cfg(feature = "std")]
120impl std::error::Error for PersistenceError {}
121
122/// Platform-agnostic secure storage abstraction.
123///
124/// Implementations should use platform-specific secure storage:
125/// - Android: EncryptedSharedPreferences / Keystore
126/// - iOS: Keychain Services
127/// - Linux: Encrypted file in XDG data directory
128/// - ESP32: Encrypted NVS partition
129/// - Windows: DPAPI
130pub trait SecureStorage {
131    /// Store bytes under the given key.
132    ///
133    /// The implementation should encrypt the data at rest using
134    /// platform-appropriate mechanisms.
135    fn store(&self, key: &str, value: &[u8]) -> Result<(), PersistenceError>;
136
137    /// Retrieve bytes for the given key.
138    ///
139    /// Returns `Ok(None)` if the key doesn't exist.
140    /// Returns `Err` if the key exists but cannot be decrypted.
141    fn retrieve(&self, key: &str) -> Result<Option<Vec<u8>>, PersistenceError>;
142
143    /// Delete the entry for the given key.
144    ///
145    /// Returns `Ok(())` even if the key didn't exist.
146    fn delete(&self, key: &str) -> Result<(), PersistenceError>;
147
148    /// Check if a key exists without retrieving its value.
149    fn exists(&self, key: &str) -> Result<bool, PersistenceError> {
150        Ok(self.retrieve(key)?.is_some())
151    }
152}
153
154/// Complete persisted state for a Peat node.
155///
156/// Contains all security-critical data needed to restore a node
157/// after reboot without network access.
158#[derive(Debug, Clone)]
159pub struct PersistedState {
160    /// Format version for migration support
161    pub version: u32,
162
163    /// Device identity (Ed25519 private key)
164    ///
165    /// This is the node's long-term identity. The private key must be
166    /// stored securely as it proves ownership of the node_id.
167    device_private_key: [u8; 32],
168
169    /// Mesh genesis block (contains mesh seed, name, policy)
170    ///
171    /// Optional - nodes may operate without mesh membership initially.
172    genesis_data: Option<Vec<u8>>,
173
174    /// TOFU identity registry (known peer public keys)
175    ///
176    /// Persisting this prevents "new device" warnings after reboot.
177    registry_data: Vec<u8>,
178
179    /// Revoked public keys
180    ///
181    /// Nodes that have been explicitly revoked from the mesh.
182    revoked_keys: Vec<[u8; 32]>,
183
184    /// Timestamp when state was last persisted
185    pub persisted_at_ms: u64,
186}
187
188impl PersistedState {
189    /// Create a new persisted state from components.
190    pub fn new(identity: &DeviceIdentity, genesis: Option<&MeshGenesis>) -> Self {
191        Self {
192            version: PERSISTED_STATE_VERSION,
193            device_private_key: identity.private_key_bytes(),
194            genesis_data: genesis.map(|g| g.encode()),
195            registry_data: Vec::new(),
196            revoked_keys: Vec::new(),
197            persisted_at_ms: 0,
198        }
199    }
200
201    /// Create persisted state with an existing identity registry.
202    pub fn with_registry(
203        identity: &DeviceIdentity,
204        genesis: Option<&MeshGenesis>,
205        registry: &IdentityRegistry,
206    ) -> Self {
207        Self {
208            version: PERSISTED_STATE_VERSION,
209            device_private_key: identity.private_key_bytes(),
210            genesis_data: genesis.map(|g| g.encode()),
211            registry_data: registry.encode(),
212            revoked_keys: Vec::new(),
213            persisted_at_ms: 0,
214        }
215    }
216
217    /// Restore the device identity from persisted state.
218    pub fn restore_identity(&self) -> Result<DeviceIdentity, PersistenceError> {
219        DeviceIdentity::from_private_key(&self.device_private_key)
220            .map_err(|e| PersistenceError::CryptoError(format!("{:?}", e)))
221    }
222
223    /// Restore the mesh genesis from persisted state.
224    pub fn restore_genesis(&self) -> Option<MeshGenesis> {
225        self.genesis_data
226            .as_ref()
227            .and_then(|data| MeshGenesis::decode(data))
228    }
229
230    /// Restore the identity registry from persisted state.
231    pub fn restore_registry(&self) -> IdentityRegistry {
232        if self.registry_data.is_empty() {
233            IdentityRegistry::new()
234        } else {
235            IdentityRegistry::decode(&self.registry_data).unwrap_or_default()
236        }
237    }
238
239    /// Get the list of revoked public keys.
240    pub fn revoked_keys(&self) -> &[[u8; 32]] {
241        &self.revoked_keys
242    }
243
244    /// Add a revoked public key.
245    pub fn add_revoked_key(&mut self, public_key: [u8; 32]) {
246        if !self.revoked_keys.contains(&public_key) {
247            self.revoked_keys.push(public_key);
248        }
249    }
250
251    /// Update the identity registry data.
252    pub fn update_registry(&mut self, registry: &IdentityRegistry) {
253        self.registry_data = registry.encode();
254    }
255
256    /// Save state to secure storage.
257    ///
258    /// The storage key used is "hive_persisted_state".
259    pub fn save(&self, storage: &dyn SecureStorage) -> Result<(), PersistenceError> {
260        let encoded = self.encode();
261        // NOTE: Key retained as "hive_persisted_state" for backward compatibility with existing persisted data
262        storage.store("hive_persisted_state", &encoded)
263    }
264
265    /// Load state from secure storage.
266    ///
267    /// Returns `Err(NotFound)` if no state has been persisted.
268    pub fn load(storage: &dyn SecureStorage) -> Result<Self, PersistenceError> {
269        let data = storage
270            .retrieve("hive_persisted_state")?
271            .ok_or(PersistenceError::NotFound)?;
272
273        Self::decode(&data)
274    }
275
276    /// Delete persisted state from storage.
277    ///
278    /// Use with caution - this will require re-provisioning.
279    pub fn delete(storage: &dyn SecureStorage) -> Result<(), PersistenceError> {
280        storage.delete("hive_persisted_state")
281    }
282
283    /// Encode the state to bytes.
284    ///
285    /// Format:
286    /// - Magic (4 bytes): "PEAT"
287    /// - Version (4 bytes): u32 LE
288    /// - Private key (32 bytes)
289    /// - Persisted at (8 bytes): u64 LE timestamp
290    /// - Genesis length (4 bytes): u32 LE (0 if none)
291    /// - Genesis data (N bytes)
292    /// - Registry length (4 bytes): u32 LE
293    /// - Registry data (N bytes)
294    /// - Revoked count (4 bytes): u32 LE
295    /// - Revoked keys (32 bytes each)
296    pub fn encode(&self) -> Vec<u8> {
297        let genesis_len = self.genesis_data.as_ref().map(|d| d.len()).unwrap_or(0);
298        let capacity = 4
299            + 4
300            + 32
301            + 8
302            + 4
303            + genesis_len
304            + 4
305            + self.registry_data.len()
306            + 4
307            + self.revoked_keys.len() * 32;
308
309        let mut buf = Vec::with_capacity(capacity);
310
311        // Magic
312        buf.extend_from_slice(&MAGIC);
313
314        // Version
315        buf.extend_from_slice(&self.version.to_le_bytes());
316
317        // Private key
318        buf.extend_from_slice(&self.device_private_key);
319
320        // Timestamp
321        buf.extend_from_slice(&self.persisted_at_ms.to_le_bytes());
322
323        // Genesis
324        buf.extend_from_slice(&(genesis_len as u32).to_le_bytes());
325        if let Some(ref data) = self.genesis_data {
326            buf.extend_from_slice(data);
327        }
328
329        // Registry
330        buf.extend_from_slice(&(self.registry_data.len() as u32).to_le_bytes());
331        buf.extend_from_slice(&self.registry_data);
332
333        // Revoked keys
334        buf.extend_from_slice(&(self.revoked_keys.len() as u32).to_le_bytes());
335        for key in &self.revoked_keys {
336            buf.extend_from_slice(key);
337        }
338
339        buf
340    }
341
342    /// Decode state from bytes.
343    pub fn decode(data: &[u8]) -> Result<Self, PersistenceError> {
344        // Minimum size: magic(4) + version(4) + key(32) + timestamp(8) + genesis_len(4) + registry_len(4) + revoked_count(4)
345        if data.len() < 60 {
346            return Err(PersistenceError::InvalidFormat);
347        }
348
349        let mut offset = 0;
350
351        // Magic
352        if data[offset..offset + 4] != MAGIC {
353            return Err(PersistenceError::InvalidFormat);
354        }
355        offset += 4;
356
357        // Version
358        let version = u32::from_le_bytes([
359            data[offset],
360            data[offset + 1],
361            data[offset + 2],
362            data[offset + 3],
363        ]);
364        offset += 4;
365
366        if version > PERSISTED_STATE_VERSION {
367            return Err(PersistenceError::UnsupportedVersion {
368                stored: version,
369                supported: PERSISTED_STATE_VERSION,
370            });
371        }
372
373        // Private key
374        let mut device_private_key = [0u8; 32];
375        device_private_key.copy_from_slice(&data[offset..offset + 32]);
376        offset += 32;
377
378        // Timestamp
379        let persisted_at_ms = u64::from_le_bytes([
380            data[offset],
381            data[offset + 1],
382            data[offset + 2],
383            data[offset + 3],
384            data[offset + 4],
385            data[offset + 5],
386            data[offset + 6],
387            data[offset + 7],
388        ]);
389        offset += 8;
390
391        // Genesis
392        let genesis_len = u32::from_le_bytes([
393            data[offset],
394            data[offset + 1],
395            data[offset + 2],
396            data[offset + 3],
397        ]) as usize;
398        offset += 4;
399
400        if data.len() < offset + genesis_len {
401            return Err(PersistenceError::InvalidFormat);
402        }
403
404        let genesis_data = if genesis_len > 0 {
405            Some(data[offset..offset + genesis_len].to_vec())
406        } else {
407            None
408        };
409        offset += genesis_len;
410
411        // Registry
412        if data.len() < offset + 4 {
413            return Err(PersistenceError::InvalidFormat);
414        }
415
416        let registry_len = u32::from_le_bytes([
417            data[offset],
418            data[offset + 1],
419            data[offset + 2],
420            data[offset + 3],
421        ]) as usize;
422        offset += 4;
423
424        if data.len() < offset + registry_len {
425            return Err(PersistenceError::InvalidFormat);
426        }
427
428        let registry_data = data[offset..offset + registry_len].to_vec();
429        offset += registry_len;
430
431        // Revoked keys
432        if data.len() < offset + 4 {
433            return Err(PersistenceError::InvalidFormat);
434        }
435
436        let revoked_count = u32::from_le_bytes([
437            data[offset],
438            data[offset + 1],
439            data[offset + 2],
440            data[offset + 3],
441        ]) as usize;
442        offset += 4;
443
444        if data.len() < offset + revoked_count * 32 {
445            return Err(PersistenceError::InvalidFormat);
446        }
447
448        let mut revoked_keys = Vec::with_capacity(revoked_count);
449        for _ in 0..revoked_count {
450            let mut key = [0u8; 32];
451            key.copy_from_slice(&data[offset..offset + 32]);
452            revoked_keys.push(key);
453            offset += 32;
454        }
455
456        Ok(Self {
457            version,
458            device_private_key,
459            genesis_data,
460            registry_data,
461            revoked_keys,
462            persisted_at_ms,
463        })
464    }
465
466    /// Set the persistence timestamp.
467    pub fn set_persisted_at(&mut self, timestamp_ms: u64) {
468        self.persisted_at_ms = timestamp_ms;
469    }
470}
471
472/// In-memory storage for testing.
473///
474/// Not secure - only use for tests!
475#[cfg(any(test, feature = "std"))]
476pub struct MemoryStorage {
477    data: std::sync::Mutex<std::collections::HashMap<String, Vec<u8>>>,
478}
479
480#[cfg(any(test, feature = "std"))]
481impl MemoryStorage {
482    /// Create a new in-memory storage.
483    pub fn new() -> Self {
484        Self {
485            data: std::sync::Mutex::new(std::collections::HashMap::new()),
486        }
487    }
488}
489
490#[cfg(any(test, feature = "std"))]
491impl Default for MemoryStorage {
492    fn default() -> Self {
493        Self::new()
494    }
495}
496
497#[cfg(any(test, feature = "std"))]
498impl SecureStorage for MemoryStorage {
499    fn store(&self, key: &str, value: &[u8]) -> Result<(), PersistenceError> {
500        let mut data = self
501            .data
502            .lock()
503            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
504        data.insert(key.to_string(), value.to_vec());
505        Ok(())
506    }
507
508    fn retrieve(&self, key: &str) -> Result<Option<Vec<u8>>, PersistenceError> {
509        let data = self
510            .data
511            .lock()
512            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
513        Ok(data.get(key).cloned())
514    }
515
516    fn delete(&self, key: &str) -> Result<(), PersistenceError> {
517        let mut data = self
518            .data
519            .lock()
520            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
521        data.remove(key);
522        Ok(())
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529    use crate::security::{DeviceIdentity, MembershipPolicy, MeshGenesis};
530
531    #[test]
532    fn test_persisted_state_roundtrip() {
533        let identity = DeviceIdentity::generate();
534        let genesis = MeshGenesis::create("TEST-MESH", &identity, MembershipPolicy::Controlled);
535
536        let mut state = PersistedState::new(&identity, Some(&genesis));
537        state.set_persisted_at(1234567890);
538        state.add_revoked_key([0xAA; 32]);
539        state.add_revoked_key([0xBB; 32]);
540
541        let encoded = state.encode();
542        let decoded = PersistedState::decode(&encoded).unwrap();
543
544        assert_eq!(decoded.version, PERSISTED_STATE_VERSION);
545        assert_eq!(decoded.persisted_at_ms, 1234567890);
546        assert_eq!(decoded.revoked_keys.len(), 2);
547
548        // Verify identity restoration
549        let restored_identity = decoded.restore_identity().unwrap();
550        assert_eq!(restored_identity.node_id(), identity.node_id());
551        assert_eq!(restored_identity.public_key(), identity.public_key());
552
553        // Verify genesis restoration
554        let restored_genesis = decoded.restore_genesis().unwrap();
555        assert_eq!(restored_genesis.mesh_id(), genesis.mesh_id());
556    }
557
558    #[test]
559    fn test_persisted_state_without_genesis() {
560        let identity = DeviceIdentity::generate();
561        let state = PersistedState::new(&identity, None);
562
563        let encoded = state.encode();
564        let decoded = PersistedState::decode(&encoded).unwrap();
565
566        assert!(decoded.restore_genesis().is_none());
567
568        let restored_identity = decoded.restore_identity().unwrap();
569        assert_eq!(restored_identity.node_id(), identity.node_id());
570    }
571
572    #[test]
573    fn test_persisted_state_with_registry() {
574        let identity1 = DeviceIdentity::generate();
575        let identity2 = DeviceIdentity::generate();
576        let identity3 = DeviceIdentity::generate();
577
578        let mut registry = IdentityRegistry::new();
579        registry.verify_or_register(&identity2.create_attestation(1000));
580        registry.verify_or_register(&identity3.create_attestation(2000));
581
582        let state = PersistedState::with_registry(&identity1, None, &registry);
583
584        let encoded = state.encode();
585        let decoded = PersistedState::decode(&encoded).unwrap();
586
587        let restored_registry = decoded.restore_registry();
588        assert_eq!(restored_registry.len(), 2);
589        assert!(restored_registry.is_known(identity2.node_id()));
590        assert!(restored_registry.is_known(identity3.node_id()));
591    }
592
593    #[test]
594    fn test_memory_storage() {
595        let storage = MemoryStorage::new();
596        let identity = DeviceIdentity::generate();
597        let state = PersistedState::new(&identity, None);
598
599        // Save
600        state.save(&storage).unwrap();
601
602        // Load
603        let loaded = PersistedState::load(&storage).unwrap();
604        let restored = loaded.restore_identity().unwrap();
605        assert_eq!(restored.node_id(), identity.node_id());
606
607        // Delete
608        PersistedState::delete(&storage).unwrap();
609        assert!(matches!(
610            PersistedState::load(&storage),
611            Err(PersistenceError::NotFound)
612        ));
613    }
614
615    #[test]
616    fn test_invalid_magic() {
617        let mut data = vec![0u8; 100];
618        data[0..4].copy_from_slice(b"NOPE");
619
620        assert!(matches!(
621            PersistedState::decode(&data),
622            Err(PersistenceError::InvalidFormat)
623        ));
624    }
625
626    #[test]
627    fn test_unsupported_version() {
628        let identity = DeviceIdentity::generate();
629        let state = PersistedState::new(&identity, None);
630        let mut encoded = state.encode();
631
632        // Set version to something higher than supported
633        encoded[4..8].copy_from_slice(&999u32.to_le_bytes());
634
635        assert!(matches!(
636            PersistedState::decode(&encoded),
637            Err(PersistenceError::UnsupportedVersion { .. })
638        ));
639    }
640
641    #[test]
642    fn test_revoked_keys_deduplication() {
643        let identity = DeviceIdentity::generate();
644        let mut state = PersistedState::new(&identity, None);
645
646        state.add_revoked_key([0xAA; 32]);
647        state.add_revoked_key([0xAA; 32]); // Duplicate
648        state.add_revoked_key([0xBB; 32]);
649
650        assert_eq!(state.revoked_keys().len(), 2);
651    }
652}