1use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
4use chrono::{DateTime, Utc};
5use rand::rngs::OsRng;
6use serde::{Deserialize, Serialize};
7use x25519_dalek::{EphemeralSecret, PublicKey};
8use zeroize::Zeroizing;
9
10use auths_crypto::SecureSeed;
11
12use super::error::PairingError;
13use super::token::PairingToken;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PairingResponse {
18 pub short_code: String,
20 pub device_x25519_pubkey: String,
22 pub device_signing_pubkey: String,
24 pub device_did: String,
26 pub signature: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub device_name: Option<String>,
31}
32
33impl PairingResponse {
34 pub fn create(
44 now: DateTime<Utc>,
45 token: &PairingToken,
46 device_seed: &SecureSeed,
47 device_pubkey: &[u8; 32],
48 device_did: String,
49 device_name: Option<String>,
50 ) -> Result<(Self, Zeroizing<[u8; 32]>), PairingError> {
51 use crate::crypto::provider_bridge;
52
53 if token.is_expired(now) {
54 return Err(PairingError::Expired);
55 }
56
57 let device_x25519_secret = EphemeralSecret::random_from_rng(OsRng);
59 let device_x25519_public = PublicKey::from(&device_x25519_secret);
60
61 let initiator_x25519_bytes = token.ephemeral_pubkey_bytes()?;
63 let initiator_x25519 = PublicKey::from(initiator_x25519_bytes);
64
65 let shared = device_x25519_secret.diffie_hellman(&initiator_x25519);
67 let shared_secret = Zeroizing::new(*shared.as_bytes());
68
69 let device_signing_pubkey = URL_SAFE_NO_PAD.encode(device_pubkey);
71
72 let device_x25519_pubkey_str = URL_SAFE_NO_PAD.encode(device_x25519_public.as_bytes());
74
75 let mut message = Vec::new();
77 message.extend_from_slice(token.short_code.as_bytes());
78 message.extend_from_slice(&initiator_x25519_bytes);
79 message.extend_from_slice(device_x25519_public.as_bytes());
80
81 let sig_bytes = provider_bridge::sign_ed25519_sync(device_seed, &message)
83 .map_err(|_| PairingError::KeyGenFailed("Ed25519 signing failed".to_string()))?;
84 let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
85
86 let response = PairingResponse {
87 short_code: token.short_code.clone(),
88 device_x25519_pubkey: device_x25519_pubkey_str,
89 device_signing_pubkey,
90 device_did,
91 signature,
92 device_name,
93 };
94
95 Ok((response, shared_secret))
96 }
97
98 pub fn verify(&self, now: DateTime<Utc>, token: &PairingToken) -> Result<(), PairingError> {
104 use crate::crypto::provider_bridge;
105
106 if token.is_expired(now) {
107 return Err(PairingError::Expired);
108 }
109
110 let initiator_x25519_bytes = token.ephemeral_pubkey_bytes()?;
112 let device_x25519_bytes = self.device_x25519_pubkey_bytes()?;
113 let device_signing_bytes = self.device_signing_pubkey_bytes()?;
114 let signature_bytes = URL_SAFE_NO_PAD
115 .decode(&self.signature)
116 .map_err(|_| PairingError::InvalidSignature)?;
117
118 let mut message = Vec::new();
120 message.extend_from_slice(token.short_code.as_bytes());
121 message.extend_from_slice(&initiator_x25519_bytes);
122 message.extend_from_slice(&device_x25519_bytes);
123
124 provider_bridge::verify_ed25519_sync(&device_signing_bytes, &message, &signature_bytes)
126 .map_err(|_| PairingError::InvalidSignature)?;
127
128 Ok(())
129 }
130
131 pub fn device_x25519_pubkey_bytes(&self) -> Result<[u8; 32], PairingError> {
133 let bytes = URL_SAFE_NO_PAD
134 .decode(&self.device_x25519_pubkey)
135 .map_err(|_| PairingError::InvalidSignature)?;
136 bytes.try_into().map_err(|_| {
137 PairingError::KeyExchangeFailed("Invalid X25519 pubkey length".to_string())
138 })
139 }
140
141 pub fn device_signing_pubkey_bytes(&self) -> Result<Vec<u8>, PairingError> {
143 URL_SAFE_NO_PAD
144 .decode(&self.device_signing_pubkey)
145 .map_err(|_| PairingError::InvalidSignature)
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::crypto::provider_bridge;
153
154 fn generate_test_seed_and_pubkey() -> (SecureSeed, [u8; 32]) {
155 provider_bridge::generate_ed25519_keypair_sync().unwrap()
156 }
157
158 fn make_token() -> super::super::token::PairingSession {
159 PairingToken::generate(
160 chrono::Utc::now(),
161 "did:keri:test123".to_string(),
162 "http://localhost:3000".to_string(),
163 vec!["sign_commit".to_string()],
164 )
165 .unwrap()
166 }
167
168 #[test]
169 fn test_create_and_verify_response() {
170 let now = chrono::Utc::now();
171 let session = make_token();
172 let (seed, pubkey) = generate_test_seed_and_pubkey();
173
174 let (response, _shared_secret) = PairingResponse::create(
175 now,
176 &session.token,
177 &seed,
178 &pubkey,
179 "did:key:z6MkTest".to_string(),
180 Some("Test Device".to_string()),
181 )
182 .unwrap();
183
184 assert!(response.verify(now, &session.token).is_ok());
185 }
186
187 #[test]
188 fn test_expired_token_rejected() {
189 use chrono::Duration;
190
191 let now = chrono::Utc::now();
192 let session = PairingToken::generate_with_expiry(
193 now,
194 "did:keri:test".to_string(),
195 "http://localhost:3000".to_string(),
196 vec![],
197 Duration::seconds(-1),
198 )
199 .unwrap();
200 let (seed, pubkey) = generate_test_seed_and_pubkey();
201
202 let result = PairingResponse::create(
203 now,
204 &session.token,
205 &seed,
206 &pubkey,
207 "did:key:z6MkTest".to_string(),
208 None,
209 );
210 assert!(matches!(result, Err(PairingError::Expired)));
211 }
212
213 #[test]
214 fn test_tampered_signature_rejected() {
215 let now = chrono::Utc::now();
216 let session = make_token();
217 let (seed, pubkey) = generate_test_seed_and_pubkey();
218
219 let (mut response, _) = PairingResponse::create(
220 now,
221 &session.token,
222 &seed,
223 &pubkey,
224 "did:key:z6MkTest".to_string(),
225 None,
226 )
227 .unwrap();
228
229 let mut sig_bytes = URL_SAFE_NO_PAD.decode(&response.signature).unwrap();
231 sig_bytes[0] ^= 0xFF;
232 response.signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
233
234 let result = response.verify(now, &session.token);
235 assert!(matches!(result, Err(PairingError::InvalidSignature)));
236 }
237
238 #[test]
239 fn test_shared_secret_matches() {
240 let now = chrono::Utc::now();
241 let mut session = make_token();
242 let (seed, pubkey) = generate_test_seed_and_pubkey();
243
244 let (response, responder_secret) = PairingResponse::create(
245 now,
246 &session.token,
247 &seed,
248 &pubkey,
249 "did:key:z6MkTest".to_string(),
250 None,
251 )
252 .unwrap();
253
254 let device_x25519_bytes = response.device_x25519_pubkey_bytes().unwrap();
256 let initiator_secret = session.complete_exchange(&device_x25519_bytes).unwrap();
257
258 assert_eq!(*initiator_secret, *responder_secret);
260 }
261
262 #[test]
263 fn test_session_consumed_prevents_reuse() {
264 let now = chrono::Utc::now();
265 let mut session = make_token();
266 let (seed, pubkey) = generate_test_seed_and_pubkey();
267
268 let (response, _) = PairingResponse::create(
269 now,
270 &session.token,
271 &seed,
272 &pubkey,
273 "did:key:z6MkTest".to_string(),
274 None,
275 )
276 .unwrap();
277
278 let device_x25519_bytes = response.device_x25519_pubkey_bytes().unwrap();
279
280 assert!(session.complete_exchange(&device_x25519_bytes).is_ok());
282
283 let result = session.complete_exchange(&device_x25519_bytes);
285 assert!(matches!(result, Err(PairingError::SessionConsumed)));
286 }
287}