hive_btle/security/
genesis.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//! Mesh genesis protocol for creating new HIVE meshes
17//!
18//! A mesh is created through a genesis event where:
19//! - A cryptographic seed is generated (256 bits of entropy)
20//! - The mesh_id is derived from the name and seed
21//! - Encryption keys are derived from the seed
22//! - The creator becomes the initial authority
23//!
24//! # Example
25//!
26//! ```
27//! use hive_btle::security::{DeviceIdentity, MeshGenesis, MembershipPolicy};
28//!
29//! // Create the founder's identity
30//! let founder = DeviceIdentity::generate();
31//!
32//! // Create a new mesh
33//! let genesis = MeshGenesis::create("ALPHA-TEAM", &founder, MembershipPolicy::Controlled);
34//!
35//! // Get derived values
36//! let mesh_id = genesis.mesh_id();           // e.g., "A1B2C3D4"
37//! let secret = genesis.encryption_secret();   // 32-byte key
38//! let beacon_key = genesis.beacon_key_base(); // For encrypted beacons
39//! ```
40
41#[cfg(not(feature = "std"))]
42use alloc::string::String;
43#[cfg(not(feature = "std"))]
44use alloc::vec::Vec;
45
46use rand_core::{OsRng, RngCore};
47
48use super::identity::DeviceIdentity;
49
50/// Membership policy controlling how nodes can join the mesh
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum MembershipPolicy {
53    /// Anyone with mesh_id can attempt to discover and join
54    /// Least secure, useful for demos and open networks
55    Open,
56
57    /// Explicit enrollment by an authority is required
58    /// Balanced security for most deployments
59    Controlled,
60
61    /// Only pre-provisioned devices can join
62    /// Highest security for sensitive operations
63    Strict,
64}
65
66impl Default for MembershipPolicy {
67    fn default() -> Self {
68        Self::Controlled
69    }
70}
71
72/// Genesis event for creating a new mesh
73///
74/// Contains all the cryptographic material needed to bootstrap a mesh.
75/// The creator should securely store this for recovery purposes.
76#[derive(Clone)]
77pub struct MeshGenesis {
78    /// Human-readable mesh name
79    pub mesh_name: String,
80
81    /// 256-bit cryptographic seed (generated from CSPRNG)
82    mesh_seed: [u8; 32],
83
84    /// Creator's device identity
85    creator_identity: DeviceIdentity,
86
87    /// Timestamp of creation (milliseconds since Unix epoch)
88    pub created_at_ms: u64,
89
90    /// Membership policy for this mesh
91    pub policy: MembershipPolicy,
92}
93
94impl MeshGenesis {
95    /// HKDF context for encryption key derivation
96    const ENCRYPTION_CONTEXT: &'static [u8] = b"HIVE-mesh-encryption-v1";
97
98    /// HKDF context for beacon key derivation
99    const BEACON_CONTEXT: &'static [u8] = b"HIVE-beacon-key-v1";
100
101    /// Create a new mesh as the founding controller
102    ///
103    /// # Arguments
104    /// * `mesh_name` - Human-readable name for the mesh
105    /// * `creator` - Device identity of the mesh creator
106    /// * `policy` - Membership policy for the mesh
107    ///
108    /// # Returns
109    /// A new MeshGenesis with cryptographically secure seed
110    pub fn create(mesh_name: &str, creator: &DeviceIdentity, policy: MembershipPolicy) -> Self {
111        let mut mesh_seed = [0u8; 32];
112        OsRng.fill_bytes(&mut mesh_seed);
113
114        Self {
115            mesh_name: mesh_name.into(),
116            mesh_seed,
117            creator_identity: creator.clone(),
118            created_at_ms: Self::now_ms(),
119            policy,
120        }
121    }
122
123    /// Create genesis with a specific seed (for testing or deterministic creation)
124    ///
125    /// # Safety
126    /// Only use with cryptographically random seeds in production.
127    pub fn with_seed(
128        mesh_name: &str,
129        seed: [u8; 32],
130        creator: &DeviceIdentity,
131        policy: MembershipPolicy,
132    ) -> Self {
133        Self {
134            mesh_name: mesh_name.into(),
135            mesh_seed: seed,
136            creator_identity: creator.clone(),
137            created_at_ms: Self::now_ms(),
138            policy,
139        }
140    }
141
142    /// Derive the mesh_id from name and seed
143    ///
144    /// The mesh_id is 8 hex characters derived from BLAKE3 keyed hash.
145    /// Format: uppercase hex, e.g., "A1B2C3D4"
146    pub fn mesh_id(&self) -> String {
147        let hash = blake3::keyed_hash(&self.mesh_seed, self.mesh_name.as_bytes());
148        let hash_bytes = hash.as_bytes();
149
150        // First 4 bytes as uppercase hex = 8 characters
151        format!(
152            "{:02X}{:02X}{:02X}{:02X}",
153            hash_bytes[0], hash_bytes[1], hash_bytes[2], hash_bytes[3]
154        )
155    }
156
157    /// Derive the mesh-wide encryption secret
158    ///
159    /// Uses BLAKE3 key derivation with a specific context.
160    pub fn encryption_secret(&self) -> [u8; 32] {
161        blake3::derive_key(
162            core::str::from_utf8(Self::ENCRYPTION_CONTEXT).unwrap(),
163            &self.mesh_seed,
164        )
165    }
166
167    /// Derive the base key for encrypted beacons
168    ///
169    /// Beacon keys are rotated, but this is the base from which they're derived.
170    pub fn beacon_key_base(&self) -> [u8; 32] {
171        blake3::derive_key(
172            core::str::from_utf8(Self::BEACON_CONTEXT).unwrap(),
173            &self.mesh_seed,
174        )
175    }
176
177    /// Get the mesh seed for secure storage
178    ///
179    /// **Security**: This is the root secret. Protect it carefully.
180    pub fn mesh_seed(&self) -> &[u8; 32] {
181        &self.mesh_seed
182    }
183
184    /// Get the creator's identity
185    pub fn creator(&self) -> &DeviceIdentity {
186        &self.creator_identity
187    }
188
189    /// Get the creator's public key
190    pub fn creator_public_key(&self) -> [u8; 32] {
191        self.creator_identity.public_key()
192    }
193
194    /// Check if a given identity is the mesh creator
195    pub fn is_creator(&self, identity: &DeviceIdentity) -> bool {
196        self.creator_identity.public_key() == identity.public_key()
197    }
198
199    /// Encode genesis data for persistence
200    ///
201    /// Format:
202    /// - mesh_name length (2 bytes, LE)
203    /// - mesh_name (variable)
204    /// - mesh_seed (32 bytes)
205    /// - creator public key (32 bytes)
206    /// - creator private key (32 bytes) - SENSITIVE!
207    /// - created_at_ms (8 bytes, LE)
208    /// - policy (1 byte)
209    ///
210    /// Total: 107 + mesh_name.len() bytes
211    pub fn encode(&self) -> Vec<u8> {
212        let name_bytes = self.mesh_name.as_bytes();
213        let mut buf = Vec::with_capacity(107 + name_bytes.len());
214
215        // Mesh name (length-prefixed)
216        buf.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
217        buf.extend_from_slice(name_bytes);
218
219        // Mesh seed
220        buf.extend_from_slice(&self.mesh_seed);
221
222        // Creator identity (public + private keys)
223        buf.extend_from_slice(&self.creator_identity.public_key());
224        buf.extend_from_slice(&self.creator_identity.private_key_bytes());
225
226        // Timestamp
227        buf.extend_from_slice(&self.created_at_ms.to_le_bytes());
228
229        // Policy
230        buf.push(match self.policy {
231            MembershipPolicy::Open => 0,
232            MembershipPolicy::Controlled => 1,
233            MembershipPolicy::Strict => 2,
234        });
235
236        buf
237    }
238
239    /// Decode genesis data from bytes
240    ///
241    /// Returns None if data is invalid or too short.
242    pub fn decode(data: &[u8]) -> Option<Self> {
243        if data.len() < 107 {
244            return None;
245        }
246
247        // Mesh name
248        let name_len = u16::from_le_bytes([data[0], data[1]]) as usize;
249        if data.len() < 107 + name_len {
250            return None;
251        }
252
253        let mesh_name = String::from_utf8(data[2..2 + name_len].to_vec()).ok()?;
254        let offset = 2 + name_len;
255
256        // Mesh seed
257        let mut mesh_seed = [0u8; 32];
258        mesh_seed.copy_from_slice(&data[offset..offset + 32]);
259
260        // Creator public key (we'll reconstruct from private)
261        let _public_key = &data[offset + 32..offset + 64];
262
263        // Creator private key
264        let mut private_key = [0u8; 32];
265        private_key.copy_from_slice(&data[offset + 64..offset + 96]);
266        let creator_identity = DeviceIdentity::from_private_key(&private_key).ok()?;
267
268        // Timestamp
269        let created_at_ms = u64::from_le_bytes([
270            data[offset + 96],
271            data[offset + 97],
272            data[offset + 98],
273            data[offset + 99],
274            data[offset + 100],
275            data[offset + 101],
276            data[offset + 102],
277            data[offset + 103],
278        ]);
279
280        // Policy
281        let policy = match data[offset + 104] {
282            0 => MembershipPolicy::Open,
283            1 => MembershipPolicy::Controlled,
284            2 => MembershipPolicy::Strict,
285            _ => return None,
286        };
287
288        Some(Self {
289            mesh_name,
290            mesh_seed,
291            creator_identity,
292            created_at_ms,
293            policy,
294        })
295    }
296
297    /// Get current timestamp in milliseconds
298    #[cfg(feature = "std")]
299    fn now_ms() -> u64 {
300        std::time::SystemTime::now()
301            .duration_since(std::time::UNIX_EPOCH)
302            .map(|d| d.as_millis() as u64)
303            .unwrap_or(0)
304    }
305
306    #[cfg(not(feature = "std"))]
307    fn now_ms() -> u64 {
308        0 // Platform should provide timestamp
309    }
310}
311
312impl core::fmt::Debug for MeshGenesis {
313    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
314        f.debug_struct("MeshGenesis")
315            .field("mesh_name", &self.mesh_name)
316            .field("mesh_id", &self.mesh_id())
317            .field("creator_node_id", &self.creator_identity.node_id())
318            .field("created_at_ms", &self.created_at_ms)
319            .field("policy", &self.policy)
320            .field("mesh_seed", &"[REDACTED]")
321            .finish()
322    }
323}
324
325/// Shareable mesh credentials (without creator's private key)
326///
327/// This can be shared with nodes joining the mesh.
328#[derive(Debug, Clone)]
329pub struct MeshCredentials {
330    /// The mesh_id (derived, for verification)
331    pub mesh_id: String,
332
333    /// Mesh name
334    pub mesh_name: String,
335
336    /// Encryption secret for mesh-wide encryption
337    pub encryption_secret: [u8; 32],
338
339    /// Creator's public key (for verification)
340    pub creator_public_key: [u8; 32],
341
342    /// Membership policy
343    pub policy: MembershipPolicy,
344}
345
346impl MeshCredentials {
347    /// Create credentials from genesis data
348    pub fn from_genesis(genesis: &MeshGenesis) -> Self {
349        Self {
350            mesh_id: genesis.mesh_id(),
351            mesh_name: genesis.mesh_name.clone(),
352            encryption_secret: genesis.encryption_secret(),
353            creator_public_key: genesis.creator_public_key(),
354            policy: genesis.policy,
355        }
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_create_genesis() {
365        let creator = DeviceIdentity::generate();
366        let genesis = MeshGenesis::create("ALPHA-TEAM", &creator, MembershipPolicy::Controlled);
367
368        assert_eq!(genesis.mesh_name, "ALPHA-TEAM");
369        assert_eq!(genesis.policy, MembershipPolicy::Controlled);
370        assert!(genesis.is_creator(&creator));
371    }
372
373    #[test]
374    fn test_mesh_id_format() {
375        let creator = DeviceIdentity::generate();
376        let genesis = MeshGenesis::create("TEST", &creator, MembershipPolicy::Open);
377
378        let mesh_id = genesis.mesh_id();
379
380        // Should be 8 uppercase hex characters
381        assert_eq!(mesh_id.len(), 8);
382        assert!(mesh_id
383            .chars()
384            .all(|c| c.is_ascii_hexdigit() && !c.is_lowercase()));
385    }
386
387    #[test]
388    fn test_mesh_id_deterministic() {
389        let creator = DeviceIdentity::generate();
390        let seed = [0x42u8; 32];
391        let genesis = MeshGenesis::with_seed("TEST", seed, &creator, MembershipPolicy::Open);
392
393        // Multiple calls return same mesh_id
394        let id1 = genesis.mesh_id();
395        let id2 = genesis.mesh_id();
396        assert_eq!(id1, id2);
397    }
398
399    #[test]
400    fn test_different_names_different_ids() {
401        let creator = DeviceIdentity::generate();
402        let seed = [0x42u8; 32];
403
404        let genesis1 = MeshGenesis::with_seed("ALPHA", seed, &creator, MembershipPolicy::Open);
405        let genesis2 = MeshGenesis::with_seed("BRAVO", seed, &creator, MembershipPolicy::Open);
406
407        assert_ne!(genesis1.mesh_id(), genesis2.mesh_id());
408    }
409
410    #[test]
411    fn test_different_seeds_different_ids() {
412        let creator = DeviceIdentity::generate();
413
414        let genesis1 =
415            MeshGenesis::with_seed("TEST", [0x42u8; 32], &creator, MembershipPolicy::Open);
416        let genesis2 =
417            MeshGenesis::with_seed("TEST", [0x43u8; 32], &creator, MembershipPolicy::Open);
418
419        assert_ne!(genesis1.mesh_id(), genesis2.mesh_id());
420    }
421
422    #[test]
423    fn test_encryption_secret_deterministic() {
424        let creator = DeviceIdentity::generate();
425        let seed = [0x42u8; 32];
426        let genesis = MeshGenesis::with_seed("TEST", seed, &creator, MembershipPolicy::Open);
427
428        let secret1 = genesis.encryption_secret();
429        let secret2 = genesis.encryption_secret();
430
431        assert_eq!(secret1, secret2);
432        assert_ne!(secret1, seed); // Derived, not the same as seed
433    }
434
435    #[test]
436    fn test_beacon_key_different_from_encryption() {
437        let creator = DeviceIdentity::generate();
438        let genesis = MeshGenesis::create("TEST", &creator, MembershipPolicy::Open);
439
440        let encryption = genesis.encryption_secret();
441        let beacon = genesis.beacon_key_base();
442
443        assert_ne!(encryption, beacon);
444    }
445
446    #[test]
447    fn test_encode_decode_roundtrip() {
448        let creator = DeviceIdentity::generate();
449        let genesis = MeshGenesis::create("ALPHA-TEAM", &creator, MembershipPolicy::Strict);
450
451        let encoded = genesis.encode();
452        let decoded = MeshGenesis::decode(&encoded).unwrap();
453
454        assert_eq!(decoded.mesh_name, genesis.mesh_name);
455        assert_eq!(decoded.mesh_id(), genesis.mesh_id());
456        assert_eq!(decoded.encryption_secret(), genesis.encryption_secret());
457        assert_eq!(decoded.policy, genesis.policy);
458        assert!(decoded.is_creator(&creator));
459    }
460
461    #[test]
462    fn test_decode_too_short() {
463        let short_data = [0u8; 50];
464        assert!(MeshGenesis::decode(&short_data).is_none());
465    }
466
467    #[test]
468    fn test_credentials_from_genesis() {
469        let creator = DeviceIdentity::generate();
470        let genesis = MeshGenesis::create("TEST", &creator, MembershipPolicy::Controlled);
471
472        let creds = MeshCredentials::from_genesis(&genesis);
473
474        assert_eq!(creds.mesh_id, genesis.mesh_id());
475        assert_eq!(creds.mesh_name, genesis.mesh_name);
476        assert_eq!(creds.encryption_secret, genesis.encryption_secret());
477        assert_eq!(creds.creator_public_key, genesis.creator_public_key());
478        assert_eq!(creds.policy, genesis.policy);
479    }
480
481    #[test]
482    fn test_policy_default() {
483        assert_eq!(MembershipPolicy::default(), MembershipPolicy::Controlled);
484    }
485
486    #[test]
487    fn test_debug_redacts_seed() {
488        let creator = DeviceIdentity::generate();
489        let genesis = MeshGenesis::create("TEST", &creator, MembershipPolicy::Open);
490
491        let debug_str = format!("{:?}", genesis);
492        assert!(debug_str.contains("REDACTED"));
493        assert!(debug_str.contains("mesh_id"));
494    }
495}