/ src / network / auth.rs
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  }