auth.rs
1 /// HMAC-based authentication for secure device-to-server communication 2 use anyhow::{Context, Result}; 3 use hmac::{Hmac, Mac}; 4 use sha2::Sha256; 5 use std::time::{SystemTime, UNIX_EPOCH}; 6 7 type HmacSha256 = Hmac<Sha256>; 8 9 /// Generate HMAC signature for authenticated requests 10 /// 11 /// Signature format: HMAC-SHA256(secret, "device_id|timestamp|endpoint|body_hash") 12 /// This prevents replay attacks and ensures request integrity 13 pub fn generate_signature( 14 device_id: &str, 15 device_secret: &str, 16 timestamp: u64, 17 endpoint: &str, 18 body: &[u8], 19 ) -> Result<String> { 20 // Hash the request body 21 use sha2::Digest; 22 let mut hasher = Sha256::new(); 23 hasher.update(body); 24 let body_hash = hasher.finalize(); 25 let body_hash_hex = hex::encode(body_hash); 26 27 // Create message to sign: device_id|timestamp|endpoint|body_hash 28 let message = format!("{}|{}|{}|{}", device_id, timestamp, endpoint, body_hash_hex); 29 30 // Generate HMAC signature 31 let mut mac = HmacSha256::new_from_slice(device_secret.as_bytes()) 32 .map_err(|_| anyhow::anyhow!("Invalid secret key length"))?; 33 mac.update(message.as_bytes()); 34 let signature = mac.finalize(); 35 let signature_bytes = signature.into_bytes(); 36 37 // Return hex-encoded signature 38 Ok(hex::encode(signature_bytes)) 39 } 40 41 /// Get current Unix timestamp in seconds 42 pub fn get_timestamp() -> u64 { 43 SystemTime::now() 44 .duration_since(UNIX_EPOCH) 45 .unwrap_or_default() 46 .as_secs() 47 } 48 49 /// Generate authentication headers for HTTP requests 50 /// 51 /// Returns a vector of (header_name, header_value) tuples to add to the request 52 pub fn generate_auth_headers( 53 device_id: &str, 54 device_secret: &str, 55 endpoint: &str, 56 body: &[u8], 57 ) -> Result<Vec<(&'static str, String)>> { 58 let timestamp = get_timestamp(); 59 let signature = generate_signature(device_id, device_secret, timestamp, endpoint, body)?; 60 61 Ok(vec![ 62 ("X-Device-ID", device_id.to_string()), 63 ("X-Timestamp", timestamp.to_string()), 64 ("Authorization", format!("HMAC {}", signature)), 65 ]) 66 } 67 68 /// Verify HMAC signature (for server-side validation or testing) 69 /// 70 /// Server should: 71 /// 1. Check timestamp is within acceptable window (e.g., ±5 minutes) 72 /// 2. Verify signature matches using stored secret for device_id 73 /// 3. Validate body hash matches actual received body 74 #[allow(dead_code)] 75 pub fn verify_signature( 76 device_id: &str, 77 device_secret: &str, 78 timestamp: u64, 79 endpoint: &str, 80 body: &[u8], 81 provided_signature: &str, 82 ) -> Result<bool> { 83 // Check timestamp window (5 minute tolerance) 84 let now = get_timestamp(); 85 let time_diff = if now > timestamp { 86 now - timestamp 87 } else { 88 timestamp - now 89 }; 90 91 if time_diff > 300 { 92 // 5 minutes 93 anyhow::bail!("Timestamp outside acceptable window"); 94 } 95 96 // Generate expected signature 97 let expected_signature = 98 generate_signature(device_id, device_secret, timestamp, endpoint, body)?; 99 100 // Constant-time comparison to prevent timing attacks 101 Ok(expected_signature == provided_signature) 102 } 103 104 /// Generate a cryptographically random device secret 105 /// 106 /// Used during manufacturing/provisioning to create unique device credentials 107 pub fn generate_device_secret() -> String { 108 let mut secret_bytes = [0u8; 32]; // 256-bit secret 109 unsafe { 110 esp_idf_svc::sys::esp_fill_random(secret_bytes.as_mut_ptr() as *mut _, 32); 111 } 112 hex::encode(secret_bytes) 113 } 114 115 /// Register device with remote server on first boot 116 /// 117 /// Sends device_id and device_secret to server's registration endpoint 118 /// Returns Ok(true) if registration successful, Ok(false) if already registered 119 pub fn register_with_server( 120 registration_url: &str, 121 device_id: &str, 122 device_secret: &str, 123 ) -> Result<bool> { 124 use embedded_svc::io::Write; 125 use esp_idf_svc::http::client::{Configuration as HttpConfig, EspHttpConnection}; 126 use log::{info, warn}; 127 128 if registration_url.is_empty() { 129 anyhow::bail!("Registration URL is empty"); 130 } 131 132 info!("Attempting to register device {} with server...", device_id); 133 134 // Create registration JSON payload 135 let payload = format!( 136 r#"{{"device_id":"{}","device_secret":"{}"}}"#, 137 device_id, device_secret 138 ); 139 140 // Create HTTP client 141 let connection = EspHttpConnection::new(&HttpConfig { 142 use_global_ca_store: true, 143 crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach), 144 timeout: Some(std::time::Duration::from_secs(30)), 145 ..Default::default() 146 }) 147 .context("Failed to create HTTP connection")?; 148 149 let mut client = embedded_svc::http::client::Client::wrap(connection); 150 151 // Prepare POST request 152 let content_len = payload.len(); 153 let headers = [ 154 ("Content-Type", "application/json"), 155 ("Content-Length", &content_len.to_string()), 156 ]; 157 158 // Send POST request 159 let mut request = client 160 .post(registration_url, &headers) 161 .context("Failed to create POST request")?; 162 163 request 164 .write_all(payload.as_bytes()) 165 .context("Failed to write payload")?; 166 167 request.flush().context("Failed to flush request")?; 168 169 // Read response 170 let mut response_buf = [0u8; 512]; 171 let mut response = request.submit().context("Failed to submit request")?; 172 173 let status = response.status(); 174 let len = embedded_svc::io::Read::read(&mut response, &mut response_buf).unwrap_or(0); 175 176 match status { 177 200 | 201 => { 178 info!("✓ Device registered successfully with server!"); 179 info!( 180 " Response: {}", 181 String::from_utf8_lossy(&response_buf[..len]) 182 ); 183 Ok(true) 184 } 185 409 => { 186 // Already registered 187 warn!("Device already registered (HTTP 409)"); 188 Ok(false) 189 } 190 _ => { 191 let error_msg = String::from_utf8_lossy(&response_buf[..len]); 192 anyhow::bail!("Registration failed with HTTP {}: {}", status, error_msg) 193 } 194 } 195 } 196 197 #[cfg(test)] 198 mod tests { 199 200 #[test] 201 fn test_signature_generation() { 202 let device_id = "TEST1234"; 203 let secret = "test_secret_key_for_hmac_signing"; 204 let timestamp = 1234567890; 205 let endpoint = "/api/backup"; 206 let body = b"test body content"; 207 208 let sig1 = generate_signature(device_id, secret, timestamp, endpoint, body).unwrap(); 209 let sig2 = generate_signature(device_id, secret, timestamp, endpoint, body).unwrap(); 210 211 // Same inputs should produce same signature 212 assert_eq!(sig1, sig2); 213 214 // Different timestamp should produce different signature 215 let sig3 = generate_signature(device_id, secret, timestamp + 1, endpoint, body).unwrap(); 216 assert_ne!(sig1, sig3); 217 } 218 219 #[test] 220 fn test_signature_verification() { 221 let device_id = "TEST1234"; 222 let secret = "test_secret_key"; 223 let timestamp = get_timestamp(); 224 let endpoint = "/api/backup"; 225 let body = b"test data"; 226 227 let signature = generate_signature(device_id, secret, timestamp, endpoint, body).unwrap(); 228 229 // Valid signature should verify 230 assert!( 231 verify_signature(device_id, secret, timestamp, endpoint, body, &signature).unwrap() 232 ); 233 234 // Invalid signature should fail 235 assert!( 236 !verify_signature(device_id, secret, timestamp, endpoint, body, "invalid") 237 .unwrap_or(false) 238 ); 239 240 // Old timestamp should fail 241 assert!(verify_signature( 242 device_id, 243 secret, 244 timestamp - 400, 245 endpoint, 246 body, 247 &signature 248 ) 249 .is_err()); 250 } 251 }