1use chrono::{DateTime, Utc};
2use zeroize::Zeroizing;
3
4use auths_crypto::SecureSeed;
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; 8],
23 pub transport_key: TransportKey,
25 pub initiator_x25519_pub: [u8; 32],
27}
28
29pub struct ResponderResult {
31 pub response: PairingResponse,
32 pub shared_secret: Zeroizing<[u8; 32]>,
33 pub sas: [u8; 8],
34 pub transport_key: TransportKey,
35}
36
37pub struct PairingProtocol {
55 session: PairingSession,
56}
57
58impl PairingProtocol {
59 pub fn initiate(
72 now: DateTime<Utc>,
73 controller_did: String,
74 endpoint: String,
75 capabilities: Vec<String>,
76 ) -> Result<(Self, PairingToken), ProtocolError> {
77 let session = PairingToken::generate(now, controller_did, endpoint, capabilities)?;
78 let token = session.token.clone();
79 Ok((Self { session }, token))
80 }
81
82 pub fn complete(
95 mut self,
96 now: DateTime<Utc>,
97 response_bytes: &[u8],
98 ) -> Result<CompletedPairing, ProtocolError> {
99 let response: PairingResponse = serde_json::from_slice(response_bytes)?;
100 response.verify(now, &self.session.token)?;
101 self.complete_inner(now, response)
102 }
103
104 pub fn complete_with_response(
110 mut self,
111 now: DateTime<Utc>,
112 response: PairingResponse,
113 ) -> Result<CompletedPairing, ProtocolError> {
114 response.verify(now, &self.session.token)?;
115 self.complete_inner(now, response)
116 }
117
118 fn complete_inner(
119 &mut self,
120 _now: DateTime<Utc>,
121 response: PairingResponse,
122 ) -> Result<CompletedPairing, ProtocolError> {
123 let initiator_x25519_pub = self.session.ephemeral_pubkey_bytes()?;
124 let responder_x25519_pub = response.device_x25519_pubkey_bytes()?;
125 let shared_secret = self.session.complete_exchange(&responder_x25519_pub)?;
126 let peer_signing_pubkey = response.device_signing_pubkey_bytes()?;
127 let peer_did = response.device_did.clone();
128 let short_code = &self.session.token.short_code;
129
130 let sas_bytes = sas::derive_sas(
131 &shared_secret,
132 &initiator_x25519_pub,
133 &responder_x25519_pub,
134 short_code,
135 );
136 let transport_key = sas::derive_transport_key(
137 &shared_secret,
138 &initiator_x25519_pub,
139 &responder_x25519_pub,
140 short_code,
141 );
142
143 Ok(CompletedPairing {
144 shared_secret,
145 peer_signing_pubkey,
146 peer_did,
147 response,
148 sas: sas_bytes,
149 transport_key,
150 initiator_x25519_pub,
151 })
152 }
153
154 pub fn token(&self) -> &PairingToken {
156 &self.session.token
157 }
158}
159
160pub fn respond_to_pairing(
177 now: DateTime<Utc>,
178 token_bytes: &[u8],
179 device_seed: &SecureSeed,
180 device_pubkey: &[u8; 32],
181 device_did: String,
182 device_name: Option<String>,
183) -> Result<ResponderResult, ProtocolError> {
184 let token: PairingToken = serde_json::from_slice(token_bytes)?;
185 let (response, shared_secret) = PairingResponse::create(
186 now,
187 &token,
188 device_seed,
189 device_pubkey,
190 device_did,
191 device_name,
192 )?;
193
194 let initiator_x25519_pub = token.ephemeral_pubkey_bytes()?;
195 let responder_x25519_pub = response.device_x25519_pubkey_bytes()?;
196 let short_code = &token.short_code;
197
198 let sas_bytes = sas::derive_sas(
199 &shared_secret,
200 &initiator_x25519_pub,
201 &responder_x25519_pub,
202 short_code,
203 );
204 let transport_key = sas::derive_transport_key(
205 &shared_secret,
206 &initiator_x25519_pub,
207 &responder_x25519_pub,
208 short_code,
209 );
210
211 Ok(ResponderResult {
212 response,
213 shared_secret,
214 sas: sas_bytes,
215 transport_key,
216 })
217}
218
219#[cfg(test)]
220#[allow(clippy::disallowed_methods)]
221mod tests {
222 use super::*;
223 use ring::rand::SystemRandom;
224 use ring::signature::{Ed25519KeyPair, KeyPair};
225
226 fn generate_test_keypair() -> (SecureSeed, [u8; 32]) {
227 let rng = SystemRandom::new();
228 let pkcs8_doc = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
229 let keypair = Ed25519KeyPair::from_pkcs8(pkcs8_doc.as_ref()).unwrap();
230 let public_key: [u8; 32] = keypair.public_key().as_ref().try_into().unwrap();
231 let seed: [u8; 32] = pkcs8_doc.as_ref()[16..48].try_into().unwrap();
232 (SecureSeed::new(seed), public_key)
233 }
234
235 #[test]
236 fn happy_path_initiate_and_complete() {
237 let now = chrono::Utc::now();
238 let (protocol, token) = PairingProtocol::initiate(
239 now,
240 "did:keri:test".to_string(),
241 "http://localhost:3000".to_string(),
242 vec!["sign_commit".to_string()],
243 )
244 .unwrap();
245
246 let (seed, pubkey) = generate_test_keypair();
247 let token_bytes = serde_json::to_vec(&token).unwrap();
248 let responder_result = respond_to_pairing(
249 now,
250 &token_bytes,
251 &seed,
252 &pubkey,
253 "did:key:z6MkTest".to_string(),
254 None,
255 )
256 .unwrap();
257
258 let response_bytes = serde_json::to_vec(&responder_result.response).unwrap();
259 let completed = protocol.complete(now, &response_bytes).unwrap();
260
261 assert_eq!(*completed.shared_secret, *responder_result.shared_secret);
262 assert_eq!(completed.peer_did, "did:key:z6MkTest");
263 assert_eq!(completed.sas, responder_result.sas);
265 }
266
267 #[test]
268 fn expired_token_fails() {
269 use chrono::Duration;
270
271 let now = chrono::Utc::now();
272 let session = PairingToken::generate_with_expiry(
273 now,
274 "did:keri:test".to_string(),
275 "http://localhost:3000".to_string(),
276 vec![],
277 Duration::seconds(-1),
278 )
279 .unwrap();
280
281 let token = session.token.clone();
282 let protocol = PairingProtocol { session };
283
284 let (seed, pubkey) = generate_test_keypair();
285 let (response, _) = PairingResponse::create(
286 now - Duration::seconds(10),
288 &token,
289 &seed,
290 &pubkey,
291 "did:key:z6MkTest".to_string(),
292 None,
293 )
294 .unwrap();
295
296 let response_bytes = serde_json::to_vec(&response).unwrap();
297 let result = protocol.complete(now, &response_bytes);
298 assert!(matches!(result, Err(ProtocolError::Expired)));
299 }
300
301 #[test]
302 fn invalid_response_bytes_fails() {
303 let now = chrono::Utc::now();
304 let (protocol, _token) = PairingProtocol::initiate(
305 now,
306 "did:keri:test".to_string(),
307 "http://localhost:3000".to_string(),
308 vec![],
309 )
310 .unwrap();
311
312 let result = protocol.complete(now, b"not valid json");
313 assert!(matches!(result, Err(ProtocolError::Serialization(_))));
314 }
315}