# Security Integration Guide
> For Developers: APIs, code examples, and integration patterns
## Quick Start
### Minimal Secure Mesh
```rust
use peat_btle::{PeatMesh, PeatMeshConfig, NodeId};
// 1. Generate or load shared secret (32 bytes)
let secret: [u8; 32] = load_from_secure_storage();
// 2. Create mesh with encryption
let config = PeatMeshConfig::new(
NodeId::new(0x12345678), // Unique node ID
"ALPHA-1", // Human-readable callsign
"DEMO", // Mesh identifier
).with_encryption(secret);
let mesh = PeatMesh::new(config);
// 3. All documents are now encrypted automatically
let encrypted_doc = mesh.build_document(); // Ready for transmission
```
## Mesh Configuration
### PeatMeshConfig
```rust
pub struct PeatMeshConfig {
pub node_id: NodeId, // 32-bit unique identifier
pub callsign: String, // Display name (e.g., "ALPHA-1")
pub mesh_id: String, // Network identifier (e.g., "DEMO")
pub peripheral_type: PeripheralType,
pub peer_config: PeerManagerConfig,
pub sync_interval_ms: u64, // How often to broadcast (default: 5000)
pub auto_broadcast_events: bool, // Auto-send on emergency/ack
pub encryption_secret: Option<[u8; 32]>, // Mesh-wide encryption key
}
```
### Configuration Builder Pattern
```rust
let config = PeatMeshConfig::new(node_id, "ALPHA-1", "DEMO")
.with_encryption(secret) // Enable encryption
.with_peripheral_type(PeripheralType::SoldierSensor)
.with_sync_interval(10_000) // 10 second sync
.with_peer_timeout(60_000) // 60 second peer timeout
.with_max_peers(16); // Limit peer tracking
```
## Phase 1: Mesh-Wide Encryption
### Enabling Encryption
**Option A: At Construction**
```rust
let config = PeatMeshConfig::new(node_id, callsign, mesh_id)
.with_encryption(secret);
let mesh = PeatMesh::new(config);
```
**Option B: After Construction**
```rust
let mut mesh = PeatMesh::new(config);
mesh.enable_encryption(&secret);
// Later, if needed:
mesh.disable_encryption();
```
### Checking Encryption Status
```rust
if mesh.is_encryption_enabled() {
println!("Documents will be encrypted");
} else {
println!("Warning: Documents sent in cleartext");
}
// Check strict mode
if mesh.is_strict_encryption_enabled() {
println!("Strict mode: unencrypted documents will be rejected");
}
```
### Enabling Strict Encryption Mode
Strict mode rejects unencrypted documents when encryption is enabled, preventing downgrade attacks:
```rust
let config = PeatMeshConfig::new(node_id, callsign, mesh_id)
.with_encryption(secret)
.with_strict_encryption(); // Reject unencrypted docs
let mesh = PeatMesh::new(config);
```
**When to use strict mode:**
- Production deployments where all nodes are encrypted
- After verifying all mesh participants have encryption enabled
- When downgrade attack prevention is required
**When NOT to use strict mode:**
- During gradual rollout (some nodes not yet encrypted)
- Development/testing with mixed encryption states
- Backward compatibility with legacy unencrypted nodes
### How Documents Are Encrypted
Encryption is transparent—just use the normal APIs:
```rust
// These are automatically encrypted if encryption is enabled:
let doc = mesh.build_document(); // Sync document
let doc = mesh.send_emergency(timestamp); // Emergency event
let doc = mesh.send_ack(timestamp); // ACK response
// Receiving side decrypts automatically:
mesh.on_ble_data_received(identifier, &encrypted_data, now_ms);
```
### Wire Format
```
Unencrypted: [Document bytes...]
Encrypted: [0xAE][0x00][nonce: 12 bytes][ciphertext + tag]
```
### Overhead Calculation
```rust
const ENCRYPTION_OVERHEAD: usize = 30; // 2 + 12 + 16 bytes
// Example: 100-byte document
// Unencrypted: 100 bytes
// Encrypted: 130 bytes (well under 244-byte BLE MTU)
```
## Phase 2: Per-Peer E2EE
Per-peer E2EE provides point-to-point encryption where **only sender and recipient can decrypt**, even other mesh members cannot read the message.
### Enabling E2EE
```rust
// Each node must enable E2EE independently
mesh.enable_peer_e2ee();
// Check status
assert!(mesh.is_peer_e2ee_enabled());
// Get our public key (for out-of-band exchange if needed)
let our_pubkey: [u8; 32] = mesh.peer_e2ee_public_key().unwrap();
```
### Session Establishment
```rust
// Node A initiates session to Node B
let peer_node_id = NodeId::new(0x22222222);
let key_exchange_msg = mesh_a.initiate_peer_e2ee(peer_node_id, now_ms)
.expect("E2EE not enabled");
// Send key_exchange_msg to Node B over BLE...
// (use normal mesh.on_ble_data_received() - handles automatically)
// Node B receives and auto-responds via the key exchange handler
// Session establishes automatically when both sides complete handshake
```
### Complete Handshake Example
```rust
// === Setup ===
let mesh_a = PeatMesh::new(config_a);
let mesh_b = PeatMesh::new(config_b);
mesh_a.enable_peer_e2ee();
mesh_b.enable_peer_e2ee();
// === Node A initiates ===
let ke1 = mesh_a.initiate_peer_e2ee(node_b_id, now_ms).unwrap();
// === Node B receives, responds ===
// In practice, this happens via on_ble_data_received()
// which calls handle_key_exchange() internally
let ke2 = mesh_b.handle_key_exchange(&ke1, now_ms)
.expect("Invalid key exchange")
.0; // Response message
// === Node A completes handshake ===
mesh_a.handle_key_exchange(&ke2, now_ms);
// === Verify session established ===
assert!(mesh_a.has_peer_e2ee_session(node_b_id));
assert!(mesh_b.has_peer_e2ee_session(node_a_id));
```
### Sending Encrypted Messages
```rust
// Check session exists first
if mesh.has_peer_e2ee_session(peer_node_id) {
let plaintext = b"Sensitive command data";
let encrypted = mesh.send_peer_e2ee(peer_node_id, plaintext, now_ms)
.expect("Encryption failed");
// Send `encrypted` bytes to peer over BLE
// Peer decrypts via on_ble_data_received() automatically
}
```
### Receiving Encrypted Messages
Decryption is automatic when using standard receive methods:
```rust
// Handles key exchange AND encrypted messages automatically
let result = mesh.on_ble_data_received(identifier, &data, now_ms);
// For E2EE messages, observers are notified:
impl PeatObserver for MyObserver {
fn on_event(&self, event: PeatEvent) {
match event {
PeatEvent::PeerE2eeEstablished { peer_node_id } => {
println!("E2EE session ready with {:08X}", peer_node_id.as_u32());
}
PeatEvent::PeerE2eeMessageReceived { from_node, data } => {
println!("Got E2EE message from {:08X}: {:?}",
from_node.as_u32(), data);
}
PeatEvent::PeerE2eeClosed { peer_node_id } => {
println!("E2EE session closed with {:08X}", peer_node_id.as_u32());
}
_ => {}
}
}
}
```
### Session Management
```rust
// Check session state
let state = mesh.peer_e2ee_session_state(peer_node_id);
match state {
Some(SessionState::AwaitingPeerKey) => println!("Handshake in progress"),
Some(SessionState::Established) => println!("Ready for encrypted comms"),
Some(SessionState::Closed) => println!("Session ended"),
None => println!("No session"),
}
// Get session counts
let total = mesh.peer_e2ee_session_count();
let ready = mesh.peer_e2ee_established_count();
println!("{}/{} sessions established", ready, total);
// Close a session
mesh.close_peer_e2ee(peer_node_id);
// Disable E2EE entirely (clears all sessions)
mesh.disable_peer_e2ee();
```
## Peer Management
### Configuration
```rust
pub struct PeerManagerConfig {
pub peer_timeout_ms: u64, // Stale peer removal (default: 45000)
pub cleanup_interval_ms: u64, // Cleanup frequency (default: 10000)
pub sync_interval_ms: u64, // Sync broadcast interval (default: 5000)
pub sync_cooldown_ms: u64, // Min time between syncs to same peer (default: 30000)
pub auto_connect: bool, // Auto-connect on discovery (default: true)
pub mesh_id: String, // Filter peers by mesh
pub max_peers: usize, // Max tracked peers (default: 8)
}
```
### Peer Lifecycle
```rust
// Discovery (called by platform BLE)
let peer = mesh.on_ble_discovered(
"device-uuid", // Platform identifier
Some("HIVE_DEMO-22222222"), // Device name
-65, // RSSI
Some("DEMO"), // Mesh ID (from name parsing)
now_ms,
);
// Connection
let node_id = mesh.on_ble_connected("device-uuid", now_ms);
// Data received (auto-decrypts if encrypted)
let result = mesh.on_ble_data_received("device-uuid", &data, now_ms);
// Disconnection
mesh.on_ble_disconnected("device-uuid", DisconnectReason::RemoteRequest);
```
### Querying Peers
```rust
// All known peers
let all_peers: Vec<PeatPeer> = mesh.get_peers();
// Connected peers only
let connected: Vec<PeatPeer> = mesh.get_connected_peers();
// Specific peer
if let Some(peer) = mesh.get_peer(node_id) {
println!("Peer {} RSSI: {}", peer.display_name(), peer.rssi);
println!("Signal: {:?}", peer.signal_strength());
println!("Connected: {}", peer.is_connected);
}
// Counts
println!("Discovered: {}, Connected: {}",
mesh.peer_count(),
mesh.connected_count());
```
### Mesh Filtering
```rust
// Check if a device belongs to our mesh
if mesh.matches_mesh(device_mesh_id) {
// Same mesh, proceed with connection
} else {
// Different mesh, ignore
}
// Device name format: "HIVE_<mesh_id>-<node_id>"
// Example: "HIVE_DEMO-12345678"
// Parsed as: mesh_id="DEMO", node_id=0x12345678
```
## Observer Pattern
### Subscribing to Events
```rust
use std::sync::Arc;
struct MyObserver;
impl PeatObserver for MyObserver {
fn on_event(&self, event: PeatEvent) {
match event {
// Peer events
PeatEvent::PeerDiscovered { peer } => {
println!("Found: {} (RSSI: {})", peer.display_name(), peer.rssi);
}
PeatEvent::PeerConnected { node_id } => {
println!("Connected: {:08X}", node_id.as_u32());
}
PeatEvent::PeerDisconnected { node_id, reason } => {
println!("Disconnected: {:08X} ({:?})", node_id.as_u32(), reason);
}
PeatEvent::PeerLost { node_id } => {
println!("Lost (stale): {:08X}", node_id.as_u32());
}
// Mesh events
PeatEvent::MeshStateChanged { peer_count, connected_count } => {
println!("Mesh: {}/{} connected", connected_count, peer_count);
}
// Sync events
PeatEvent::DocumentSynced { from_node, total_count } => {
println!("Synced from {:08X}, count={}", from_node.as_u32(), total_count);
}
PeatEvent::EmergencyReceived { from_node } => {
println!("EMERGENCY from {:08X}!", from_node.as_u32());
}
PeatEvent::AckReceived { from_node } => {
println!("ACK from {:08X}", from_node.as_u32());
}
// E2EE events
PeatEvent::PeerE2eeEstablished { peer_node_id } => {
println!("E2EE ready: {:08X}", peer_node_id.as_u32());
}
PeatEvent::PeerE2eeMessageReceived { from_node, data } => {
println!("E2EE msg from {:08X}: {} bytes",
from_node.as_u32(), data.len());
}
PeatEvent::PeerE2eeClosed { peer_node_id } => {
println!("E2EE closed: {:08X}", peer_node_id.as_u32());
}
// Security events
PeatEvent::SecurityViolation { kind, source } => {
log::warn!("Security violation: {:?} from {:?}", kind, source);
// Handle violations:
// - UnencryptedInStrictMode: downgrade attack attempt
// - DecryptionFailed: wrong key or corrupted data
// - ReplayDetected: duplicate message counter
// - UnauthorizedNode: unknown node attempted access
}
}
}
}
// Register observer
let observer = Arc::new(MyObserver);
mesh.add_observer(observer.clone());
// Unregister when done
mesh.remove_observer(&observer);
```
## Error Handling
### Encryption Errors
```rust
use peat_btle::security::EncryptionError;
match mesh_encryption_key.decrypt(&encrypted_doc) {
Ok(plaintext) => { /* process */ }
Err(EncryptionError::DecryptionFailed) => {
// Wrong key or corrupted data
log::warn!("Decryption failed - possible key mismatch");
}
Err(EncryptionError::InvalidFormat) => {
// Not valid encrypted format
log::warn!("Invalid encrypted document format");
}
Err(EncryptionError::EncryptionFailed) => {
// Should not happen in practice
log::error!("Encryption operation failed");
}
}
```
### Handling Mismatched Keys
```rust
// When an encrypted mesh receives data from wrong key:
let result = mesh.on_ble_data_received(id, &data, now_ms);
if result.is_none() && data[0] == ENCRYPTED_MARKER {
// Document was encrypted but we couldn't decrypt
// Either: wrong shared secret, or corrupted data
log::warn!("Could not decrypt document from {}", id);
}
```
## Platform Integration
### iOS (Swift via UniFFI)
```swift
import PeatBTLE
// Create mesh
let config = PeatMeshConfig(
nodeId: NodeId(value: 0x12345678),
callsign: "ALPHA-1",
meshId: "DEMO"
)
config.setEncryption(secret: secretData)
let mesh = PeatMesh(config: config)
// CoreBluetooth callbacks → PeatMesh
func peripheral(_ peripheral: CBPeripheral,
didUpdateValueFor characteristic: CBCharacteristic,
error: Error?) {
guard let data = characteristic.value else { return }
let nowMs = UInt64(Date().timeIntervalSince1970 * 1000)
mesh.onBleDataReceived(
identifier: peripheral.identifier.uuidString,
data: data,
nowMs: nowMs
)
}
```
### Android (Kotlin via JNI)
```kotlin
import com.hive.btle.PeatMesh
import com.hive.btle.PeatMeshConfig
// Create mesh
val config = PeatMeshConfig(
nodeId = NodeId(0x12345678),
callsign = "ALPHA-1",
meshId = "DEMO"
).apply {
setEncryption(secret)
}
val mesh = PeatMesh(config)
// BluetoothGattCallback → PeatMesh
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
val data = characteristic.value
val nowMs = System.currentTimeMillis()
mesh.onBleDataReceived(
identifier = gatt.device.address,
data = data,
nowMs = nowMs
)
}
```
### ESP32 (C via FFI)
```c
#include "peat_btle.h"
// Create mesh
hive_mesh_config_t config = {
.node_id = 0x12345678,
.callsign = "ALPHA-1",
.mesh_id = "DEMO",
};
memcpy(config.encryption_secret, secret, 32);
config.encryption_enabled = true;
hive_mesh_t* mesh = hive_mesh_new(&config);
// NimBLE callback → PeatMesh
static int gatt_write_cb(uint16_t conn_handle, ...) {
uint64_t now_ms = esp_timer_get_time() / 1000;
hive_mesh_on_ble_data(mesh, identifier, data, len, now_ms);
return 0;
}
```
## Best Practices
### Secret Management
```rust
// DO: Generate cryptographically secure secret
use rand::RngCore;
let mut secret = [0u8; 32];
rand::rngs::OsRng.fill_bytes(&mut secret);
// DO: Store in secure enclave/keystore
#[cfg(target_os = "ios")]
let secret = keychain::get("mesh_secret")?;
#[cfg(target_os = "android")]
let secret = android_keystore::get("mesh_secret")?;
// DON'T: Hardcode secrets
let secret = [0x42u8; 32]; // INSECURE!
// DON'T: Log secrets
log::info!("Using secret: {:?}", secret); // NEVER DO THIS
```
### Graceful Degradation
```rust
// Handle mixed encrypted/unencrypted mesh during rollout
let config = if secure_mode_required {
PeatMeshConfig::new(node_id, callsign, mesh_id)
.with_encryption(secret)
} else {
// Development mode - no encryption
PeatMeshConfig::new(node_id, callsign, mesh_id)
};
```
### Testing
```rust
#[cfg(test)]
mod tests {
use super::*;
fn create_test_mesh(node_id: u32, secret: Option<[u8; 32]>) -> PeatMesh {
let mut config = PeatMeshConfig::new(
NodeId::new(node_id),
&format!("TEST-{}", node_id),
"TEST"
);
if let Some(s) = secret {
config = config.with_encryption(s);
}
PeatMesh::new(config)
}
#[test]
fn test_encrypted_exchange() {
let secret = [0x42u8; 32];
let mesh1 = create_test_mesh(0x11111111, Some(secret));
let mesh2 = create_test_mesh(0x22222222, Some(secret));
// mesh1 sends
let doc = mesh1.build_document();
assert!(doc[0] == ENCRYPTED_MARKER);
// mesh2 receives
let result = mesh2.on_ble_data_received_from_node(
NodeId::new(0x11111111),
&doc,
1000
);
assert!(result.is_some());
}
#[test]
fn test_wrong_key_rejected() {
let mesh1 = create_test_mesh(0x11111111, Some([0x42u8; 32]));
let mesh2 = create_test_mesh(0x22222222, Some([0x43u8; 32])); // Different!
let doc = mesh1.build_document();
let result = mesh2.on_ble_data_received_from_node(
NodeId::new(0x11111111),
&doc,
1000
);
assert!(result.is_none()); // Decryption failed
}
}
```
## Troubleshooting
### "Decryption failed" on all messages
1. Verify both nodes have identical 32-byte secret
2. Check mesh_id matches (key is derived from mesh_id + secret)
3. Ensure encryption enabled on both ends
### E2EE session not establishing
1. Verify `enable_peer_e2ee()` called on both nodes
2. Check max_sessions limit not reached
3. Verify key exchange messages being delivered
### Peers not discovered
1. Verify mesh_id matches in device name
2. Check BLE advertising is active
3. Verify `matches_mesh()` returns true
### High latency / dropped messages
1. Check encryption overhead fits in MTU (< 244 bytes typical)
2. Reduce document size if near MTU limit
3. Consider compression for large payloads