1use chrono::{DateTime, Duration, Utc};
14use hmac::{Hmac, Mac};
15use rand::Rng;
16use serde::{Deserialize, Serialize};
17use sha2::Sha256;
18use uuid::Uuid;
19
20type HmacSha256 = Hmac<Sha256>;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DeviceIdentity {
29 pub device_id: Uuid,
30 pub device_name: String,
31 pub public_key: String,
32 pub created_at: DateTime<Utc>,
33 pub last_seen: DateTime<Utc>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PairingChallenge {
39 pub challenge_id: Uuid,
40 pub nonce: String,
41 pub expires_at: DateTime<Utc>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PairingResponse {
47 pub challenge_id: Uuid,
48 pub device_id: Uuid,
49 pub device_name: String,
50 pub public_key: String,
51 pub response_hmac: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub enum PairingResult {
57 Accepted,
58 Rejected,
59 Expired,
60}
61
62#[derive(Debug, Clone)]
64pub struct TotpVerifier {
65 secret: Vec<u8>,
66 period: u64,
68 digits: u32,
70}
71
72impl TotpVerifier {
73 pub fn new(secret: &[u8], period: u64, digits: u32) -> Self {
75 Self {
76 secret: secret.to_vec(),
77 period,
78 digits,
79 }
80 }
81
82 pub fn generate(&self) -> String {
84 self.generate_at(Utc::now().timestamp() as u64)
85 }
86
87 pub fn generate_at(&self, timestamp: u64) -> String {
89 let counter = timestamp / self.period;
90 let counter_bytes = counter.to_be_bytes();
91
92 let mut mac =
93 HmacSha256::new_from_slice(&self.secret).expect("HMAC can take key of any size");
94 mac.update(&counter_bytes);
95 let result = mac.finalize().into_bytes();
96
97 let offset = (result[result.len() - 1] & 0x0f) as usize;
99 let truncated = u32::from_be_bytes([
100 result[offset] & 0x7f,
101 result[offset + 1],
102 result[offset + 2],
103 result[offset + 3],
104 ]);
105
106 let code = truncated % 10u32.pow(self.digits);
107 format!("{:0>width$}", code, width = self.digits as usize)
108 }
109
110 pub fn verify(&self, code: &str) -> bool {
112 let now = Utc::now().timestamp() as u64;
113 for offset in [0i64, -1, 1] {
115 let ts = (now as i64 + offset * self.period as i64) as u64;
116 if self.generate_at(ts) == code {
117 return true;
118 }
119 }
120 false
121 }
122
123 pub fn verify_at(&self, code: &str, timestamp: u64) -> bool {
125 for offset in [0i64, -1, 1] {
126 let ts = (timestamp as i64 + offset * self.period as i64) as u64;
127 if self.generate_at(ts) == code {
128 return true;
129 }
130 }
131 false
132 }
133}
134
135#[derive(Debug, Clone, Default)]
142pub struct PairingManager {
143 shared_secret: Vec<u8>,
144 paired_devices: Vec<DeviceIdentity>,
145 pending_challenges: Vec<PairingChallenge>,
146 challenge_ttl_secs: i64,
148}
149
150impl PairingManager {
151 pub fn new(shared_secret: &[u8]) -> Self {
153 Self {
154 shared_secret: shared_secret.to_vec(),
155 paired_devices: Vec::new(),
156 pending_challenges: Vec::new(),
157 challenge_ttl_secs: 300, }
159 }
160
161 pub fn create_challenge(&mut self) -> PairingChallenge {
163 let mut rng = rand::thread_rng();
164 let nonce_bytes: [u8; 32] = rng.r#gen();
165 let nonce = hex::encode(nonce_bytes);
166
167 let challenge = PairingChallenge {
168 challenge_id: Uuid::new_v4(),
169 nonce,
170 expires_at: Utc::now() + Duration::seconds(self.challenge_ttl_secs),
171 };
172
173 self.pending_challenges.push(challenge.clone());
174 challenge
175 }
176
177 pub fn verify_response(&mut self, response: &PairingResponse) -> PairingResult {
179 let challenge_idx = self
181 .pending_challenges
182 .iter()
183 .position(|c| c.challenge_id == response.challenge_id);
184
185 let Some(idx) = challenge_idx else {
186 return PairingResult::Rejected;
187 };
188
189 let challenge = self.pending_challenges.remove(idx);
190
191 if Utc::now() > challenge.expires_at {
193 return PairingResult::Expired;
194 }
195
196 let expected = compute_hmac(&self.shared_secret, challenge.nonce.as_bytes());
198 if expected != response.response_hmac {
199 return PairingResult::Rejected;
200 }
201
202 let now = Utc::now();
204 self.paired_devices.push(DeviceIdentity {
205 device_id: response.device_id,
206 device_name: response.device_name.clone(),
207 public_key: response.public_key.clone(),
208 created_at: now,
209 last_seen: now,
210 });
211
212 PairingResult::Accepted
213 }
214
215 pub fn paired_devices(&self) -> &[DeviceIdentity] {
217 &self.paired_devices
218 }
219
220 pub fn revoke_device(&mut self, device_id: &Uuid) -> bool {
222 let before = self.paired_devices.len();
223 self.paired_devices.retain(|d| d.device_id != *device_id);
224 self.paired_devices.len() < before
225 }
226
227 pub fn cleanup_expired(&mut self) {
229 let now = Utc::now();
230 self.pending_challenges.retain(|c| c.expires_at > now);
231 }
232
233 pub fn pending_count(&self) -> usize {
235 self.pending_challenges.len()
236 }
237}
238
239fn compute_hmac(key: &[u8], data: &[u8]) -> String {
241 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
242 mac.update(data);
243 let result = mac.finalize().into_bytes();
244 hex::encode(result)
245}
246
247mod hex {
249 pub fn encode(bytes: impl AsRef<[u8]>) -> String {
250 bytes
251 .as_ref()
252 .iter()
253 .map(|b| format!("{:02x}", b))
254 .collect()
255 }
256}
257
258#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn test_secret() -> Vec<u8> {
267 b"test-shared-secret-key-32bytes!!".to_vec()
268 }
269
270 #[test]
273 fn test_create_challenge() {
274 let mut mgr = PairingManager::new(&test_secret());
275 let challenge = mgr.create_challenge();
276
277 assert!(!challenge.challenge_id.is_nil());
278 assert!(!challenge.nonce.is_empty());
279 assert!(challenge.expires_at > Utc::now());
280 assert_eq!(mgr.pending_count(), 1);
281 }
282
283 #[test]
284 fn test_create_multiple_challenges() {
285 let mut mgr = PairingManager::new(&test_secret());
286 mgr.create_challenge();
287 mgr.create_challenge();
288 mgr.create_challenge();
289 assert_eq!(mgr.pending_count(), 3);
290 }
291
292 #[test]
295 fn test_valid_pairing_flow() {
296 let secret = test_secret();
297 let mut mgr = PairingManager::new(&secret);
298 let challenge = mgr.create_challenge();
299
300 let response_hmac = compute_hmac(&secret, challenge.nonce.as_bytes());
302 let response = PairingResponse {
303 challenge_id: challenge.challenge_id,
304 device_id: Uuid::new_v4(),
305 device_name: "My Phone".into(),
306 public_key: "pk-abc123".into(),
307 response_hmac,
308 };
309
310 let result = mgr.verify_response(&response);
311 assert_eq!(result, PairingResult::Accepted);
312 assert_eq!(mgr.paired_devices().len(), 1);
313 assert_eq!(mgr.paired_devices()[0].device_name, "My Phone");
314 }
315
316 #[test]
317 fn test_wrong_hmac_rejected() {
318 let mut mgr = PairingManager::new(&test_secret());
319 let challenge = mgr.create_challenge();
320
321 let response = PairingResponse {
322 challenge_id: challenge.challenge_id,
323 device_id: Uuid::new_v4(),
324 device_name: "Evil Device".into(),
325 public_key: "pk-evil".into(),
326 response_hmac: "wrong-hmac".into(),
327 };
328
329 let result = mgr.verify_response(&response);
330 assert_eq!(result, PairingResult::Rejected);
331 assert!(mgr.paired_devices().is_empty());
332 }
333
334 #[test]
335 fn test_unknown_challenge_rejected() {
336 let mut mgr = PairingManager::new(&test_secret());
337
338 let response = PairingResponse {
339 challenge_id: Uuid::new_v4(), device_id: Uuid::new_v4(),
341 device_name: "Unknown".into(),
342 public_key: "pk".into(),
343 response_hmac: "whatever".into(),
344 };
345
346 let result = mgr.verify_response(&response);
347 assert_eq!(result, PairingResult::Rejected);
348 }
349
350 #[test]
351 fn test_expired_challenge() {
352 let secret = test_secret();
353 let mut mgr = PairingManager::new(&secret);
354 mgr.challenge_ttl_secs = -1; let challenge = mgr.create_challenge();
357 let response_hmac = compute_hmac(&secret, challenge.nonce.as_bytes());
358
359 let response = PairingResponse {
360 challenge_id: challenge.challenge_id,
361 device_id: Uuid::new_v4(),
362 device_name: "Late".into(),
363 public_key: "pk".into(),
364 response_hmac,
365 };
366
367 let result = mgr.verify_response(&response);
368 assert_eq!(result, PairingResult::Expired);
369 }
370
371 #[test]
374 fn test_revoke_device() {
375 let secret = test_secret();
376 let mut mgr = PairingManager::new(&secret);
377 let challenge = mgr.create_challenge();
378
379 let device_id = Uuid::new_v4();
380 let response_hmac = compute_hmac(&secret, challenge.nonce.as_bytes());
381 let response = PairingResponse {
382 challenge_id: challenge.challenge_id,
383 device_id,
384 device_name: "Phone".into(),
385 public_key: "pk".into(),
386 response_hmac,
387 };
388
389 mgr.verify_response(&response);
390 assert_eq!(mgr.paired_devices().len(), 1);
391
392 assert!(mgr.revoke_device(&device_id));
393 assert!(mgr.paired_devices().is_empty());
394 }
395
396 #[test]
397 fn test_revoke_nonexistent_device() {
398 let mut mgr = PairingManager::new(&test_secret());
399 assert!(!mgr.revoke_device(&Uuid::new_v4()));
400 }
401
402 #[test]
405 fn test_totp_generate_deterministic() {
406 let verifier = TotpVerifier::new(b"secret", 30, 6);
407 let code1 = verifier.generate_at(1000000);
408 let code2 = verifier.generate_at(1000000);
409 assert_eq!(code1, code2);
410 assert_eq!(code1.len(), 6);
411 }
412
413 #[test]
414 fn test_totp_different_timestamps_different_codes() {
415 let verifier = TotpVerifier::new(b"secret", 30, 6);
416 let code1 = verifier.generate_at(0);
417 let code2 = verifier.generate_at(30);
418 assert_ne!(code1, code2);
421 }
422
423 #[test]
424 fn test_totp_verify_at_exact() {
425 let verifier = TotpVerifier::new(b"my-key", 30, 6);
426 let ts = 1700000000u64;
427 let code = verifier.generate_at(ts);
428 assert!(verifier.verify_at(&code, ts));
429 }
430
431 #[test]
432 fn test_totp_verify_at_with_drift() {
433 let verifier = TotpVerifier::new(b"my-key", 30, 6);
434 let ts = 1700000000u64;
435 let code = verifier.generate_at(ts - 30);
437 assert!(verifier.verify_at(&code, ts));
439 }
440
441 #[test]
442 fn test_totp_reject_wrong_code() {
443 let verifier = TotpVerifier::new(b"my-key", 30, 6);
444 assert!(!verifier.verify_at("000000", 1700000000));
445 }
446
447 #[test]
450 fn test_cleanup_expired_challenges() {
451 let mut mgr = PairingManager::new(&test_secret());
452 mgr.challenge_ttl_secs = -1; mgr.create_challenge();
454 mgr.create_challenge();
455 assert_eq!(mgr.pending_count(), 2);
456
457 mgr.cleanup_expired();
458 assert_eq!(mgr.pending_count(), 0);
459 }
460
461 #[test]
464 fn test_device_identity_serialization() {
465 let now = Utc::now();
466 let device = DeviceIdentity {
467 device_id: Uuid::new_v4(),
468 device_name: "Test".into(),
469 public_key: "pk-123".into(),
470 created_at: now,
471 last_seen: now,
472 };
473
474 let json = serde_json::to_string(&device).unwrap();
475 let restored: DeviceIdentity = serde_json::from_str(&json).unwrap();
476 assert_eq!(device.device_id, restored.device_id);
477 assert_eq!(device.device_name, restored.device_name);
478 }
479
480 #[test]
481 fn test_pairing_result_serialization() {
482 let json = serde_json::to_string(&PairingResult::Accepted).unwrap();
483 let restored: PairingResult = serde_json::from_str(&json).unwrap();
484 assert_eq!(restored, PairingResult::Accepted);
485 }
486}