1use chrono::{DateTime, Utc};
2use k256::ecdsa::{SigningKey, signature::Signer};
3use roboticus_core::{Result, RoboticusError};
4use serde::{Deserialize, Serialize};
5use sha2::Digest;
6use std::collections::HashMap;
7use tracing::{debug, info};
8
9#[derive(Clone, Serialize, Deserialize)]
11pub struct DeviceIdentity {
12 pub device_id: String,
13 pub public_key_hex: String,
14 pub created_at: DateTime<Utc>,
15 #[serde(default)]
16 pub device_name: String,
17 #[serde(skip)]
18 pub signing_key: Option<SigningKey>,
19}
20
21impl std::fmt::Debug for DeviceIdentity {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("DeviceIdentity")
24 .field("device_id", &self.device_id)
25 .field("public_key_hex", &self.public_key_hex)
26 .field("created_at", &self.created_at)
27 .field("device_name", &self.device_name)
28 .field(
29 "signing_key",
30 &self.signing_key.as_ref().map(|_| "[REDACTED]"),
31 )
32 .finish()
33 }
34}
35
36impl DeviceIdentity {
37 pub fn generate(device_name: &str) -> Self {
39 let device_id = format!("dev_{}", generate_short_id());
40 let (public_key_hex, signing_key) = generate_keypair();
41
42 info!(device_id = %device_id, name = %device_name, "generated device identity");
43
44 Self {
45 device_id,
46 public_key_hex,
47 created_at: Utc::now(),
48 device_name: device_name.to_string(),
49 signing_key: Some(signing_key),
50 }
51 }
52
53 pub fn fingerprint(&self) -> String {
54 let hash = sha2::Sha256::digest(self.public_key_hex.as_bytes());
55 hex::encode(&hash[..8])
56 }
57
58 pub fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
59 let key = self
60 .signing_key
61 .as_ref()
62 .ok_or_else(|| RoboticusError::Config("no signing key available".into()))?;
63 let signature: k256::ecdsa::Signature = key.sign(data);
64 Ok(signature.to_bytes().to_vec())
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
70pub enum PairingState {
71 Pending,
72 Verified,
73 Rejected,
74 Expired,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct PairedDevice {
80 pub device_id: String,
81 pub public_key_hex: String,
82 pub device_name: String,
83 pub state: PairingState,
84 pub paired_at: Option<DateTime<Utc>>,
85 pub last_seen: Option<DateTime<Utc>>,
86}
87
88pub struct DeviceManager {
90 identity: DeviceIdentity,
91 paired_devices: HashMap<String, PairedDevice>,
92 max_paired: usize,
93}
94
95impl DeviceManager {
96 pub fn new(identity: DeviceIdentity, max_paired: usize) -> Self {
97 Self {
98 identity,
99 paired_devices: HashMap::new(),
100 max_paired,
101 }
102 }
103
104 pub fn identity(&self) -> &DeviceIdentity {
105 &self.identity
106 }
107
108 pub fn initiate_pairing(
110 &mut self,
111 remote_id: &str,
112 remote_pubkey: &str,
113 remote_name: &str,
114 ) -> Result<()> {
115 if self.paired_devices.len() >= self.max_paired {
116 return Err(RoboticusError::Config(format!(
117 "maximum paired devices ({}) reached",
118 self.max_paired
119 )));
120 }
121
122 if self.paired_devices.contains_key(remote_id) {
123 return Err(RoboticusError::Config(format!(
124 "device '{}' is already in pairing list",
125 remote_id
126 )));
127 }
128
129 self.paired_devices.insert(
130 remote_id.to_string(),
131 PairedDevice {
132 device_id: remote_id.to_string(),
133 public_key_hex: remote_pubkey.to_string(),
134 device_name: remote_name.to_string(),
135 state: PairingState::Pending,
136 paired_at: None,
137 last_seen: None,
138 },
139 );
140
141 debug!(remote = %remote_id, "pairing initiated");
142 Ok(())
143 }
144
145 pub fn verify_pairing(&mut self, remote_id: &str) -> Result<()> {
147 let device = self
148 .paired_devices
149 .get_mut(remote_id)
150 .ok_or_else(|| RoboticusError::Config(format!("device '{}' not found", remote_id)))?;
151
152 if device.state != PairingState::Pending {
153 return Err(RoboticusError::Config(format!(
154 "device '{}' is not in pending state",
155 remote_id
156 )));
157 }
158
159 device.state = PairingState::Verified;
160 device.paired_at = Some(Utc::now());
161 device.last_seen = Some(Utc::now());
162
163 info!(remote = %remote_id, "pairing verified");
164 Ok(())
165 }
166
167 pub fn reject_pairing(&mut self, remote_id: &str) -> Result<()> {
169 let device = self
170 .paired_devices
171 .get_mut(remote_id)
172 .ok_or_else(|| RoboticusError::Config(format!("device '{}' not found", remote_id)))?;
173
174 device.state = PairingState::Rejected;
175 debug!(remote = %remote_id, "pairing rejected");
176 Ok(())
177 }
178
179 pub fn unpair(&mut self, remote_id: &str) -> Result<()> {
181 self.paired_devices
182 .remove(remote_id)
183 .ok_or_else(|| RoboticusError::Config(format!("device '{}' not found", remote_id)))?;
184
185 info!(remote = %remote_id, "device unpaired");
186 Ok(())
187 }
188
189 pub fn record_seen(&mut self, remote_id: &str) {
191 if let Some(device) = self.paired_devices.get_mut(remote_id) {
192 device.last_seen = Some(Utc::now());
193 }
194 }
195
196 pub fn trusted_devices(&self) -> Vec<&PairedDevice> {
198 self.paired_devices
199 .values()
200 .filter(|d| d.state == PairingState::Verified)
201 .collect()
202 }
203
204 pub fn all_devices(&self) -> Vec<&PairedDevice> {
206 self.paired_devices.values().collect()
207 }
208
209 pub fn paired_count(&self) -> usize {
210 self.paired_devices.len()
211 }
212
213 pub fn is_trusted(&self, remote_id: &str) -> bool {
215 self.paired_devices
216 .get(remote_id)
217 .is_some_and(|d| d.state == PairingState::Verified)
218 }
219}
220
221fn generate_short_id() -> String {
222 format!("{:016x}", rand::random::<u64>())
223}
224
225fn generate_keypair() -> (String, SigningKey) {
226 let signing_key = SigningKey::random(&mut k256::elliptic_curve::rand_core::OsRng);
227 let point = signing_key.verifying_key().to_encoded_point(true);
228 (hex::encode(point.as_bytes()), signing_key)
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn test_identity() -> DeviceIdentity {
236 DeviceIdentity::generate("test-device")
237 }
238
239 fn test_manager() -> DeviceManager {
240 DeviceManager::new(test_identity(), 5)
241 }
242
243 #[test]
244 fn generate_identity() {
245 let id = DeviceIdentity::generate("laptop");
246 assert!(id.device_id.starts_with("dev_"));
247 assert_eq!(id.device_id.len(), 20);
248 assert!(!id.public_key_hex.is_empty());
249 assert_eq!(id.device_name, "laptop");
250 }
251
252 #[test]
253 fn identity_fingerprint() {
254 let id = test_identity();
255 let fp = id.fingerprint();
256 assert_eq!(fp.len(), 16);
257 }
258
259 #[test]
260 fn initiate_pairing() {
261 let mut mgr = test_manager();
262 mgr.initiate_pairing("remote-1", "04abcdef", "phone")
263 .unwrap();
264 assert_eq!(mgr.paired_count(), 1);
265 assert!(!mgr.is_trusted("remote-1"));
266 }
267
268 #[test]
269 fn verify_pairing() {
270 let mut mgr = test_manager();
271 mgr.initiate_pairing("remote-1", "04abcdef", "phone")
272 .unwrap();
273 mgr.verify_pairing("remote-1").unwrap();
274 assert!(mgr.is_trusted("remote-1"));
275 assert_eq!(mgr.trusted_devices().len(), 1);
276 }
277
278 #[test]
279 fn reject_pairing() {
280 let mut mgr = test_manager();
281 mgr.initiate_pairing("remote-1", "04abcdef", "phone")
282 .unwrap();
283 mgr.reject_pairing("remote-1").unwrap();
284 assert!(!mgr.is_trusted("remote-1"));
285 }
286
287 #[test]
288 fn unpair() {
289 let mut mgr = test_manager();
290 mgr.initiate_pairing("remote-1", "04abcdef", "phone")
291 .unwrap();
292 mgr.unpair("remote-1").unwrap();
293 assert_eq!(mgr.paired_count(), 0);
294 }
295
296 #[test]
297 fn max_paired_limit() {
298 let mut mgr = DeviceManager::new(test_identity(), 2);
299 mgr.initiate_pairing("d1", "key1", "dev1").unwrap();
300 mgr.initiate_pairing("d2", "key2", "dev2").unwrap();
301 let err = mgr.initiate_pairing("d3", "key3", "dev3").unwrap_err();
302 assert!(err.to_string().contains("maximum"));
303 }
304
305 #[test]
306 fn duplicate_pairing_rejected() {
307 let mut mgr = test_manager();
308 mgr.initiate_pairing("d1", "key1", "dev1").unwrap();
309 let err = mgr.initiate_pairing("d1", "key1", "dev1").unwrap_err();
310 assert!(err.to_string().contains("already"));
311 }
312
313 #[test]
314 fn verify_nonexistent_fails() {
315 let mut mgr = test_manager();
316 assert!(mgr.verify_pairing("nope").is_err());
317 }
318
319 #[test]
320 fn verify_non_pending_fails() {
321 let mut mgr = test_manager();
322 mgr.initiate_pairing("d1", "key1", "dev1").unwrap();
323 mgr.verify_pairing("d1").unwrap();
324 assert!(mgr.verify_pairing("d1").is_err());
325 }
326
327 #[test]
328 fn record_seen() {
329 let mut mgr = test_manager();
330 mgr.initiate_pairing("d1", "key1", "dev1").unwrap();
331 mgr.verify_pairing("d1").unwrap();
332 mgr.record_seen("d1");
333 let devs = mgr.trusted_devices();
334 assert!(devs[0].last_seen.is_some());
335 }
336
337 #[test]
338 fn pairing_state_serde() {
339 for state in [
340 PairingState::Pending,
341 PairingState::Verified,
342 PairingState::Rejected,
343 PairingState::Expired,
344 ] {
345 let json = serde_json::to_string(&state).unwrap();
346 let back: PairingState = serde_json::from_str(&json).unwrap();
347 assert_eq!(state, back);
348 }
349 }
350
351 #[test]
352 fn identity_serde() {
353 let id = test_identity();
354 let json = serde_json::to_string(&id).unwrap();
355 let back: DeviceIdentity = serde_json::from_str(&json).unwrap();
356 assert_eq!(id.device_id, back.device_id);
357 }
358
359 #[test]
362 fn identity_debug_format() {
363 let id = test_identity();
364 let dbg = format!("{:?}", id);
365 assert!(dbg.contains("DeviceIdentity"));
366 assert!(dbg.contains("device_id"));
367 assert!(dbg.contains("public_key_hex"));
368 assert!(dbg.contains("created_at"));
369 assert!(dbg.contains("device_name"));
370 assert!(dbg.contains("[REDACTED]"));
372 }
373
374 #[test]
375 fn identity_debug_without_signing_key() {
376 let mut id = test_identity();
377 id.signing_key = None;
378 let dbg = format!("{:?}", id);
379 assert!(dbg.contains("DeviceIdentity"));
380 assert!(dbg.contains("None"));
381 }
382
383 #[test]
386 fn identity_sign_succeeds() {
387 let id = test_identity();
388 let sig = id.sign(b"hello world").unwrap();
389 assert!(!sig.is_empty());
390 }
391
392 #[test]
393 fn identity_sign_without_key_fails() {
394 let mut id = test_identity();
395 id.signing_key = None;
396 let result = id.sign(b"test data");
397 assert!(result.is_err());
398 assert!(result.unwrap_err().to_string().contains("no signing key"));
399 }
400
401 #[test]
404 fn manager_identity_accessor() {
405 let mgr = test_manager();
406 let id = mgr.identity();
407 assert!(id.device_id.starts_with("dev_"));
408 }
409
410 #[test]
413 fn all_devices_includes_all_states() {
414 let mut mgr = test_manager();
415 mgr.initiate_pairing("d1", "k1", "dev1").unwrap();
416 mgr.initiate_pairing("d2", "k2", "dev2").unwrap();
417 mgr.verify_pairing("d1").unwrap();
418 mgr.reject_pairing("d2").unwrap();
419
420 let all = mgr.all_devices();
421 assert_eq!(all.len(), 2);
422 }
423}