Skip to main content

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