hive_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 hive_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 = HiveMesh::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"HIVE";
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 HIVE 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        storage.store("hive_persisted_state", &encoded)
262    }
263
264    /// Load state from secure storage.
265    ///
266    /// Returns `Err(NotFound)` if no state has been persisted.
267    pub fn load(storage: &dyn SecureStorage) -> Result<Self, PersistenceError> {
268        let data = storage
269            .retrieve("hive_persisted_state")?
270            .ok_or(PersistenceError::NotFound)?;
271
272        Self::decode(&data)
273    }
274
275    /// Delete persisted state from storage.
276    ///
277    /// Use with caution - this will require re-provisioning.
278    pub fn delete(storage: &dyn SecureStorage) -> Result<(), PersistenceError> {
279        storage.delete("hive_persisted_state")
280    }
281
282    /// Encode the state to bytes.
283    ///
284    /// Format:
285    /// - Magic (4 bytes): "HIVE"
286    /// - Version (4 bytes): u32 LE
287    /// - Private key (32 bytes)
288    /// - Persisted at (8 bytes): u64 LE timestamp
289    /// - Genesis length (4 bytes): u32 LE (0 if none)
290    /// - Genesis data (N bytes)
291    /// - Registry length (4 bytes): u32 LE
292    /// - Registry data (N bytes)
293    /// - Revoked count (4 bytes): u32 LE
294    /// - Revoked keys (32 bytes each)
295    pub fn encode(&self) -> Vec<u8> {
296        let genesis_len = self.genesis_data.as_ref().map(|d| d.len()).unwrap_or(0);
297        let capacity = 4
298            + 4
299            + 32
300            + 8
301            + 4
302            + genesis_len
303            + 4
304            + self.registry_data.len()
305            + 4
306            + self.revoked_keys.len() * 32;
307
308        let mut buf = Vec::with_capacity(capacity);
309
310        // Magic
311        buf.extend_from_slice(&MAGIC);
312
313        // Version
314        buf.extend_from_slice(&self.version.to_le_bytes());
315
316        // Private key
317        buf.extend_from_slice(&self.device_private_key);
318
319        // Timestamp
320        buf.extend_from_slice(&self.persisted_at_ms.to_le_bytes());
321
322        // Genesis
323        buf.extend_from_slice(&(genesis_len as u32).to_le_bytes());
324        if let Some(ref data) = self.genesis_data {
325            buf.extend_from_slice(data);
326        }
327
328        // Registry
329        buf.extend_from_slice(&(self.registry_data.len() as u32).to_le_bytes());
330        buf.extend_from_slice(&self.registry_data);
331
332        // Revoked keys
333        buf.extend_from_slice(&(self.revoked_keys.len() as u32).to_le_bytes());
334        for key in &self.revoked_keys {
335            buf.extend_from_slice(key);
336        }
337
338        buf
339    }
340
341    /// Decode state from bytes.
342    pub fn decode(data: &[u8]) -> Result<Self, PersistenceError> {
343        // Minimum size: magic(4) + version(4) + key(32) + timestamp(8) + genesis_len(4) + registry_len(4) + revoked_count(4)
344        if data.len() < 60 {
345            return Err(PersistenceError::InvalidFormat);
346        }
347
348        let mut offset = 0;
349
350        // Magic
351        if data[offset..offset + 4] != MAGIC {
352            return Err(PersistenceError::InvalidFormat);
353        }
354        offset += 4;
355
356        // Version
357        let version = u32::from_le_bytes([
358            data[offset],
359            data[offset + 1],
360            data[offset + 2],
361            data[offset + 3],
362        ]);
363        offset += 4;
364
365        if version > PERSISTED_STATE_VERSION {
366            return Err(PersistenceError::UnsupportedVersion {
367                stored: version,
368                supported: PERSISTED_STATE_VERSION,
369            });
370        }
371
372        // Private key
373        let mut device_private_key = [0u8; 32];
374        device_private_key.copy_from_slice(&data[offset..offset + 32]);
375        offset += 32;
376
377        // Timestamp
378        let persisted_at_ms = u64::from_le_bytes([
379            data[offset],
380            data[offset + 1],
381            data[offset + 2],
382            data[offset + 3],
383            data[offset + 4],
384            data[offset + 5],
385            data[offset + 6],
386            data[offset + 7],
387        ]);
388        offset += 8;
389
390        // Genesis
391        let genesis_len = u32::from_le_bytes([
392            data[offset],
393            data[offset + 1],
394            data[offset + 2],
395            data[offset + 3],
396        ]) as usize;
397        offset += 4;
398
399        if data.len() < offset + genesis_len {
400            return Err(PersistenceError::InvalidFormat);
401        }
402
403        let genesis_data = if genesis_len > 0 {
404            Some(data[offset..offset + genesis_len].to_vec())
405        } else {
406            None
407        };
408        offset += genesis_len;
409
410        // Registry
411        if data.len() < offset + 4 {
412            return Err(PersistenceError::InvalidFormat);
413        }
414
415        let registry_len = u32::from_le_bytes([
416            data[offset],
417            data[offset + 1],
418            data[offset + 2],
419            data[offset + 3],
420        ]) as usize;
421        offset += 4;
422
423        if data.len() < offset + registry_len {
424            return Err(PersistenceError::InvalidFormat);
425        }
426
427        let registry_data = data[offset..offset + registry_len].to_vec();
428        offset += registry_len;
429
430        // Revoked keys
431        if data.len() < offset + 4 {
432            return Err(PersistenceError::InvalidFormat);
433        }
434
435        let revoked_count = u32::from_le_bytes([
436            data[offset],
437            data[offset + 1],
438            data[offset + 2],
439            data[offset + 3],
440        ]) as usize;
441        offset += 4;
442
443        if data.len() < offset + revoked_count * 32 {
444            return Err(PersistenceError::InvalidFormat);
445        }
446
447        let mut revoked_keys = Vec::with_capacity(revoked_count);
448        for _ in 0..revoked_count {
449            let mut key = [0u8; 32];
450            key.copy_from_slice(&data[offset..offset + 32]);
451            revoked_keys.push(key);
452            offset += 32;
453        }
454
455        Ok(Self {
456            version,
457            device_private_key,
458            genesis_data,
459            registry_data,
460            revoked_keys,
461            persisted_at_ms,
462        })
463    }
464
465    /// Set the persistence timestamp.
466    pub fn set_persisted_at(&mut self, timestamp_ms: u64) {
467        self.persisted_at_ms = timestamp_ms;
468    }
469}
470
471/// In-memory storage for testing.
472///
473/// Not secure - only use for tests!
474#[cfg(any(test, feature = "std"))]
475pub struct MemoryStorage {
476    data: std::sync::Mutex<std::collections::HashMap<String, Vec<u8>>>,
477}
478
479#[cfg(any(test, feature = "std"))]
480impl MemoryStorage {
481    /// Create a new in-memory storage.
482    pub fn new() -> Self {
483        Self {
484            data: std::sync::Mutex::new(std::collections::HashMap::new()),
485        }
486    }
487}
488
489#[cfg(any(test, feature = "std"))]
490impl Default for MemoryStorage {
491    fn default() -> Self {
492        Self::new()
493    }
494}
495
496#[cfg(any(test, feature = "std"))]
497impl SecureStorage for MemoryStorage {
498    fn store(&self, key: &str, value: &[u8]) -> Result<(), PersistenceError> {
499        let mut data = self
500            .data
501            .lock()
502            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
503        data.insert(key.to_string(), value.to_vec());
504        Ok(())
505    }
506
507    fn retrieve(&self, key: &str) -> Result<Option<Vec<u8>>, PersistenceError> {
508        let data = self
509            .data
510            .lock()
511            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
512        Ok(data.get(key).cloned())
513    }
514
515    fn delete(&self, key: &str) -> Result<(), PersistenceError> {
516        let mut data = self
517            .data
518            .lock()
519            .map_err(|e| PersistenceError::StorageError(e.to_string()))?;
520        data.remove(key);
521        Ok(())
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::security::{DeviceIdentity, MembershipPolicy, MeshGenesis};
529
530    #[test]
531    fn test_persisted_state_roundtrip() {
532        let identity = DeviceIdentity::generate();
533        let genesis = MeshGenesis::create("TEST-MESH", &identity, MembershipPolicy::Controlled);
534
535        let mut state = PersistedState::new(&identity, Some(&genesis));
536        state.set_persisted_at(1234567890);
537        state.add_revoked_key([0xAA; 32]);
538        state.add_revoked_key([0xBB; 32]);
539
540        let encoded = state.encode();
541        let decoded = PersistedState::decode(&encoded).unwrap();
542
543        assert_eq!(decoded.version, PERSISTED_STATE_VERSION);
544        assert_eq!(decoded.persisted_at_ms, 1234567890);
545        assert_eq!(decoded.revoked_keys.len(), 2);
546
547        // Verify identity restoration
548        let restored_identity = decoded.restore_identity().unwrap();
549        assert_eq!(restored_identity.node_id(), identity.node_id());
550        assert_eq!(restored_identity.public_key(), identity.public_key());
551
552        // Verify genesis restoration
553        let restored_genesis = decoded.restore_genesis().unwrap();
554        assert_eq!(restored_genesis.mesh_id(), genesis.mesh_id());
555    }
556
557    #[test]
558    fn test_persisted_state_without_genesis() {
559        let identity = DeviceIdentity::generate();
560        let state = PersistedState::new(&identity, None);
561
562        let encoded = state.encode();
563        let decoded = PersistedState::decode(&encoded).unwrap();
564
565        assert!(decoded.restore_genesis().is_none());
566
567        let restored_identity = decoded.restore_identity().unwrap();
568        assert_eq!(restored_identity.node_id(), identity.node_id());
569    }
570
571    #[test]
572    fn test_persisted_state_with_registry() {
573        let identity1 = DeviceIdentity::generate();
574        let identity2 = DeviceIdentity::generate();
575        let identity3 = DeviceIdentity::generate();
576
577        let mut registry = IdentityRegistry::new();
578        registry.verify_or_register(&identity2.create_attestation(1000));
579        registry.verify_or_register(&identity3.create_attestation(2000));
580
581        let state = PersistedState::with_registry(&identity1, None, &registry);
582
583        let encoded = state.encode();
584        let decoded = PersistedState::decode(&encoded).unwrap();
585
586        let restored_registry = decoded.restore_registry();
587        assert_eq!(restored_registry.len(), 2);
588        assert!(restored_registry.is_known(identity2.node_id()));
589        assert!(restored_registry.is_known(identity3.node_id()));
590    }
591
592    #[test]
593    fn test_memory_storage() {
594        let storage = MemoryStorage::new();
595        let identity = DeviceIdentity::generate();
596        let state = PersistedState::new(&identity, None);
597
598        // Save
599        state.save(&storage).unwrap();
600
601        // Load
602        let loaded = PersistedState::load(&storage).unwrap();
603        let restored = loaded.restore_identity().unwrap();
604        assert_eq!(restored.node_id(), identity.node_id());
605
606        // Delete
607        PersistedState::delete(&storage).unwrap();
608        assert!(matches!(
609            PersistedState::load(&storage),
610            Err(PersistenceError::NotFound)
611        ));
612    }
613
614    #[test]
615    fn test_invalid_magic() {
616        let mut data = vec![0u8; 100];
617        data[0..4].copy_from_slice(b"NOPE");
618
619        assert!(matches!(
620            PersistedState::decode(&data),
621            Err(PersistenceError::InvalidFormat)
622        ));
623    }
624
625    #[test]
626    fn test_unsupported_version() {
627        let identity = DeviceIdentity::generate();
628        let state = PersistedState::new(&identity, None);
629        let mut encoded = state.encode();
630
631        // Set version to something higher than supported
632        encoded[4..8].copy_from_slice(&999u32.to_le_bytes());
633
634        assert!(matches!(
635            PersistedState::decode(&encoded),
636            Err(PersistenceError::UnsupportedVersion { .. })
637        ));
638    }
639
640    #[test]
641    fn test_revoked_keys_deduplication() {
642        let identity = DeviceIdentity::generate();
643        let mut state = PersistedState::new(&identity, None);
644
645        state.add_revoked_key([0xAA; 32]);
646        state.add_revoked_key([0xAA; 32]); // Duplicate
647        state.add_revoked_key([0xBB; 32]);
648
649        assert_eq!(state.revoked_keys().len(), 2);
650    }
651}