Skip to main content

peat_mesh/security/
device_id.rs

1//! Device identifier type derived from Ed25519 public key.
2
3use super::error::SecurityError;
4use crate::transport::NodeId;
5use ed25519_dalek::VerifyingKey;
6use serde::{Deserialize, Serialize};
7use sha2::{Digest, Sha256};
8use std::fmt;
9
10/// Device identifier derived from Ed25519 public key.
11///
12/// The DeviceId is the first 16 bytes of SHA-256(public_key), providing:
13/// - Uniqueness guarantee from the cryptographic hash
14/// - Compact representation (32 hex chars instead of 64)
15/// - Collision resistance from SHA-256
16///
17/// # Example
18///
19/// ```ignore
20/// use peat_mesh::security::{DeviceKeypair, DeviceId};
21///
22/// let keypair = DeviceKeypair::generate();
23/// let device_id = keypair.device_id();
24///
25/// // Convert to hex string for display/serialization
26/// let hex = device_id.to_hex();
27///
28/// // Parse back from hex
29/// let parsed = DeviceId::from_hex(&hex)?;
30/// assert_eq!(device_id, parsed);
31/// ```
32#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub struct DeviceId([u8; 16]);
34
35impl DeviceId {
36    /// Create a DeviceId from an Ed25519 public key.
37    ///
38    /// The ID is computed as SHA-256(public_key)[0..16].
39    pub fn from_public_key(public_key: &VerifyingKey) -> Self {
40        let mut hasher = Sha256::new();
41        hasher.update(public_key.as_bytes());
42        let hash = hasher.finalize();
43
44        let mut id = [0u8; 16];
45        id.copy_from_slice(&hash[..16]);
46        DeviceId(id)
47    }
48
49    /// Create a DeviceId from raw public key bytes (32 bytes).
50    pub fn from_public_key_bytes(bytes: &[u8]) -> Result<Self, SecurityError> {
51        if bytes.len() != 32 {
52            return Err(SecurityError::InvalidPublicKey(format!(
53                "expected 32 bytes, got {}",
54                bytes.len()
55            )));
56        }
57
58        let verifying_key = VerifyingKey::from_bytes(bytes.try_into().unwrap())
59            .map_err(|e| SecurityError::InvalidPublicKey(e.to_string()))?;
60
61        Ok(Self::from_public_key(&verifying_key))
62    }
63
64    /// Create a DeviceId from raw bytes (16 bytes).
65    pub fn from_bytes(bytes: [u8; 16]) -> Self {
66        DeviceId(bytes)
67    }
68
69    /// Get the raw bytes of this DeviceId.
70    pub fn as_bytes(&self) -> &[u8; 16] {
71        &self.0
72    }
73
74    /// Convert to a lowercase hex string (32 characters).
75    pub fn to_hex(self) -> String {
76        hex_encode(&self.0)
77    }
78
79    /// Parse from a hex string (32 characters).
80    pub fn from_hex(hex: &str) -> Result<Self, SecurityError> {
81        let bytes = hex_decode(hex)
82            .map_err(|e| SecurityError::InvalidDeviceId(format!("invalid hex: {}", e)))?;
83
84        if bytes.len() != 16 {
85            return Err(SecurityError::InvalidDeviceId(format!(
86                "expected 16 bytes (32 hex chars), got {} bytes",
87                bytes.len()
88            )));
89        }
90
91        let mut id = [0u8; 16];
92        id.copy_from_slice(&bytes);
93        Ok(DeviceId(id))
94    }
95}
96
97impl fmt::Display for DeviceId {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}", self.to_hex())
100    }
101}
102
103impl fmt::Debug for DeviceId {
104    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105        write!(f, "DeviceId({})", self.to_hex())
106    }
107}
108
109impl From<DeviceId> for NodeId {
110    fn from(device_id: DeviceId) -> Self {
111        NodeId::new(device_id.to_hex())
112    }
113}
114
115impl TryFrom<&NodeId> for DeviceId {
116    type Error = SecurityError;
117
118    fn try_from(node_id: &NodeId) -> Result<Self, Self::Error> {
119        DeviceId::from_hex(node_id.as_str())
120    }
121}
122
123// Simple hex encode/decode without external dependency
124fn hex_encode(bytes: &[u8]) -> String {
125    bytes.iter().map(|b| format!("{:02x}", b)).collect()
126}
127
128fn hex_decode(hex: &str) -> Result<Vec<u8>, String> {
129    if !hex.len().is_multiple_of(2) {
130        return Err("odd length hex string".to_string());
131    }
132
133    (0..hex.len())
134        .step_by(2)
135        .map(|i| {
136            u8::from_str_radix(&hex[i..i + 2], 16)
137                .map_err(|e| format!("invalid hex at position {}: {}", i, e))
138        })
139        .collect()
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use ed25519_dalek::SigningKey;
146    use rand_core::OsRng;
147
148    #[test]
149    fn test_device_id_from_public_key_deterministic() {
150        let signing_key = SigningKey::generate(&mut OsRng);
151        let verifying_key = signing_key.verifying_key();
152
153        let id1 = DeviceId::from_public_key(&verifying_key);
154        let id2 = DeviceId::from_public_key(&verifying_key);
155
156        assert_eq!(id1, id2);
157    }
158
159    #[test]
160    fn test_device_id_different_keys_different_ids() {
161        let key1 = SigningKey::generate(&mut OsRng);
162        let key2 = SigningKey::generate(&mut OsRng);
163
164        let id1 = DeviceId::from_public_key(&key1.verifying_key());
165        let id2 = DeviceId::from_public_key(&key2.verifying_key());
166
167        assert_ne!(id1, id2);
168    }
169
170    #[test]
171    fn test_device_id_hex_roundtrip() {
172        let key = SigningKey::generate(&mut OsRng);
173        let id = DeviceId::from_public_key(&key.verifying_key());
174
175        let hex = id.to_hex();
176        assert_eq!(hex.len(), 32);
177
178        let parsed = DeviceId::from_hex(&hex).unwrap();
179        assert_eq!(id, parsed);
180    }
181
182    #[test]
183    fn test_device_id_to_node_id() {
184        let key = SigningKey::generate(&mut OsRng);
185        let device_id = DeviceId::from_public_key(&key.verifying_key());
186
187        let node_id: NodeId = device_id.into();
188        assert_eq!(node_id.as_str(), device_id.to_hex());
189    }
190
191    #[test]
192    fn test_device_id_from_invalid_hex() {
193        let result = DeviceId::from_hex("not-valid-hex");
194        assert!(result.is_err());
195
196        let result = DeviceId::from_hex("abc"); // odd length
197        assert!(result.is_err());
198
199        let result = DeviceId::from_hex("00112233"); // too short
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn test_from_public_key_bytes() {
205        let key = SigningKey::generate(&mut OsRng);
206        let pk_bytes = key.verifying_key().to_bytes();
207
208        let id = DeviceId::from_public_key_bytes(&pk_bytes).unwrap();
209        let expected = DeviceId::from_public_key(&key.verifying_key());
210        assert_eq!(id, expected);
211    }
212
213    #[test]
214    fn test_from_public_key_bytes_wrong_length() {
215        let result = DeviceId::from_public_key_bytes(&[0u8; 16]);
216        assert!(result.is_err());
217    }
218
219    #[test]
220    fn test_from_bytes_and_as_bytes() {
221        let raw = [1u8; 16];
222        let id = DeviceId::from_bytes(raw);
223        assert_eq!(id.as_bytes(), &raw);
224    }
225
226    #[test]
227    fn test_display() {
228        let id = DeviceId::from_bytes([0xab; 16]);
229        let s = format!("{}", id);
230        assert_eq!(s, "abababababababababababababababab");
231    }
232
233    #[test]
234    fn test_debug() {
235        let id = DeviceId::from_bytes([0xcd; 16]);
236        let s = format!("{:?}", id);
237        assert!(s.starts_with("DeviceId("));
238        assert!(s.contains("cdcdcdcd"));
239    }
240
241    #[test]
242    fn test_try_from_node_id() {
243        let key = SigningKey::generate(&mut OsRng);
244        let device_id = DeviceId::from_public_key(&key.verifying_key());
245        let node_id: NodeId = device_id.into();
246
247        let back: DeviceId = (&node_id).try_into().unwrap();
248        assert_eq!(back, device_id);
249    }
250
251    #[test]
252    fn test_try_from_invalid_node_id() {
253        let node_id = NodeId::new("not-hex".into());
254        let result: Result<DeviceId, _> = (&node_id).try_into();
255        assert!(result.is_err());
256    }
257
258    #[test]
259    fn test_hex_decode_invalid_chars() {
260        assert!(hex_decode("zz").is_err());
261        assert!(hex_decode("gg").is_err());
262    }
263
264    #[test]
265    fn test_hex_roundtrip() {
266        let bytes = vec![0x00, 0xff, 0x0a, 0xb5];
267        let encoded = hex_encode(&bytes);
268        assert_eq!(encoded, "00ff0ab5");
269        let decoded = hex_decode(&encoded).unwrap();
270        assert_eq!(decoded, bytes);
271    }
272}