1use chrono::{DateTime, Utc};
2use zeroize::Zeroizing;
3
4use auths_crypto::TypedSeed;
5
6use crate::error::ProtocolError;
7use crate::response::PairingResponse;
8use crate::sas::{self, TransportKey};
9use crate::token::{PairingSession, PairingToken};
10
11pub struct CompletedPairing {
13 pub shared_secret: Zeroizing<[u8; 32]>,
15 pub peer_signing_pubkey: Vec<u8>,
17 pub peer_did: String,
19 pub response: PairingResponse,
21 pub sas: [u8; 10],
23 pub transport_key: TransportKey,
25 pub initiator_ephemeral_pub: Vec<u8>,
27}
28
29impl std::fmt::Debug for CompletedPairing {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 f.debug_struct("CompletedPairing")
32 .field("shared_secret", &"[redacted 32 bytes]")
33 .field(
34 "peer_signing_pubkey",
35 &format!("{} bytes", self.peer_signing_pubkey.len()),
36 )
37 .field("peer_did", &self.peer_did)
38 .field("response", &self.response)
39 .field("sas", &"[redacted 10 bytes]")
40 .field("transport_key", &"[redacted]")
41 .field(
42 "initiator_ephemeral_pub",
43 &format!("{} bytes", self.initiator_ephemeral_pub.len()),
44 )
45 .finish()
46 }
47}
48
49pub struct ResponderResult {
51 pub response: PairingResponse,
52 pub shared_secret: Zeroizing<[u8; 32]>,
53 pub sas: [u8; 10],
54 pub transport_key: TransportKey,
55}
56
57pub struct PairingProtocol {
75 session: PairingSession,
76}
77
78impl PairingProtocol {
79 pub fn initiate(
92 now: DateTime<Utc>,
93 controller_did: String,
94 endpoint: String,
95 capabilities: Vec<String>,
96 ) -> Result<(Self, PairingToken), ProtocolError> {
97 let session = PairingToken::generate(now, controller_did, endpoint, capabilities)?;
98 let token = session.token.clone();
99 Ok((Self { session }, token))
100 }
101
102 pub fn complete(
115 mut self,
116 now: DateTime<Utc>,
117 response_bytes: &[u8],
118 ) -> Result<CompletedPairing, ProtocolError> {
119 let response: PairingResponse = serde_json::from_slice(response_bytes)?;
120 response.verify(now, &self.session.token)?;
121 self.complete_inner(now, response)
122 }
123
124 pub fn complete_with_response(
130 mut self,
131 now: DateTime<Utc>,
132 response: PairingResponse,
133 ) -> Result<CompletedPairing, ProtocolError> {
134 response.verify(now, &self.session.token)?;
135 self.complete_inner(now, response)
136 }
137
138 fn complete_inner(
139 &mut self,
140 _now: DateTime<Utc>,
141 response: PairingResponse,
142 ) -> Result<CompletedPairing, ProtocolError> {
143 let initiator_ecdh_pub = self.session.ephemeral_pubkey_bytes()?;
144 let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
145 let shared_secret = self.session.complete_exchange(&responder_ecdh_pub)?;
146 let peer_signing_pubkey = response.device_signing_pubkey_bytes()?;
147 let peer_did = response.device_did.clone();
148 let session_id = &self.session.token.session_id;
149 let short_code = &self.session.token.short_code;
150
151 let sas_bytes = sas::derive_sas(
152 &shared_secret,
153 &initiator_ecdh_pub,
154 &responder_ecdh_pub,
155 session_id,
156 short_code,
157 );
158 let transport_key = sas::derive_transport_key(
159 &shared_secret,
160 &initiator_ecdh_pub,
161 &responder_ecdh_pub,
162 session_id,
163 short_code,
164 );
165
166 Ok(CompletedPairing {
167 shared_secret,
168 peer_signing_pubkey,
169 peer_did,
170 response,
171 sas: sas_bytes,
172 transport_key,
173 initiator_ephemeral_pub: initiator_ecdh_pub,
174 })
175 }
176
177 pub fn token(&self) -> &PairingToken {
179 &self.session.token
180 }
181}
182
183use std::marker::PhantomData;
215
216pub struct Init;
218pub struct Responded;
221pub struct Confirmed;
223pub struct Paired;
226
227#[non_exhaustive]
232pub struct SasMatch {
233 _sealed: (),
234}
235
236impl SasMatch {
237 pub fn user_confirmed_visual_match(_ours: &[u8; 10], _theirs: &[u8; 10]) -> Self {
247 Self { _sealed: () }
248 }
249}
250
251pub struct PairingFlow<S> {
254 session: PairingSession,
255 accepted: Option<CompletedPairingUnconfirmed>,
256 _state: PhantomData<S>,
257}
258
259struct CompletedPairingUnconfirmed {
262 shared_secret: Zeroizing<[u8; 32]>,
263 peer_signing_pubkey: Vec<u8>,
264 peer_did: String,
265 response: PairingResponse,
266 sas: [u8; 10],
267 transport_key: TransportKey,
268 initiator_ephemeral_pub: Vec<u8>,
269}
270
271impl PairingFlow<Init> {
272 pub fn initiate(
275 now: DateTime<Utc>,
276 controller_did: String,
277 endpoint: String,
278 capabilities: Vec<String>,
279 ) -> Result<(Self, PairingToken), ProtocolError> {
280 let session = PairingToken::generate(now, controller_did, endpoint, capabilities)?;
281 let token = session.token.clone();
282 Ok((
283 Self {
284 session,
285 accepted: None,
286 _state: PhantomData,
287 },
288 token,
289 ))
290 }
291
292 pub fn token(&self) -> &PairingToken {
294 &self.session.token
295 }
296
297 pub fn accept_response(
300 mut self,
301 now: DateTime<Utc>,
302 response: PairingResponse,
303 ) -> Result<PairingFlow<Responded>, ProtocolError> {
304 response.verify(now, &self.session.token)?;
305 let initiator_ecdh_pub = self.session.ephemeral_pubkey_bytes()?;
306 let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
307 let shared_secret = self.session.complete_exchange(&responder_ecdh_pub)?;
308 let peer_signing_pubkey = response.device_signing_pubkey_bytes()?;
309 let peer_did = response.device_did.clone();
310 let session_id = &self.session.token.session_id;
311 let short_code = &self.session.token.short_code;
312
313 let sas = sas::derive_sas(
314 &shared_secret,
315 &initiator_ecdh_pub,
316 &responder_ecdh_pub,
317 session_id,
318 short_code,
319 );
320 let transport_key = sas::derive_transport_key(
321 &shared_secret,
322 &initiator_ecdh_pub,
323 &responder_ecdh_pub,
324 session_id,
325 short_code,
326 );
327
328 Ok(PairingFlow {
329 session: self.session,
330 accepted: Some(CompletedPairingUnconfirmed {
331 shared_secret,
332 peer_signing_pubkey,
333 peer_did,
334 response,
335 sas,
336 transport_key,
337 initiator_ephemeral_pub: initiator_ecdh_pub,
338 }),
339 _state: PhantomData,
340 })
341 }
342}
343
344impl PairingFlow<Responded> {
345 pub fn sas(&self) -> Result<&[u8; 10], ProtocolError> {
348 self.accepted
349 .as_ref()
350 .map(|a| &a.sas)
351 .ok_or_else(|| ProtocolError::KeyExchangeFailed("flow has no accepted response".into()))
352 }
353
354 pub fn confirm(self, _proof: SasMatch) -> PairingFlow<Confirmed> {
359 PairingFlow {
360 session: self.session,
361 accepted: self.accepted,
362 _state: PhantomData,
363 }
364 }
365}
366
367impl PairingFlow<Confirmed> {
368 pub fn finalize(self) -> Result<(PairingFlow<Paired>, CompletedPairing), ProtocolError> {
374 let accepted = self.accepted.ok_or_else(|| {
375 ProtocolError::KeyExchangeFailed(
376 "flow reached Confirmed without accepted response (impossible)".into(),
377 )
378 })?;
379 let completed = CompletedPairing {
380 shared_secret: accepted.shared_secret,
381 peer_signing_pubkey: accepted.peer_signing_pubkey,
382 peer_did: accepted.peer_did,
383 response: accepted.response,
384 sas: accepted.sas,
385 transport_key: accepted.transport_key,
386 initiator_ephemeral_pub: accepted.initiator_ephemeral_pub,
387 };
388 Ok((
389 PairingFlow {
390 session: self.session,
391 accepted: None,
392 _state: PhantomData,
393 },
394 completed,
395 ))
396 }
397}
398
399pub fn respond_to_pairing(
416 now: DateTime<Utc>,
417 token_bytes: &[u8],
418 device_seed: &TypedSeed,
419 device_pubkey: &[u8],
420 device_did: String,
421 device_name: Option<String>,
422) -> Result<ResponderResult, ProtocolError> {
423 let token: PairingToken = serde_json::from_slice(token_bytes)?;
424 let (response, shared_secret) = PairingResponse::create(
425 now,
426 &token,
427 device_seed,
428 device_pubkey,
429 device_did,
430 device_name,
431 )?;
432
433 let initiator_ecdh_pub = token.ephemeral_pubkey_bytes()?;
434 let responder_ecdh_pub = response.device_ephemeral_pubkey_bytes()?;
435 let session_id = &token.session_id;
436 let short_code = &token.short_code;
437
438 let sas_bytes = sas::derive_sas(
439 &shared_secret,
440 &initiator_ecdh_pub,
441 &responder_ecdh_pub,
442 session_id,
443 short_code,
444 );
445 let transport_key = sas::derive_transport_key(
446 &shared_secret,
447 &initiator_ecdh_pub,
448 &responder_ecdh_pub,
449 session_id,
450 short_code,
451 );
452
453 Ok(ResponderResult {
454 response,
455 shared_secret,
456 sas: sas_bytes,
457 transport_key,
458 })
459}
460
461#[cfg(test)]
462#[allow(clippy::disallowed_methods)]
463mod tests {
464 use super::*;
465
466 fn generate_test_keypair() -> (TypedSeed, Vec<u8>) {
467 use p256::ecdsa::SigningKey;
468 use p256::elliptic_curve::rand_core::OsRng as P256Rng;
469 use p256::pkcs8::EncodePrivateKey;
470
471 let sk = SigningKey::random(&mut P256Rng);
472 let pkcs8 = sk.to_pkcs8_der().unwrap();
473 let parsed = auths_crypto::parse_key_material(pkcs8.as_bytes()).unwrap();
474 (parsed.seed, parsed.public_key)
475 }
476
477 #[test]
478 fn happy_path_initiate_and_complete() {
479 let now = chrono::Utc::now();
480 let (protocol, token) = PairingProtocol::initiate(
481 now,
482 "did:keri:test".to_string(),
483 "http://localhost:3000".to_string(),
484 vec!["sign_commit".to_string()],
485 )
486 .unwrap();
487
488 let (seed, pubkey) = generate_test_keypair();
489 let token_bytes = serde_json::to_vec(&token).unwrap();
490 let responder_result = respond_to_pairing(
491 now,
492 &token_bytes,
493 &seed,
494 &pubkey,
495 "did:key:zDnaTest".to_string(),
496 None,
497 )
498 .unwrap();
499
500 let response_bytes = serde_json::to_vec(&responder_result.response).unwrap();
501 let completed = protocol.complete(now, &response_bytes).unwrap();
502
503 assert_eq!(*completed.shared_secret, *responder_result.shared_secret);
504 assert_eq!(completed.peer_did, "did:key:zDnaTest");
505 assert_eq!(completed.sas, responder_result.sas);
507 }
508
509 #[test]
510 fn expired_token_fails() {
511 use chrono::Duration;
512
513 let now = chrono::Utc::now();
514 let session = PairingToken::generate_with_expiry(
515 now,
516 "did:keri:test".to_string(),
517 "http://localhost:3000".to_string(),
518 vec![],
519 Duration::seconds(-1),
520 )
521 .unwrap();
522
523 let token = session.token.clone();
524 let protocol = PairingProtocol { session };
525
526 let (seed, pubkey) = generate_test_keypair();
527 let (response, _) = PairingResponse::create(
528 now - Duration::seconds(10),
530 &token,
531 &seed,
532 &pubkey,
533 "did:key:zDnaTest".to_string(),
534 None,
535 )
536 .unwrap();
537
538 let response_bytes = serde_json::to_vec(&response).unwrap();
539 let result = protocol.complete(now, &response_bytes);
540 assert!(matches!(result, Err(ProtocolError::Expired)));
541 }
542
543 #[test]
544 fn invalid_response_bytes_fails() {
545 let now = chrono::Utc::now();
546 let (protocol, _token) = PairingProtocol::initiate(
547 now,
548 "did:keri:test".to_string(),
549 "http://localhost:3000".to_string(),
550 vec![],
551 )
552 .unwrap();
553
554 let result = protocol.complete(now, b"not valid json");
555 assert!(matches!(result, Err(ProtocolError::Serialization(_))));
556 }
557}