peat_protocol/security/
authenticator.rs1use super::device_id::DeviceId;
4use super::error::SecurityError;
5use super::keypair::DeviceKeypair;
6use super::{CHALLENGE_NONCE_SIZE, DEFAULT_CHALLENGE_TIMEOUT_SECS};
7use peat_schema::security::v1::{Challenge, SignedChallengeResponse};
8use rand_core::{OsRng, RngCore};
9use std::collections::HashMap;
10use std::sync::RwLock;
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13pub struct DeviceAuthenticator {
41 keypair: DeviceKeypair,
43
44 verified_peers: RwLock<HashMap<DeviceId, VerifiedPeer>>,
46
47 challenge_timeout: Duration,
49}
50
51#[derive(Debug, Clone)]
53pub struct VerifiedPeer {
54 pub device_id: DeviceId,
56
57 pub public_key: [u8; 32],
59
60 pub verified_at: SystemTime,
62}
63
64impl DeviceAuthenticator {
65 pub fn new(keypair: DeviceKeypair) -> Self {
67 Self::with_timeout(keypair, Duration::from_secs(DEFAULT_CHALLENGE_TIMEOUT_SECS))
68 }
69
70 pub fn with_timeout(keypair: DeviceKeypair, challenge_timeout: Duration) -> Self {
72 DeviceAuthenticator {
73 keypair,
74 verified_peers: RwLock::new(HashMap::new()),
75 challenge_timeout,
76 }
77 }
78
79 pub fn device_id(&self) -> DeviceId {
81 self.keypair.device_id()
82 }
83
84 pub fn public_key_bytes(&self) -> [u8; 32] {
86 self.keypair.public_key_bytes()
87 }
88
89 pub fn generate_challenge(&self) -> Challenge {
97 let mut nonce = [0u8; CHALLENGE_NONCE_SIZE];
98 OsRng.fill_bytes(&mut nonce);
99
100 let now = SystemTime::now()
101 .duration_since(UNIX_EPOCH)
102 .unwrap_or_default();
103
104 let expires = now + self.challenge_timeout;
105
106 Challenge {
107 nonce: nonce.to_vec(),
108 timestamp: Some(peat_schema::common::v1::Timestamp {
109 seconds: now.as_secs(),
110 nanos: now.subsec_nanos(),
111 }),
112 challenger_id: self.device_id().to_hex(),
113 expires_at: Some(peat_schema::common::v1::Timestamp {
114 seconds: expires.as_secs(),
115 nanos: expires.subsec_nanos(),
116 }),
117 }
118 }
119
120 pub fn respond_to_challenge(
124 &self,
125 challenge: &Challenge,
126 ) -> Result<SignedChallengeResponse, SecurityError> {
127 self.check_challenge_expiry(challenge)?;
129
130 let message = self.create_challenge_message(challenge);
132
133 let signature = self.keypair.sign(&message);
135
136 Ok(SignedChallengeResponse {
137 challenge_nonce: challenge.nonce.clone(),
138 public_key: self.keypair.public_key_bytes().to_vec(),
139 signature: signature.to_bytes().to_vec(),
140 timestamp: Some(peat_schema::common::v1::Timestamp {
141 seconds: SystemTime::now()
142 .duration_since(UNIX_EPOCH)
143 .unwrap_or_default()
144 .as_secs(),
145 nanos: 0,
146 }),
147 device_type: 0, certificates: vec![], })
150 }
151
152 pub fn verify_response(
156 &self,
157 response: &SignedChallengeResponse,
158 ) -> Result<DeviceId, SecurityError> {
159 let public_key = DeviceKeypair::verifying_key_from_bytes(&response.public_key)?;
161
162 let peer_device_id = DeviceId::from_public_key(&public_key);
164
165 let mut message = response.challenge_nonce.clone();
169 message.extend_from_slice(self.device_id().to_hex().as_bytes());
170 if let Some(ts) = &response.timestamp {
172 message.extend_from_slice(&ts.seconds.to_le_bytes());
173 }
174
175 let signature = DeviceKeypair::signature_from_bytes(&response.signature)?;
177 DeviceKeypair::verify_with_key(&public_key, &message, &signature)?;
178
179 let verified_peer = VerifiedPeer {
181 device_id: peer_device_id,
182 public_key: public_key.to_bytes(),
183 verified_at: SystemTime::now(),
184 };
185
186 self.verified_peers
187 .write()
188 .map_err(|e| SecurityError::Internal(format!("lock poisoned: {}", e)))?
189 .insert(peer_device_id, verified_peer);
190
191 Ok(peer_device_id)
192 }
193
194 pub fn is_verified(&self, device_id: &DeviceId) -> bool {
196 self.verified_peers
197 .read()
198 .map(|cache| cache.contains_key(device_id))
199 .unwrap_or(false)
200 }
201
202 pub fn get_verified_peer(&self, device_id: &DeviceId) -> Option<VerifiedPeer> {
204 self.verified_peers
205 .read()
206 .ok()
207 .and_then(|cache| cache.get(device_id).cloned())
208 }
209
210 pub fn remove_peer(&self, device_id: &DeviceId) {
212 if let Ok(mut cache) = self.verified_peers.write() {
213 cache.remove(device_id);
214 }
215 }
216
217 pub fn clear_verified_peers(&self) {
219 if let Ok(mut cache) = self.verified_peers.write() {
220 cache.clear();
221 }
222 }
223
224 pub fn verified_peer_count(&self) -> usize {
226 self.verified_peers
227 .read()
228 .map(|cache| cache.len())
229 .unwrap_or(0)
230 }
231
232 fn create_challenge_message(&self, challenge: &Challenge) -> Vec<u8> {
234 let mut message = challenge.nonce.clone();
235 message.extend_from_slice(challenge.challenger_id.as_bytes());
236 if let Some(ts) = &challenge.timestamp {
237 message.extend_from_slice(&ts.seconds.to_le_bytes());
238 }
239 message
240 }
241
242 fn check_challenge_expiry(&self, challenge: &Challenge) -> Result<(), SecurityError> {
244 if let Some(expires) = &challenge.expires_at {
245 let now = SystemTime::now()
246 .duration_since(UNIX_EPOCH)
247 .unwrap_or_default();
248
249 if now.as_secs() > expires.seconds {
250 return Err(SecurityError::ChallengeExpired(expires.seconds));
251 }
252 }
253 Ok(())
254 }
255}
256
257impl std::fmt::Debug for DeviceAuthenticator {
258 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259 f.debug_struct("DeviceAuthenticator")
260 .field("device_id", &self.device_id())
261 .field("verified_peer_count", &self.verified_peer_count())
262 .field("challenge_timeout", &self.challenge_timeout)
263 .finish()
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn create_test_authenticator() -> DeviceAuthenticator {
272 let keypair = DeviceKeypair::generate();
273 DeviceAuthenticator::new(keypair)
274 }
275
276 #[test]
277 fn test_generate_challenge() {
278 let auth = create_test_authenticator();
279 let challenge = auth.generate_challenge();
280
281 assert_eq!(challenge.nonce.len(), CHALLENGE_NONCE_SIZE);
282 assert!(!challenge.challenger_id.is_empty());
283 assert!(challenge.timestamp.is_some());
284 assert!(challenge.expires_at.is_some());
285 }
286
287 #[test]
288 fn test_challenge_nonce_unique() {
289 let auth = create_test_authenticator();
290 let c1 = auth.generate_challenge();
291 let c2 = auth.generate_challenge();
292
293 assert_ne!(c1.nonce, c2.nonce);
294 }
295
296 #[test]
297 fn test_respond_to_challenge() {
298 let auth1 = create_test_authenticator();
299 let auth2 = create_test_authenticator();
300
301 let challenge = auth1.generate_challenge();
302 let response = auth2.respond_to_challenge(&challenge).unwrap();
303
304 assert_eq!(response.public_key.len(), 32);
305 assert_eq!(response.signature.len(), 64);
306 assert_eq!(response.challenge_nonce, challenge.nonce);
307 }
308
309 #[test]
310 fn test_full_authentication_flow() {
311 let auth1 = create_test_authenticator();
312 let auth2 = create_test_authenticator();
313
314 let challenge = auth1.generate_challenge();
316
317 let response = auth2.respond_to_challenge(&challenge).unwrap();
319
320 let peer_id = auth1.verify_response(&response).unwrap();
322
323 assert_eq!(peer_id, auth2.device_id());
325
326 assert!(auth1.is_verified(&peer_id));
328 }
329
330 #[test]
331 fn test_expired_challenge_rejected() {
332 let auth = create_test_authenticator();
333
334 let mut challenge = auth.generate_challenge();
336 challenge.expires_at = Some(peat_schema::common::v1::Timestamp {
337 seconds: 0, nanos: 0,
339 });
340
341 let result = auth.respond_to_challenge(&challenge);
342 assert!(matches!(result, Err(SecurityError::ChallengeExpired(_))));
343 }
344
345 #[test]
346 fn test_invalid_signature_rejected() {
347 let auth1 = create_test_authenticator();
348 let auth2 = create_test_authenticator();
349
350 let challenge = auth1.generate_challenge();
351 let mut response = auth2.respond_to_challenge(&challenge).unwrap();
352
353 response.signature[0] ^= 0xFF;
355
356 let result = auth1.verify_response(&response);
357 assert!(matches!(result, Err(SecurityError::InvalidSignature(_))));
358 }
359
360 #[test]
361 fn test_remove_peer() {
362 let auth1 = create_test_authenticator();
363 let auth2 = create_test_authenticator();
364
365 let challenge = auth1.generate_challenge();
367 let response = auth2.respond_to_challenge(&challenge).unwrap();
368 let peer_id = auth1.verify_response(&response).unwrap();
369
370 assert!(auth1.is_verified(&peer_id));
371
372 auth1.remove_peer(&peer_id);
374 assert!(!auth1.is_verified(&peer_id));
375 }
376
377 #[test]
378 fn test_clear_verified_peers() {
379 let auth1 = create_test_authenticator();
380 let auth2 = create_test_authenticator();
381 let auth3 = create_test_authenticator();
382
383 let c1 = auth1.generate_challenge();
385 let r1 = auth2.respond_to_challenge(&c1).unwrap();
386 auth1.verify_response(&r1).unwrap();
387
388 let c2 = auth1.generate_challenge();
389 let r2 = auth3.respond_to_challenge(&c2).unwrap();
390 auth1.verify_response(&r2).unwrap();
391
392 assert_eq!(auth1.verified_peer_count(), 2);
393
394 auth1.clear_verified_peers();
395 assert_eq!(auth1.verified_peer_count(), 0);
396 }
397}