1use crate::proto::{opack, tlv::TlvBuffer};
20use chacha20poly1305::{
21 aead::{Aead, KeyInit},
22 ChaCha20Poly1305,
23};
24use ed25519_dalek::{SigningKey, VerifyingKey};
25use hkdf::Hkdf;
26use rand::rngs::OsRng;
27use sha2::{Digest, Sha512};
28use uuid::Uuid;
29
30const TYPE_METHOD: u8 = 0x00;
33const TYPE_IDENTIFIER: u8 = 0x01;
34const TYPE_PUBLIC_KEY: u8 = 0x03;
35const TYPE_PROOF: u8 = 0x04;
36const TYPE_ENCRYPTED_DATA: u8 = 0x05;
37const TYPE_STATE: u8 = 0x06;
38const TYPE_SIGNATURE: u8 = 0x0A;
39const TYPE_INFO: u8 = 0x11;
40
41const STATE_START_REQUEST: u8 = 0x01;
44const STATE_VERIFY_REQUEST: u8 = 0x03;
45const STATE_PHASE5: u8 = 0x05;
46
47pub struct HostIdentity {
50 pub identifier: String,
51 pub signing_key: SigningKey,
52}
53
54impl HostIdentity {
55 pub fn generate() -> Self {
56 let mut rng = OsRng;
57 let signing_key = SigningKey::generate(&mut rng);
58 Self {
59 identifier: Uuid::new_v4().to_string().to_uppercase(),
60 signing_key,
61 }
62 }
63
64 pub fn from_private_key_bytes(
65 identifier: impl Into<String>,
66 private_key: &[u8],
67 ) -> Result<Self, PairingError> {
68 let private_key: [u8; 32] = private_key.try_into().map_err(|_| {
69 PairingError::Crypto(format!(
70 "expected 32-byte Ed25519 private key seed, got {} bytes",
71 private_key.len()
72 ))
73 })?;
74 Ok(Self {
75 identifier: identifier.into(),
76 signing_key: SigningKey::from_bytes(&private_key),
77 })
78 }
79
80 pub fn public_key_bytes(&self) -> Vec<u8> {
81 VerifyingKey::from(&self.signing_key).to_bytes().to_vec()
82 }
83
84 pub fn private_key_bytes(&self) -> Vec<u8> {
85 self.signing_key.to_bytes().to_vec()
86 }
87
88 pub fn sign(&self, msg: &[u8]) -> Vec<u8> {
89 use ed25519_dalek::Signer;
90 self.signing_key.sign(msg).to_bytes().to_vec()
91 }
92}
93
94pub struct SrpSession {
100 pub client_public: Vec<u8>,
101 pub client_proof: Vec<u8>,
102 pub session_key: Vec<u8>,
103 verifier: SrpVerifier,
105}
106
107struct SrpVerifier {
108 m2_expected: Vec<u8>,
109}
110
111impl SrpSession {
112 pub fn new(salt: &[u8], device_public: &[u8]) -> Result<Self, PairingError> {
114 let inner = {
116 let mut h = Sha512::new();
117 h.update(b"Pair-Setup:000000");
118 h.finalize()
119 };
120 let x_hash = {
121 let mut h = Sha512::new();
122 h.update(salt);
123 h.update(inner);
124 h.finalize()
125 };
126
127 srp_compute(salt, device_public, &x_hash)
129 }
130
131 pub fn verify_server_proof(&self, server_proof: &[u8]) -> bool {
132 server_proof == self.verifier.m2_expected.as_slice()
133 }
134}
135
136fn srp_compute(salt: &[u8], device_public_b: &[u8], x: &[u8]) -> Result<SrpSession, PairingError> {
150 use num_bigint::BigUint;
151 use num_traits::One;
152
153 let n_hex = concat!(
155 "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1",
156 "29024E088A67CC74020BBEA63B139B22514A08798E3404DD",
157 "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245",
158 "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED",
159 "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D",
160 "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F",
161 "83655D23DCA3AD961C62F356208552BB9ED529077096966D",
162 "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B",
163 "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9",
164 "DE2BCBF6955817183995497CEA956AE515D2261898FA0510",
165 "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64",
166 "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7",
167 "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B",
168 "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C",
169 "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31",
170 "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"
171 );
172 let g = BigUint::from(5u32);
173 let n = BigUint::parse_bytes(n_hex.as_bytes(), 16)
174 .ok_or(PairingError::Crypto("SRP: invalid N".into()))?;
175
176 let k = {
178 let n_bytes = n.to_bytes_be();
179 let mut g_bytes = vec![0u8; n_bytes.len()];
180 let g_b = g.to_bytes_be();
181 g_bytes[n_bytes.len() - g_b.len()..].copy_from_slice(&g_b);
182 let mut h = Sha512::new();
183 h.update(&n_bytes);
184 h.update(&g_bytes);
185 BigUint::from_bytes_be(&h.finalize())
186 };
187
188 let a_secret: [u8; 32] = rand::random();
190 let a = BigUint::from_bytes_be(&a_secret);
191 let big_a = g.modpow(&a, &n);
193 let big_a_bytes = big_a.to_bytes_be();
194
195 let big_b = BigUint::from_bytes_be(device_public_b);
197
198 let n_len = n.to_bytes_be().len();
200 let u = {
201 let mut a_padded = vec![0u8; n_len.saturating_sub(big_a_bytes.len())];
202 a_padded.extend_from_slice(&big_a_bytes);
203 let b_bytes = big_b.to_bytes_be();
204 let mut b_padded = vec![0u8; n_len.saturating_sub(b_bytes.len())];
205 b_padded.extend_from_slice(&b_bytes);
206 let mut h = Sha512::new();
207 h.update(&a_padded);
208 h.update(&b_padded);
209 BigUint::from_bytes_be(&h.finalize())
210 };
211
212 let x_big = BigUint::from_bytes_be(x);
214
215 let v = g.modpow(&x_big, &n);
217
218 let kv = (k * &v) % &n;
220 let base = if big_b >= kv {
221 (big_b - kv) % &n
222 } else {
223 return Err(PairingError::Crypto("SRP: B < k*v".into()));
224 };
225 let exp = (&a + &u * &x_big) % (&n - BigUint::one());
226 let s = base.modpow(&exp, &n);
227 let s_bytes = {
228 let raw = s.to_bytes_be();
229 let mut padded = vec![0u8; n_len.saturating_sub(raw.len())];
230 padded.extend_from_slice(&raw);
231 padded
232 };
233
234 let session_key = {
236 let mut h = Sha512::new();
237 h.update(&s_bytes);
238 h.finalize().to_vec()
239 };
240
241 let h_n = {
243 let mut h = Sha512::new();
244 h.update(n.to_bytes_be());
245 h.finalize()
246 };
247 let h_g = {
248 let mut h = Sha512::new();
249 h.update(g.to_bytes_be());
250 h.finalize()
251 };
252 let xor_ng: Vec<u8> = h_n.iter().zip(h_g.iter()).map(|(a, b)| a ^ b).collect();
253 let h_i = {
254 let mut h = Sha512::new();
255 h.update(b"Pair-Setup");
256 h.finalize()
257 };
258
259 let m1 = {
260 let mut h = Sha512::new();
261 h.update(&xor_ng);
262 h.update(h_i);
263 h.update(salt);
264 h.update(&big_a_bytes);
265 h.update(device_public_b);
266 h.update(&session_key);
267 h.finalize().to_vec()
268 };
269
270 let m2 = {
272 let mut h = Sha512::new();
273 h.update(&big_a_bytes);
274 h.update(&m1);
275 h.update(&session_key);
276 h.finalize().to_vec()
277 };
278
279 Ok(SrpSession {
280 client_public: big_a_bytes,
281 client_proof: m1,
282 session_key,
283 verifier: SrpVerifier { m2_expected: m2 },
284 })
285}
286
287fn hkdf_sha512(ikm: &[u8], salt: &[u8], info: &[u8]) -> Result<[u8; 32], PairingError> {
290 let h = Hkdf::<Sha512>::new(if salt.is_empty() { None } else { Some(salt) }, ikm);
291 let mut out = [0u8; 32];
292 h.expand(info, &mut out)
293 .map_err(|e| PairingError::Crypto(format!("HKDF expand failed: {e}")))?;
294 Ok(out)
295}
296
297fn chacha_nonce(label: &[u8]) -> [u8; 12] {
300 let mut n = [0u8; 12];
301 let end = n.len();
302 let start = end - label.len().min(8);
303 n[start..end].copy_from_slice(&label[..label.len().min(8)]);
304 n
305}
306
307fn chacha_seal(
308 key: &[u8; 32],
309 nonce_label: &[u8],
310 plaintext: &[u8],
311) -> Result<Vec<u8>, PairingError> {
312 let cipher = ChaCha20Poly1305::new_from_slice(key)
313 .map_err(|e| PairingError::Crypto(format!("ChaCha key init failed: {e}")))?;
314 let nonce = chacha20poly1305::Nonce::from(chacha_nonce(nonce_label));
315 cipher
316 .encrypt(&nonce, plaintext)
317 .map_err(|e| PairingError::Crypto(format!("ChaCha seal failed: {e}")))
318}
319
320fn chacha_open(
321 key: &[u8; 32],
322 nonce_label: &[u8],
323 ciphertext: &[u8],
324) -> Result<Vec<u8>, PairingError> {
325 let cipher = ChaCha20Poly1305::new_from_slice(key)
326 .map_err(|e| PairingError::Crypto(format!("ChaCha key init failed: {e}")))?;
327 let nonce = chacha20poly1305::Nonce::from(chacha_nonce(nonce_label));
328 cipher.decrypt(&nonce, ciphertext).map_err(|_| {
329 PairingError::Crypto("ChaCha decrypt failed (wrong key or tampered data)".into())
330 })
331}
332
333#[derive(Debug, thiserror::Error)]
336pub enum PairingError {
337 #[error("IO error: {0}")]
338 Io(#[from] std::io::Error),
339 #[error("crypto error: {0}")]
340 Crypto(String),
341 #[error("protocol error: {0}")]
342 Protocol(String),
343 #[error("user must press Trust on device")]
344 TrustRequired,
345 #[error("server proof verification failed")]
346 ServerProofInvalid,
347}
348
349#[derive(Debug, Clone)]
353pub struct PairingResult {
354 pub host_identifier: String,
355 pub host_public_key: Vec<u8>,
356 }
359
360pub fn build_setup_tlv() -> Vec<u8> {
364 let mut buf = TlvBuffer::new();
365 buf.push_u8(TYPE_METHOD, 0x00);
366 buf.push_u8(TYPE_STATE, STATE_START_REQUEST);
367 buf.into_bytes()
368}
369
370pub fn build_srp_proof_tlv(srp: &SrpSession) -> Vec<u8> {
372 let mut buf = TlvBuffer::new();
373 buf.push_u8(TYPE_STATE, STATE_VERIFY_REQUEST);
374 buf.push_bytes(TYPE_PUBLIC_KEY, &srp.client_public);
375 buf.push_bytes(TYPE_PROOF, &srp.client_proof);
376 buf.into_bytes()
377}
378
379pub fn build_device_info_tlv(
383 session_key: &[u8],
384 identity: &HostIdentity,
385) -> Result<(Vec<u8>, [u8; 32]), PairingError> {
386 let controller_salt = b"Pair-Setup-Controller-Sign-Salt";
388 let controller_info = b"Pair-Setup-Controller-Sign-Info";
389 let sign_key = hkdf_sha512(session_key, controller_salt, controller_info)?;
390
391 let mut sign_msg = sign_key.to_vec();
393 sign_msg.extend_from_slice(identity.identifier.as_bytes());
394 sign_msg.extend_from_slice(&identity.public_key_bytes());
395 let signature = identity.sign(&sign_msg);
396
397 let device_info = opack::encode(&opack::OpackValue::Dict(vec![
403 (
404 opack::OpackValue::String("accountID".into()),
405 opack::OpackValue::String(identity.identifier.clone()),
406 ),
407 (
408 opack::OpackValue::String("altIRK".into()),
409 opack::OpackValue::Bytes(vec![
410 0x5e, 0xca, 0x81, 0x91, 0x92, 0x02, 0x82, 0x00, 0x11, 0x22, 0x33, 0x44, 0xbb, 0xf2,
411 0x4a, 0xc8,
412 ]),
413 ),
414 (
415 opack::OpackValue::String("btAddr".into()),
416 opack::OpackValue::String("FF:DD:99:66:BB:AA".into()),
417 ),
418 (
419 opack::OpackValue::String("mac".into()),
420 opack::OpackValue::Bytes(vec![0xff, 0x44, 0x88, 0x66, 0x33, 0x99]),
421 ),
422 (
423 opack::OpackValue::String("model".into()),
424 opack::OpackValue::String("ios-rs".into()),
425 ),
426 (
427 opack::OpackValue::String("name".into()),
428 opack::OpackValue::String("ios-rs-host".into()),
429 ),
430 (
431 opack::OpackValue::String("remotepairing_serial_number".into()),
432 opack::OpackValue::String("ios-rs-serial".into()),
433 ),
434 ]))
435 .map_err(|e| PairingError::Protocol(e.to_string()))?;
436
437 let mut inner = TlvBuffer::new();
439 inner.push_bytes(TYPE_SIGNATURE, &signature);
440 inner.push_bytes(TYPE_PUBLIC_KEY, &identity.public_key_bytes());
441 inner.push_bytes(TYPE_IDENTIFIER, identity.identifier.as_bytes());
442 inner.push_bytes(TYPE_INFO, &device_info);
443 let inner_bytes = inner.into_bytes();
444
445 let setup_key = hkdf_sha512(
447 session_key,
448 b"Pair-Setup-Encrypt-Salt",
449 b"Pair-Setup-Encrypt-Info",
450 )?;
451 let encrypted = chacha_seal(&setup_key, b"PS-Msg05", &inner_bytes)?;
452
453 let mut outer = TlvBuffer::new();
455 outer.push_u8(TYPE_STATE, STATE_PHASE5);
456 outer.push_bytes(TYPE_ENCRYPTED_DATA, &encrypted);
457
458 Ok((outer.into_bytes(), setup_key))
459}
460
461pub fn verify_device_info_response(
467 setup_key: &[u8; 32],
468 encrypted_data: &[u8],
469) -> Result<(), PairingError> {
470 chacha_open(setup_key, b"PS-Msg06", encrypted_data)?;
471 Ok(())
472}
473
474pub fn derive_cipher_keys(session_key: &[u8]) -> Result<([u8; 32], [u8; 32]), PairingError> {
476 let client_key = hkdf_sha512(session_key, &[], b"ClientEncrypt-main")?;
477 let server_key = hkdf_sha512(session_key, &[], b"ServerEncrypt-main")?;
478 Ok((client_key, server_key))
479}
480
481pub fn build_verify_start_tlv(x25519_pub: &[u8]) -> Vec<u8> {
485 let mut buf = TlvBuffer::new();
486 buf.push_u8(TYPE_STATE, STATE_START_REQUEST);
487 buf.push_bytes(TYPE_PUBLIC_KEY, x25519_pub);
488 buf.into_bytes()
489}
490
491#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct VerifyPairSession {
496 pub tlv: Vec<u8>,
497 pub encryption_key: [u8; 32],
498 pub client_key: [u8; 32],
499 pub server_key: [u8; 32],
500}
501
502pub fn build_verify_step2_tlv(
505 our_secret: [u8; 32], our_public: &[u8; 32], device_public: &[u8; 32], identity: &HostIdentity,
509) -> Result<VerifyPairSession, PairingError> {
510 use x25519_dalek::{PublicKey as X25519Pub, StaticSecret};
512 let our = StaticSecret::from(our_secret);
513 let dev = X25519Pub::from(*device_public);
514 let shared = our.diffie_hellman(&dev).to_bytes();
515
516 let derived = hkdf_sha512(
518 &shared,
519 b"Pair-Verify-Encrypt-Salt",
520 b"Pair-Verify-Encrypt-Info",
521 )?;
522
523 let mut sign_msg = our_public.to_vec();
525 sign_msg.extend_from_slice(identity.identifier.as_bytes());
526 sign_msg.extend_from_slice(device_public);
527 let sig = identity.sign(&sign_msg);
528
529 let mut inner = TlvBuffer::new();
531 inner.push_bytes(TYPE_SIGNATURE, &sig);
532 inner.push_bytes(TYPE_IDENTIFIER, identity.identifier.as_bytes());
533 let inner_bytes = inner.into_bytes();
534 let encrypted = chacha_seal(&derived, b"PV-Msg03", &inner_bytes)?;
535
536 let mut outer = TlvBuffer::new();
537 outer.push_u8(TYPE_STATE, STATE_VERIFY_REQUEST);
538 outer.push_bytes(TYPE_ENCRYPTED_DATA, &encrypted);
539
540 let client_key = hkdf_sha512(&shared, &[], b"ClientEncrypt-main")?;
542 let server_key = hkdf_sha512(&shared, &[], b"ServerEncrypt-main")?;
543
544 Ok(VerifyPairSession {
545 tlv: outer.into_bytes(),
546 encryption_key: shared,
547 client_key,
548 server_key,
549 })
550}
551
552pub fn verify_pair_step2(
553 our_secret: [u8; 32], our_public: &[u8; 32], device_public: &[u8; 32], identity: &HostIdentity,
557) -> Result<([u8; 32], [u8; 32]), PairingError> {
558 let session = build_verify_step2_tlv(our_secret, our_public, device_public, identity)?;
559 Ok((session.client_key, session.server_key))
560}
561
562#[cfg(test)]
563mod tests {
564 use bytes::Bytes;
565
566 use super::*;
567
568 #[test]
569 fn test_host_identity_generation() {
570 let id = HostIdentity::generate();
571 assert_eq!(id.identifier.len(), 36);
572 assert_eq!(id.public_key_bytes().len(), 32);
573 assert_eq!(id.private_key_bytes().len(), 32);
574 }
575
576 #[test]
577 fn test_chacha_roundtrip() {
578 let key = [0u8; 32];
579 let plaintext = b"hello pairing world";
580 let ct = chacha_seal(&key, b"PS-Msg05", plaintext).unwrap();
581 let pt = chacha_open(&key, b"PS-Msg05", &ct).unwrap();
582 assert_eq!(pt, plaintext);
583 }
584
585 #[test]
586 fn test_hkdf_sha512_deterministic() {
587 let k1 = hkdf_sha512(b"session_key", b"salt", b"ClientEncrypt-main").unwrap();
588 let k2 = hkdf_sha512(b"session_key", b"salt", b"ClientEncrypt-main").unwrap();
589 assert_eq!(k1, k2);
590 let k3 = hkdf_sha512(b"session_key", b"salt", b"ServerEncrypt-main").unwrap();
591 assert_ne!(k1, k3);
592 }
593
594 #[test]
595 fn test_build_setup_tlv() {
596 let tlv = build_setup_tlv();
597 assert!(tlv.len() >= 6);
599 assert_eq!(tlv[0], TYPE_METHOD);
600 assert_eq!(tlv[3], TYPE_STATE);
601 assert_eq!(tlv[5], STATE_START_REQUEST);
602 }
603
604 #[test]
605 fn test_derive_cipher_keys_different() {
606 let (ck, sk) = derive_cipher_keys(b"test_session_key").unwrap();
607 assert_ne!(ck, sk);
608 assert_eq!(ck.len(), 32);
609 assert_eq!(sk.len(), 32);
610 }
611
612 #[test]
613 fn test_device_info_tlv() {
614 let identity = HostIdentity::generate();
615 let session_key = vec![0x42u8; 64];
616 let (tlv, setup_key) = build_device_info_tlv(&session_key, &identity).unwrap();
617 assert!(!tlv.is_empty());
618 assert_eq!(setup_key.len(), 32);
619 }
620
621 #[test]
622 fn test_build_verify_step2_tlv_returns_state_and_keys() {
623 let identity = HostIdentity::generate();
624 let our_secret = [0x11; 32];
625 let our_static = x25519_dalek::StaticSecret::from(our_secret);
626 let our_public = x25519_dalek::PublicKey::from(&our_static).to_bytes();
627 let device_secret = [0x22; 32];
628 let device_static = x25519_dalek::StaticSecret::from(device_secret);
629 let device_public = x25519_dalek::PublicKey::from(&device_static).to_bytes();
630
631 let session =
632 build_verify_step2_tlv(our_secret, &our_public, &device_public, &identity).unwrap();
633
634 let decoded = TlvBuffer::decode(&session.tlv);
635 assert_eq!(
636 decoded.get(&TYPE_STATE).map(Bytes::as_ref),
637 Some(&[STATE_VERIFY_REQUEST][..])
638 );
639 assert!(decoded
640 .get(&TYPE_ENCRYPTED_DATA)
641 .is_some_and(|value| !value.is_empty()));
642 assert_ne!(session.client_key, session.server_key);
643 assert_ne!(session.encryption_key, [0u8; 32]);
644 }
645}