Skip to main content

uvb_device_binding/
lib.rs

1//! # Device Binding and Trust Management
2//!
3//! Enterprise-grade device binding to address:
4//! - **Risk #21**: Single-factor fallback on trusted devices (device trust misuse)
5//!
6//! ## Features
7//!
8//! - **Device Fingerprinting**: Browser/OS/hardware identification
9//! - **Device Registration**: MFA-protected device enrollment
10//! - **Trust Expiration**: Automatic trust expiry (30 days default)
11//! - **Periodic Re-auth**: Require MFA even on trusted devices
12//! - **Device Revocation**: User and automatic revocation
13//! - **Risk-Based Trust**: Location, IP, behavior analysis
14//! - **Sensitive Operation Blocks**: Never skip MFA for critical actions
15//! - **Device History**: Track all device registrations and usage
16
17use async_trait::async_trait;
18use chrono::{DateTime, Duration, Utc};
19use serde::{Deserialize, Serialize};
20use sha2::{Digest, Sha256};
21use std::collections::HashMap;
22use thiserror::Error;
23use tracing::{debug, info, warn};
24
25use uvb_core::{TenantId, UserId};
26
27/// Errors that can occur during device binding operations
28#[derive(Debug, Error)]
29pub enum DeviceBindingError {
30    #[error("Storage error: {0}")]
31    Storage(String),
32
33    #[error("Device not found: {0}")]
34    DeviceNotFound(String),
35
36    #[error("Device trust expired (expired at: {0})")]
37    TrustExpired(DateTime<Utc>),
38
39    #[error("Device requires re-authentication (last auth: {0})")]
40    ReauthRequired(DateTime<Utc>),
41
42    #[error("Device is revoked (reason: {0})")]
43    DeviceRevoked(String),
44
45    #[error("MFA required for device registration")]
46    MfaRequired,
47
48    #[error("Operation {0} requires MFA even on trusted devices")]
49    SensitiveOperationBlocked(String),
50
51    #[error("Risk score too high for trusted device: {score} (threshold: {threshold})")]
52    RiskTooHigh { score: u8, threshold: u8 },
53
54    #[error("Device limit reached: {current} (max: {max})")]
55    DeviceLimitReached { current: usize, max: usize },
56
57    #[error("Invalid device fingerprint")]
58    InvalidFingerprint,
59
60    #[error("Device trust not established")]
61    NotTrusted,
62}
63
64/// Device type classification
65#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
66pub enum DeviceType {
67    Desktop,
68    Mobile,
69    Tablet,
70    Unknown,
71}
72
73/// Device platform/OS
74#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
75#[allow(non_camel_case_types)]
76pub enum DevicePlatform {
77    Windows,
78    MacOS,
79    Linux,
80    iOS,
81    Android,
82    ChromeOS,
83    Unknown(String),
84}
85
86impl DevicePlatform {
87    /// Parse platform from user agent string
88    pub fn from_user_agent(user_agent: &str) -> Self {
89        let ua_lower = user_agent.to_lowercase();
90
91        if ua_lower.contains("windows") {
92            Self::Windows
93        } else if ua_lower.contains("mac os") || ua_lower.contains("macos") {
94            Self::MacOS
95        } else if ua_lower.contains("linux") && !ua_lower.contains("android") {
96            Self::Linux
97        } else if ua_lower.contains("iphone") || ua_lower.contains("ipad") {
98            Self::iOS
99        } else if ua_lower.contains("android") {
100            Self::Android
101        } else if ua_lower.contains("cros") {
102            Self::ChromeOS
103        } else {
104            Self::Unknown(user_agent.to_string())
105        }
106    }
107}
108
109/// Browser identification
110#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
111pub enum BrowserType {
112    Chrome,
113    Firefox,
114    Safari,
115    Edge,
116    Opera,
117    Brave,
118    Unknown(String),
119}
120
121impl BrowserType {
122    /// Parse browser from user agent string
123    pub fn from_user_agent(user_agent: &str) -> Self {
124        let ua_lower = user_agent.to_lowercase();
125
126        if ua_lower.contains("edg/") || ua_lower.contains("edge/") {
127            Self::Edge
128        } else if ua_lower.contains("brave") {
129            Self::Brave
130        } else if ua_lower.contains("opr/") || ua_lower.contains("opera") {
131            Self::Opera
132        } else if ua_lower.contains("chrome") || ua_lower.contains("crios") {
133            Self::Chrome
134        } else if ua_lower.contains("firefox") || ua_lower.contains("fxios") {
135            Self::Firefox
136        } else if ua_lower.contains("safari") {
137            Self::Safari
138        } else {
139            Self::Unknown(user_agent.to_string())
140        }
141    }
142}
143
144/// Device fingerprint for identification
145#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
146pub struct DeviceFingerprint {
147    /// User agent string
148    pub user_agent: String,
149
150    /// Device platform/OS
151    pub platform: DevicePlatform,
152
153    /// Browser type
154    pub browser: BrowserType,
155
156    /// Screen resolution (e.g., "1920x1080")
157    pub screen_resolution: Option<String>,
158
159    /// Timezone offset in minutes
160    pub timezone_offset: Option<i32>,
161
162    /// Browser language
163    pub language: Option<String>,
164
165    /// Hardware concurrency (CPU cores)
166    pub hardware_concurrency: Option<u32>,
167
168    /// WebGL vendor
169    pub webgl_vendor: Option<String>,
170
171    /// WebGL renderer
172    pub webgl_renderer: Option<String>,
173
174    /// Canvas fingerprint hash
175    pub canvas_hash: Option<String>,
176
177    /// Installed fonts hash
178    pub fonts_hash: Option<String>,
179
180    /// Browser plugins
181    pub plugins: Vec<String>,
182
183    /// Touch support
184    pub touch_support: bool,
185
186    /// Color depth
187    pub color_depth: Option<u8>,
188}
189
190impl DeviceFingerprint {
191    /// Generate a unique device ID from fingerprint
192    pub fn generate_device_id(&self) -> String {
193        let mut hasher = Sha256::new();
194
195        // Combine stable attributes
196        hasher.update(self.user_agent.as_bytes());
197        if let Some(ref res) = self.screen_resolution {
198            hasher.update(res.as_bytes());
199        }
200        if let Some(ref webgl_vendor) = self.webgl_vendor {
201            hasher.update(webgl_vendor.as_bytes());
202        }
203        if let Some(ref webgl_renderer) = self.webgl_renderer {
204            hasher.update(webgl_renderer.as_bytes());
205        }
206        if let Some(ref canvas) = self.canvas_hash {
207            hasher.update(canvas.as_bytes());
208        }
209        if let Some(ref fonts) = self.fonts_hash {
210            hasher.update(fonts.as_bytes());
211        }
212
213        hex::encode(hasher.finalize())
214    }
215
216    /// Calculate similarity score with another fingerprint (0.0 to 1.0)
217    pub fn similarity(&self, other: &DeviceFingerprint) -> f64 {
218        let mut matches = 0;
219        let mut total = 0;
220
221        // User agent
222        total += 1;
223        if self.user_agent == other.user_agent {
224            matches += 1;
225        }
226
227        // Screen resolution (important for desktop devices)
228        if self.screen_resolution.is_some() && other.screen_resolution.is_some() {
229            total += 1;
230            if self.screen_resolution == other.screen_resolution {
231                matches += 1;
232            }
233        }
234
235        // WebGL (stable attributes)
236        if self.webgl_vendor.is_some() && other.webgl_vendor.is_some() {
237            total += 1;
238            if self.webgl_vendor == other.webgl_vendor {
239                matches += 1;
240            }
241        }
242
243        if self.webgl_renderer.is_some() && other.webgl_renderer.is_some() {
244            total += 1;
245            if self.webgl_renderer == other.webgl_renderer {
246                matches += 1;
247            }
248        }
249
250        // Canvas hash (unique per device)
251        if self.canvas_hash.is_some() && other.canvas_hash.is_some() {
252            total += 2; // Weight canvas more heavily
253            if self.canvas_hash == other.canvas_hash {
254                matches += 2;
255            }
256        }
257
258        // Fonts hash
259        if self.fonts_hash.is_some() && other.fonts_hash.is_some() {
260            total += 1;
261            if self.fonts_hash == other.fonts_hash {
262                matches += 1;
263            }
264        }
265
266        // Hardware concurrency
267        if self.hardware_concurrency.is_some() && other.hardware_concurrency.is_some() {
268            total += 1;
269            if self.hardware_concurrency == other.hardware_concurrency {
270                matches += 1;
271            }
272        }
273
274        if total == 0 {
275            return 0.0;
276        }
277
278        matches as f64 / total as f64
279    }
280}
281
282/// Trusted device information
283#[derive(Clone, Debug, Serialize, Deserialize)]
284pub struct TrustedDevice {
285    /// Unique device ID
286    pub device_id: String,
287
288    /// User ID this device belongs to
289    pub user_id: UserId,
290
291    /// Tenant ID
292    pub tenant_id: TenantId,
293
294    /// Device fingerprint
295    pub fingerprint: DeviceFingerprint,
296
297    /// Device type
298    pub device_type: DeviceType,
299
300    /// User-provided device name (optional)
301    pub device_name: Option<String>,
302
303    /// When device was registered
304    pub registered_at: DateTime<Utc>,
305
306    /// When trust expires
307    pub expires_at: DateTime<Utc>,
308
309    /// Last authentication time
310    pub last_auth_at: DateTime<Utc>,
311
312    /// Last seen IP address
313    pub last_ip: Option<String>,
314
315    /// Last seen location (city, country)
316    pub last_location: Option<String>,
317
318    /// Is device currently trusted
319    pub is_trusted: bool,
320
321    /// Revocation reason (if revoked)
322    pub revoked_reason: Option<String>,
323
324    /// Revoked timestamp
325    pub revoked_at: Option<DateTime<Utc>>,
326
327    /// Number of times device was used
328    pub usage_count: u64,
329
330    /// Risk score (0-100)
331    pub risk_score: u8,
332}
333
334impl TrustedDevice {
335    /// Check if device trust is expired
336    pub fn is_expired(&self) -> bool {
337        Utc::now() > self.expires_at
338    }
339
340    /// Check if device requires re-authentication
341    pub fn requires_reauth(&self, reauth_interval: Duration) -> bool {
342        let next_auth_time = self.last_auth_at + reauth_interval;
343        Utc::now() > next_auth_time
344    }
345
346    /// Check if device is revoked
347    pub fn is_revoked(&self) -> bool {
348        self.revoked_reason.is_some()
349    }
350
351    /// Check if device is valid for use
352    pub fn is_valid(&self) -> Result<(), DeviceBindingError> {
353        if self.is_revoked() {
354            return Err(DeviceBindingError::DeviceRevoked(
355                self.revoked_reason.clone().unwrap_or_default(),
356            ));
357        }
358
359        if self.is_expired() {
360            return Err(DeviceBindingError::TrustExpired(self.expires_at));
361        }
362
363        if !self.is_trusted {
364            return Err(DeviceBindingError::NotTrusted);
365        }
366
367        Ok(())
368    }
369
370    /// Get device age in days
371    pub fn age_days(&self) -> i64 {
372        (Utc::now() - self.registered_at).num_days()
373    }
374}
375
376/// Device binding configuration
377#[derive(Clone, Debug, Serialize, Deserialize)]
378pub struct DeviceBindingConfig {
379    /// Enable device binding
380    pub enabled: bool,
381
382    /// Trust expiration (days)
383    pub trust_expiration_days: i64,
384
385    /// Require re-authentication interval (days)
386    pub reauth_interval_days: i64,
387
388    /// Maximum devices per user
389    pub max_devices_per_user: usize,
390
391    /// Risk score threshold for trust (0-100)
392    pub risk_threshold: u8,
393
394    /// Require MFA for device registration
395    pub require_mfa_for_registration: bool,
396
397    /// Automatically revoke devices on suspicious activity
398    pub auto_revoke_on_suspicion: bool,
399
400    /// Operations that always require MFA (even on trusted devices)
401    pub sensitive_operations: Vec<String>,
402
403    /// Minimum fingerprint similarity for device recognition (0.0-1.0)
404    pub min_fingerprint_similarity: f64,
405
406    /// Enable location-based risk scoring
407    pub enable_location_risk: bool,
408
409    /// Enable IP-based risk scoring
410    pub enable_ip_risk: bool,
411}
412
413impl DeviceBindingConfig {
414    /// Create default configuration
415    pub fn new_default() -> Self {
416        Self {
417            enabled: true,
418            trust_expiration_days: 30,
419            reauth_interval_days: 7,
420            max_devices_per_user: 10,
421            risk_threshold: 70,
422            require_mfa_for_registration: true,
423            auto_revoke_on_suspicion: true,
424            sensitive_operations: vec![
425                "change_password".to_string(),
426                "change_email".to_string(),
427                "change_phone".to_string(),
428                "add_payment_method".to_string(),
429                "delete_account".to_string(),
430                "change_mfa_settings".to_string(),
431                "transfer_funds".to_string(),
432            ],
433            min_fingerprint_similarity: 0.8,
434            enable_location_risk: true,
435            enable_ip_risk: true,
436        }
437    }
438
439    /// Create strict configuration
440    pub fn strict() -> Self {
441        let mut config = Self::new_default();
442        config.trust_expiration_days = 14; // 2 weeks
443        config.reauth_interval_days = 3; // Every 3 days
444        config.max_devices_per_user = 5;
445        config.risk_threshold = 50; // Stricter
446        config.min_fingerprint_similarity = 0.9; // More strict matching
447        config
448    }
449
450    /// Create lenient configuration (development)
451    pub fn lenient() -> Self {
452        let mut config = Self::new_default();
453        config.trust_expiration_days = 365; // 1 year
454        config.reauth_interval_days = 30;
455        config.max_devices_per_user = 50;
456        config.risk_threshold = 90; // Very lenient
457        config.require_mfa_for_registration = false;
458        config.auto_revoke_on_suspicion = false;
459        config.sensitive_operations = vec![]; // Allow everything
460        config.min_fingerprint_similarity = 0.5;
461        config.enable_location_risk = false;
462        config.enable_ip_risk = false;
463        config
464    }
465
466    /// Check if operation is sensitive
467    pub fn is_sensitive_operation(&self, operation: &str) -> bool {
468        self.sensitive_operations.iter().any(|op| op == operation)
469    }
470}
471
472/// Device registration request
473#[derive(Clone, Debug, Serialize, Deserialize)]
474pub struct DeviceRegistrationRequest {
475    pub user_id: UserId,
476    pub tenant_id: TenantId,
477    pub fingerprint: DeviceFingerprint,
478    pub device_type: DeviceType,
479    pub device_name: Option<String>,
480    pub ip_address: Option<String>,
481    pub location: Option<String>,
482    pub mfa_verified: bool,
483}
484
485/// Device trust validation result
486#[derive(Clone, Debug, Serialize, Deserialize)]
487pub struct DeviceTrustResult {
488    pub device_id: String,
489    pub is_trusted: bool,
490    pub requires_mfa: bool,
491    pub reason: String,
492    pub risk_score: u8,
493    pub expires_at: Option<DateTime<Utc>>,
494}
495
496/// Storage trait for device binding
497#[async_trait]
498pub trait DeviceBindingStorage: Send + Sync {
499    /// Save a trusted device
500    async fn save_device(&self, device: &TrustedDevice) -> Result<(), DeviceBindingError>;
501
502    /// Get device by ID
503    async fn get_device(&self, device_id: &str) -> Result<TrustedDevice, DeviceBindingError>;
504
505    /// Get all devices for a user
506    async fn get_user_devices(
507        &self,
508        user_id: &UserId,
509    ) -> Result<Vec<TrustedDevice>, DeviceBindingError>;
510
511    /// Update device last authentication time
512    async fn update_last_auth(
513        &self,
514        device_id: &str,
515        timestamp: DateTime<Utc>,
516    ) -> Result<(), DeviceBindingError>;
517
518    /// Revoke device
519    async fn revoke_device(
520        &self,
521        device_id: &str,
522        reason: String,
523        revoked_at: DateTime<Utc>,
524    ) -> Result<(), DeviceBindingError>;
525
526    /// Delete device
527    async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError>;
528
529    /// Find devices by fingerprint similarity
530    async fn find_similar_devices(
531        &self,
532        user_id: &UserId,
533        fingerprint: &DeviceFingerprint,
534        min_similarity: f64,
535    ) -> Result<Vec<TrustedDevice>, DeviceBindingError>;
536}
537
538/// In-memory storage for testing
539pub struct InMemoryDeviceStorage {
540    devices: tokio::sync::RwLock<HashMap<String, TrustedDevice>>,
541}
542
543impl InMemoryDeviceStorage {
544    pub fn new() -> Self {
545        Self {
546            devices: tokio::sync::RwLock::new(HashMap::new()),
547        }
548    }
549}
550
551impl Default for InMemoryDeviceStorage {
552    fn default() -> Self {
553        Self::new()
554    }
555}
556
557#[async_trait]
558impl DeviceBindingStorage for InMemoryDeviceStorage {
559    async fn save_device(&self, device: &TrustedDevice) -> Result<(), DeviceBindingError> {
560        let mut devices = self.devices.write().await;
561        devices.insert(device.device_id.clone(), device.clone());
562        Ok(())
563    }
564
565    async fn get_device(&self, device_id: &str) -> Result<TrustedDevice, DeviceBindingError> {
566        let devices = self.devices.read().await;
567        devices
568            .get(device_id)
569            .cloned()
570            .ok_or_else(|| DeviceBindingError::DeviceNotFound(device_id.to_string()))
571    }
572
573    async fn get_user_devices(
574        &self,
575        user_id: &UserId,
576    ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
577        let devices = self.devices.read().await;
578        Ok(devices
579            .values()
580            .filter(|d| d.user_id == *user_id)
581            .cloned()
582            .collect())
583    }
584
585    async fn update_last_auth(
586        &self,
587        device_id: &str,
588        timestamp: DateTime<Utc>,
589    ) -> Result<(), DeviceBindingError> {
590        let mut devices = self.devices.write().await;
591        if let Some(device) = devices.get_mut(device_id) {
592            device.last_auth_at = timestamp;
593            device.usage_count += 1;
594            Ok(())
595        } else {
596            Err(DeviceBindingError::DeviceNotFound(device_id.to_string()))
597        }
598    }
599
600    async fn revoke_device(
601        &self,
602        device_id: &str,
603        reason: String,
604        revoked_at: DateTime<Utc>,
605    ) -> Result<(), DeviceBindingError> {
606        let mut devices = self.devices.write().await;
607        if let Some(device) = devices.get_mut(device_id) {
608            device.is_trusted = false;
609            device.revoked_reason = Some(reason);
610            device.revoked_at = Some(revoked_at);
611            Ok(())
612        } else {
613            Err(DeviceBindingError::DeviceNotFound(device_id.to_string()))
614        }
615    }
616
617    async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError> {
618        let mut devices = self.devices.write().await;
619        devices
620            .remove(device_id)
621            .ok_or_else(|| DeviceBindingError::DeviceNotFound(device_id.to_string()))?;
622        Ok(())
623    }
624
625    async fn find_similar_devices(
626        &self,
627        user_id: &UserId,
628        fingerprint: &DeviceFingerprint,
629        min_similarity: f64,
630    ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
631        let devices = self.devices.read().await;
632        Ok(devices
633            .values()
634            .filter(|d| {
635                d.user_id == *user_id && fingerprint.similarity(&d.fingerprint) >= min_similarity
636            })
637            .cloned()
638            .collect())
639    }
640}
641
642/// Device binding manager
643pub struct DeviceBindingManager<S: DeviceBindingStorage> {
644    storage: S,
645    config: DeviceBindingConfig,
646}
647
648impl<S: DeviceBindingStorage> DeviceBindingManager<S> {
649    /// Create a new manager with configuration
650    pub fn new(storage: S, config: DeviceBindingConfig) -> Self {
651        Self { storage, config }
652    }
653
654    /// Register a new device
655    pub async fn register_device(
656        &self,
657        request: DeviceRegistrationRequest,
658    ) -> Result<TrustedDevice, DeviceBindingError> {
659        if !self.config.enabled {
660            return Err(DeviceBindingError::Storage(
661                "Device binding disabled".to_string(),
662            ));
663        }
664
665        // Verify MFA if required
666        if self.config.require_mfa_for_registration && !request.mfa_verified {
667            return Err(DeviceBindingError::MfaRequired);
668        }
669
670        // Check device limit
671        let existing_devices = self.storage.get_user_devices(&request.user_id).await?;
672        if existing_devices.len() >= self.config.max_devices_per_user {
673            return Err(DeviceBindingError::DeviceLimitReached {
674                current: existing_devices.len(),
675                max: self.config.max_devices_per_user,
676            });
677        }
678
679        // Generate device ID
680        let device_id = request.fingerprint.generate_device_id();
681
682        // Calculate expiration
683        let expires_at = Utc::now() + Duration::days(self.config.trust_expiration_days);
684
685        let device = TrustedDevice {
686            device_id: device_id.clone(),
687            user_id: request.user_id,
688            tenant_id: request.tenant_id,
689            fingerprint: request.fingerprint,
690            device_type: request.device_type,
691            device_name: request.device_name,
692            registered_at: Utc::now(),
693            expires_at,
694            last_auth_at: Utc::now(),
695            last_ip: request.ip_address,
696            last_location: request.location,
697            is_trusted: true,
698            revoked_reason: None,
699            revoked_at: None,
700            usage_count: 0,
701            risk_score: 0, // Initial risk is 0
702        };
703
704        self.storage.save_device(&device).await?;
705
706        info!(
707            "Registered new device {} for user {:?}",
708            device_id, device.user_id
709        );
710
711        Ok(device)
712    }
713
714    /// Validate device trust
715    pub async fn validate_device_trust(
716        &self,
717        device_id: &str,
718        operation: &str,
719    ) -> Result<DeviceTrustResult, DeviceBindingError> {
720        if !self.config.enabled {
721            return Ok(DeviceTrustResult {
722                device_id: device_id.to_string(),
723                is_trusted: false,
724                requires_mfa: true,
725                reason: "Device binding disabled".to_string(),
726                risk_score: 0,
727                expires_at: None,
728            });
729        }
730
731        // Sensitive operations always require MFA
732        if self.config.is_sensitive_operation(operation) {
733            return Ok(DeviceTrustResult {
734                device_id: device_id.to_string(),
735                is_trusted: false,
736                requires_mfa: true,
737                reason: format!("Sensitive operation '{}' requires MFA", operation),
738                risk_score: 100,
739                expires_at: None,
740            });
741        }
742
743        let device = self.storage.get_device(device_id).await?;
744
745        // Check if device is valid
746        if let Err(e) = device.is_valid() {
747            return Ok(DeviceTrustResult {
748                device_id: device_id.to_string(),
749                is_trusted: false,
750                requires_mfa: true,
751                reason: e.to_string(),
752                risk_score: 100,
753                expires_at: Some(device.expires_at),
754            });
755        }
756
757        // Check if re-authentication is required
758        let reauth_interval = Duration::days(self.config.reauth_interval_days);
759        if device.requires_reauth(reauth_interval) {
760            return Ok(DeviceTrustResult {
761                device_id: device_id.to_string(),
762                is_trusted: true, // Device is trusted, but needs reauth
763                requires_mfa: true,
764                reason: "Periodic re-authentication required".to_string(),
765                risk_score: device.risk_score,
766                expires_at: Some(device.expires_at),
767            });
768        }
769
770        // Check risk score
771        if device.risk_score > self.config.risk_threshold {
772            warn!(
773                "Device {} risk score {} exceeds threshold {}",
774                device_id, device.risk_score, self.config.risk_threshold
775            );
776
777            if self.config.auto_revoke_on_suspicion {
778                self.storage
779                    .revoke_device(
780                        device_id,
781                        format!(
782                            "Automatic revocation due to high risk score: {}",
783                            device.risk_score
784                        ),
785                        Utc::now(),
786                    )
787                    .await?;
788
789                return Ok(DeviceTrustResult {
790                    device_id: device_id.to_string(),
791                    is_trusted: false,
792                    requires_mfa: true,
793                    reason: "Device revoked due to high risk".to_string(),
794                    risk_score: device.risk_score,
795                    expires_at: None,
796                });
797            }
798
799            return Ok(DeviceTrustResult {
800                device_id: device_id.to_string(),
801                is_trusted: false,
802                requires_mfa: true,
803                reason: format!(
804                    "Risk score {} exceeds threshold {}",
805                    device.risk_score, self.config.risk_threshold
806                ),
807                risk_score: device.risk_score,
808                expires_at: Some(device.expires_at),
809            });
810        }
811
812        // Device is trusted
813        Ok(DeviceTrustResult {
814            device_id: device_id.to_string(),
815            is_trusted: true,
816            requires_mfa: false,
817            reason: "Device is trusted".to_string(),
818            risk_score: device.risk_score,
819            expires_at: Some(device.expires_at),
820        })
821    }
822
823    /// Update device authentication time
824    pub async fn record_authentication(&self, device_id: &str) -> Result<(), DeviceBindingError> {
825        self.storage.update_last_auth(device_id, Utc::now()).await?;
826        debug!("Recorded authentication for device {}", device_id);
827        Ok(())
828    }
829
830    /// Revoke a device
831    pub async fn revoke_device(
832        &self,
833        device_id: &str,
834        reason: String,
835    ) -> Result<(), DeviceBindingError> {
836        self.storage
837            .revoke_device(device_id, reason.clone(), Utc::now())
838            .await?;
839        info!("Revoked device {}: {}", device_id, reason);
840        Ok(())
841    }
842
843    /// Delete a device
844    pub async fn delete_device(&self, device_id: &str) -> Result<(), DeviceBindingError> {
845        self.storage.delete_device(device_id).await?;
846        info!("Deleted device {}", device_id);
847        Ok(())
848    }
849
850    /// Get all devices for a user
851    pub async fn get_user_devices(
852        &self,
853        user_id: &UserId,
854    ) -> Result<Vec<TrustedDevice>, DeviceBindingError> {
855        self.storage.get_user_devices(user_id).await
856    }
857
858    /// Find existing device by fingerprint
859    pub async fn find_device_by_fingerprint(
860        &self,
861        user_id: &UserId,
862        fingerprint: &DeviceFingerprint,
863    ) -> Result<Option<TrustedDevice>, DeviceBindingError> {
864        let similar = self
865            .storage
866            .find_similar_devices(user_id, fingerprint, self.config.min_fingerprint_similarity)
867            .await?;
868
869        Ok(similar.into_iter().next())
870    }
871
872    /// Cleanup expired devices
873    pub async fn cleanup_expired_devices(
874        &self,
875        user_id: &UserId,
876    ) -> Result<usize, DeviceBindingError> {
877        let devices = self.storage.get_user_devices(user_id).await?;
878        let mut removed = 0;
879
880        for device in devices {
881            if device.is_expired() {
882                self.storage.delete_device(&device.device_id).await?;
883                removed += 1;
884            }
885        }
886
887        if removed > 0 {
888            info!(
889                "Cleaned up {} expired devices for user {:?}",
890                removed, user_id
891            );
892        }
893
894        Ok(removed)
895    }
896}
897
898#[cfg(test)]
899mod tests {
900    use super::*;
901
902    fn create_test_fingerprint() -> DeviceFingerprint {
903        DeviceFingerprint {
904            user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)".to_string(),
905            platform: DevicePlatform::MacOS,
906            browser: BrowserType::Chrome,
907            screen_resolution: Some("1920x1080".to_string()),
908            timezone_offset: Some(-480),
909            language: Some("en-US".to_string()),
910            hardware_concurrency: Some(8),
911            webgl_vendor: Some("Intel Inc.".to_string()),
912            webgl_renderer: Some("Intel Iris Pro".to_string()),
913            canvas_hash: Some("abc123".to_string()),
914            fonts_hash: Some("def456".to_string()),
915            plugins: vec![],
916            touch_support: false,
917            color_depth: Some(24),
918        }
919    }
920
921    #[test]
922    fn test_device_id_generation() {
923        let fp = create_test_fingerprint();
924        let id1 = fp.generate_device_id();
925        let id2 = fp.generate_device_id();
926
927        // Same fingerprint should generate same ID
928        assert_eq!(id1, id2);
929        assert_eq!(id1.len(), 64); // SHA256 hex
930    }
931
932    #[test]
933    fn test_fingerprint_similarity() {
934        let fp1 = create_test_fingerprint();
935        let mut fp2 = fp1.clone();
936
937        // Identical fingerprints
938        assert_eq!(fp1.similarity(&fp2), 1.0);
939
940        // Change one attribute
941        fp2.user_agent = "Different".to_string();
942        assert!(fp1.similarity(&fp2) < 1.0);
943    }
944
945    #[test]
946    fn test_platform_detection() {
947        assert_eq!(
948            DevicePlatform::from_user_agent("Mozilla/5.0 (Windows NT 10.0)"),
949            DevicePlatform::Windows
950        );
951        assert_eq!(
952            DevicePlatform::from_user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"),
953            DevicePlatform::MacOS
954        );
955        assert_eq!(
956            DevicePlatform::from_user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 14_0)"),
957            DevicePlatform::iOS
958        );
959    }
960
961    #[test]
962    fn test_browser_detection() {
963        assert_eq!(
964            BrowserType::from_user_agent("Chrome/91.0.4472.124"),
965            BrowserType::Chrome
966        );
967        assert_eq!(
968            BrowserType::from_user_agent("Firefox/89.0"),
969            BrowserType::Firefox
970        );
971        assert_eq!(
972            BrowserType::from_user_agent("Edg/91.0.864.59"),
973            BrowserType::Edge
974        );
975    }
976
977    #[test]
978    fn test_config_presets() {
979        let default_config = DeviceBindingConfig::new_default();
980        assert_eq!(default_config.trust_expiration_days, 30);
981        assert!(default_config.require_mfa_for_registration);
982
983        let strict_config = DeviceBindingConfig::strict();
984        assert_eq!(strict_config.trust_expiration_days, 14);
985        assert_eq!(strict_config.reauth_interval_days, 3);
986
987        let lenient_config = DeviceBindingConfig::lenient();
988        assert_eq!(lenient_config.trust_expiration_days, 365);
989        assert!(!lenient_config.require_mfa_for_registration);
990    }
991
992    #[test]
993    fn test_sensitive_operations() {
994        let config = DeviceBindingConfig::new_default();
995        assert!(config.is_sensitive_operation("change_password"));
996        assert!(config.is_sensitive_operation("delete_account"));
997        assert!(!config.is_sensitive_operation("view_profile"));
998    }
999
1000    #[tokio::test]
1001    async fn test_device_registration() {
1002        let storage = InMemoryDeviceStorage::new();
1003        let config = DeviceBindingConfig::new_default();
1004        let manager = DeviceBindingManager::new(storage, config);
1005
1006        let user_id = UserId::new("test_user");
1007        let tenant_id = TenantId::new("test_tenant");
1008        let fingerprint = create_test_fingerprint();
1009
1010        let request = DeviceRegistrationRequest {
1011            user_id: user_id.clone(),
1012            tenant_id,
1013            fingerprint,
1014            device_type: DeviceType::Desktop,
1015            device_name: Some("My Laptop".to_string()),
1016            ip_address: Some("192.0.2.1".to_string()),
1017            location: Some("San Francisco, CA".to_string()),
1018            mfa_verified: true,
1019        };
1020
1021        let device = manager.register_device(request).await.unwrap();
1022        assert_eq!(device.user_id, user_id);
1023        assert!(device.is_trusted);
1024        assert!(!device.is_expired());
1025    }
1026
1027    #[tokio::test]
1028    async fn test_device_limit_enforcement() {
1029        let storage = InMemoryDeviceStorage::new();
1030        let mut config = DeviceBindingConfig::new_default();
1031        config.max_devices_per_user = 2;
1032        let manager = DeviceBindingManager::new(storage, config);
1033
1034        let user_id = UserId::new("test_user");
1035
1036        // Register 2 devices (should succeed)
1037        for i in 0..2 {
1038            let mut fp = create_test_fingerprint();
1039            fp.user_agent = format!("Device {}", i);
1040
1041            let request = DeviceRegistrationRequest {
1042                user_id: user_id.clone(),
1043                tenant_id: TenantId::new("test_tenant"),
1044                fingerprint: fp,
1045                device_type: DeviceType::Desktop,
1046                device_name: Some(format!("Device {}", i)),
1047                ip_address: None,
1048                location: None,
1049                mfa_verified: true,
1050            };
1051
1052            manager.register_device(request).await.unwrap();
1053        }
1054
1055        // Third device should fail
1056        let mut fp = create_test_fingerprint();
1057        fp.user_agent = "Device 3".to_string();
1058
1059        let request = DeviceRegistrationRequest {
1060            user_id,
1061            tenant_id: TenantId::new("test_tenant"),
1062            fingerprint: fp,
1063            device_type: DeviceType::Desktop,
1064            device_name: Some("Device 3".to_string()),
1065            ip_address: None,
1066            location: None,
1067            mfa_verified: true,
1068        };
1069
1070        let result = manager.register_device(request).await;
1071        assert!(matches!(
1072            result,
1073            Err(DeviceBindingError::DeviceLimitReached { .. })
1074        ));
1075    }
1076
1077    #[tokio::test]
1078    async fn test_sensitive_operation_blocks() {
1079        let storage = InMemoryDeviceStorage::new();
1080        let config = DeviceBindingConfig::new_default();
1081        let manager = DeviceBindingManager::new(storage, config);
1082
1083        let user_id = UserId::new("test_user");
1084        let fingerprint = create_test_fingerprint();
1085
1086        let request = DeviceRegistrationRequest {
1087            user_id,
1088            tenant_id: TenantId::new("test_tenant"),
1089            fingerprint,
1090            device_type: DeviceType::Desktop,
1091            device_name: Some("Test Device".to_string()),
1092            ip_address: None,
1093            location: None,
1094            mfa_verified: true,
1095        };
1096
1097        let device = manager.register_device(request).await.unwrap();
1098
1099        // Sensitive operation should require MFA even on trusted device
1100        let result = manager
1101            .validate_device_trust(&device.device_id, "change_password")
1102            .await
1103            .unwrap();
1104
1105        assert!(result.requires_mfa);
1106        assert!(result.reason.contains("Sensitive operation"));
1107    }
1108}