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