Skip to main content

ios_core/lockdown/
pairing.rs

1//! SRP (Secure Remote Password) pairing for new iOS 17+ devices.
2//!
3//! Implements the complete pairing handshake that runs when connecting to a new
4//! (untrusted) device for the first time. The user must press "Trust" on the device.
5//!
6//! Service: `com.apple.internal.dt.coredevice.untrusted.tunnelservice`
7//! Accessed via: RSD on the device's USB-Ethernet or mDNS IPv6 address (port 58783)
8//!
9//! Flow:
10//! 1. Send handshake (wireProtocolVersion=19, attemptPairVerify=true)
11//! 2. Try verifyPair() [if we already have keys] – skip if no keys
12//! 3. setupManualPairing() – TLV{typeMethod=0, typeState=1}
13//! 4. readDeviceKey() – receive device's SRP salt + public key
14//! 5. setupSessionKey() – SRP-3072-SHA512, derive session key
15//! 6. exchangeDeviceInfo() – HKDF → Ed25519 sign → ChaCha encrypt device info
16//! 7. setupCiphers() – HKDF → two ChaCha20Poly1305 streams
17//! 8. createUnlockKey() – final handshake to finish pairing
18
19use 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
30// ── TLV type codes (from go-ios tunnel/tlvbuffer.go) ──────────────────────────
31
32const 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
41// ── Pair state values ─────────────────────────────────────────────────────────
42
43const STATE_START_REQUEST: u8 = 0x01;
44const STATE_VERIFY_REQUEST: u8 = 0x03;
45const STATE_PHASE5: u8 = 0x05;
46
47// ── Identity (host keys, generated fresh each pairing) ────────────────────────
48
49pub 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
94// ── SRP session ───────────────────────────────────────────────────────────────
95
96/// SRP-3072-SHA512 session, matching go-ios srp.go.
97///
98/// Custom password hash: SHA512(salt || SHA512("Pair-Setup:000000"))
99pub struct SrpSession {
100    pub client_public: Vec<u8>,
101    pub client_proof: Vec<u8>,
102    pub session_key: Vec<u8>,
103    // Internal SRP state for server proof verification
104    verifier: SrpVerifier,
105}
106
107struct SrpVerifier {
108    m2_expected: Vec<u8>,
109}
110
111impl SrpSession {
112    /// Initialize SRP session from the device's salt and public key.
113    pub fn new(salt: &[u8], device_public: &[u8]) -> Result<Self, PairingError> {
114        // Custom password derivation: SHA512(salt || SHA512("Pair-Setup:000000"))
115        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        // RFC 5054 SRP-3072 with the custom x
128        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
136/// Minimal SRP-3072 computation without the srp crate (uses BigUint arithmetic).
137///
138/// This implements SRP-6a as per RFC 5054, Group 3072.
139///
140/// Algorithm overview (SRP-3072-SHA512):
141///   1. k = H(N || pad(g))           — multiplier parameter
142///   2. x = H(salt || H(I || ":" || P))  — derived by caller as password hash
143///   3. A = g^a mod N                — client ephemeral public key
144///   4. u = H(pad(A) || pad(B))      — scrambling parameter
145///   5. S = (B - k*g^x)^(a + u*x) mod N  — premaster secret
146///   6. K = H(S)                     — session key
147///   7. M1 = H(H(N) XOR H(g) || H(I) || salt || A || B || K)  — client proof
148///   8. M2 = H(A || M1 || K)         — expected server proof
149fn 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    // RFC 5054 3072-bit group
154    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    // Step 1: k = H(N || pad(g)) — SRP multiplier parameter
177    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    // Step 2: Generate ephemeral private a (random 256-bit scalar)
189    let a_secret: [u8; 32] = rand::random();
190    let a = BigUint::from_bytes_be(&a_secret);
191    // Step 3: A = g^a mod N — client ephemeral public key
192    let big_a = g.modpow(&a, &n);
193    let big_a_bytes = big_a.to_bytes_be();
194
195    // B (device public)
196    let big_b = BigUint::from_bytes_be(device_public_b);
197
198    // Step 4: u = H(pad(A) || pad(B)) — scrambling parameter
199    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    // x already derived by caller: x = H(salt || H("Pair-Setup:000000"))
213    let x_big = BigUint::from_bytes_be(x);
214
215    // v = g^x mod N (password verifier, used to compute premaster secret)
216    let v = g.modpow(&x_big, &n);
217
218    // Step 5: S = (B - k*v)^(a + u*x) mod N — premaster secret
219    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    // Step 6: K = H(S) — session key derived from premaster secret
235    let session_key = {
236        let mut h = Sha512::new();
237        h.update(&s_bytes);
238        h.finalize().to_vec()
239    };
240
241    // Step 7: M1 = H(H(N) XOR H(g) || H(I) || salt || A || B || K) — client proof
242    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    // Step 8: M2 (expected server proof) = H(A || M1 || K) — for verifying device
271    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
287// ── HKDF helpers ──────────────────────────────────────────────────────────────
288
289fn 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
297// ── ChaCha20Poly1305 helpers ──────────────────────────────────────────────────
298
299fn 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// ── Error type ────────────────────────────────────────────────────────────────
334
335#[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// ── Pairing result ────────────────────────────────────────────────────────────
350
351/// Host identity and session info to be persisted as a pair record.
352#[derive(Debug, Clone)]
353pub struct PairingResult {
354    pub host_identifier: String,
355    pub host_public_key: Vec<u8>,
356    // In a full implementation, we'd also store the device's public key
357    // and the session's derived key material for future verifyPair().
358}
359
360// ── Pure crypto step functions (testable without network) ─────────────────────
361
362/// Build the TLV bytes for the setupManualPairing initiation (State 1).
363pub 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
370/// Build the TLV bytes for the SRP proof message (State 3).
371pub 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
379/// Build the TLV bytes for the device info exchange (State 5).
380///
381/// Returns (tlv_bytes, setup_key) where setup_key is needed to decrypt the response.
382pub fn build_device_info_tlv(
383    session_key: &[u8],
384    identity: &HostIdentity,
385) -> Result<(Vec<u8>, [u8; 32]), PairingError> {
386    // 1. HKDF for controller sign
387    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    // 2. Sign: H_controller || identifier || ed25519_public
392    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    // 3. Opack-encode device info
398    // The altIRK, btAddr, mac, remotepairing_serial_number values below are
399    // hardcoded dummy placeholders required by the Apple pairing protocol.
400    // The device expects these fields to be present but does not validate their
401    // actual content for a host-side pairing session.
402    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    // 4. Build inner TLV
438    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    // 5. Derive encryption key + encrypt
446    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    // 6. Outer TLV
454    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
461/// Verify the device info response (State 6).
462///
463/// Decrypts the device's encrypted TLV response using the setup key derived
464/// during the device info exchange. A successful decryption proves the device
465/// holds the same session key, completing mutual authentication.
466pub 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
474/// Derive the two session cipher keys from the SRP session key.
475pub 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
481// ── VerifyPair (for already-paired devices) ───────────────────────────────────
482
483/// Build the TLV for the pair verify initiation.
484pub 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/// Complete the pair verify handshake (step 2).
492///
493/// Returns the derived cipher keys (client_key, server_key) on success.
494#[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
502/// Build the pair verify step-2 TLV and derive the keys required for the
503/// subsequent encrypted control channel and TLS-PSK listener connection.
504pub fn build_verify_step2_tlv(
505    our_secret: [u8; 32],     // x25519 secret scalar bytes
506    our_public: &[u8; 32],    // x25519 public
507    device_public: &[u8; 32], // from device TLV
508    identity: &HostIdentity,
509) -> Result<VerifyPairSession, PairingError> {
510    // ECDH
511    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    // Derive encryption key
517    let derived = hkdf_sha512(
518        &shared,
519        b"Pair-Verify-Encrypt-Salt",
520        b"Pair-Verify-Encrypt-Info",
521    )?;
522
523    // Sign: our_public || identifier || device_public
524    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    // Encrypt
530    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    // Session keys from shared secret
541    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],     // x25519 secret scalar bytes
554    our_public: &[u8; 32],    // x25519 public
555    device_public: &[u8; 32], // from device TLV
556    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        // Should have: [TYPE_METHOD=0, len=1, val=0, TYPE_STATE=6, len=1, val=1]
598        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}