Skip to main content

wacore/
pair.rs

1use crate::libsignal::protocol::{KeyPair, PublicKey};
2use aes_gcm::Aes256Gcm;
3use aes_gcm::aead::{Aead, KeyInit, Payload};
4use base64::Engine as _;
5use base64::prelude::*;
6use hkdf::Hkdf;
7use hmac::{Hmac, Mac};
8use prost::Message;
9
10use sha2::Sha256;
11use wacore_binary::builder::NodeBuilder;
12use wacore_binary::jid::{Jid, SERVER_JID};
13use wacore_binary::node::Node;
14use waproto::whatsapp as wa;
15use waproto::whatsapp::AdvEncryptionType;
16
17// Prefixes from whatsmeow/pair.go, crucial for signature verification
18const ADV_PREFIX_ACCOUNT_SIGNATURE: &[u8] = &[6, 0];
19const ADV_PREFIX_DEVICE_SIGNATURE_GENERATE: &[u8] = &[6, 1];
20const ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE: &[u8] = &[6, 5];
21const ADV_HOSTED_PREFIX_DEVICE_SIGNATURE_VERIFICATION: &[u8] = &[6, 6];
22
23// Aliases for HMAC-SHA256
24type HmacSha256 = Hmac<Sha256>;
25
26#[derive(Debug)]
27pub struct PairCryptoError {
28    pub code: u16,
29    pub text: &'static str,
30    pub source: anyhow::Error,
31}
32
33impl std::fmt::Display for PairCryptoError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        write!(
36            f,
37            "pairing crypto failed with code {}: {} (source: {})",
38            self.code, self.text, self.source
39        )
40    }
41}
42
43impl std::error::Error for PairCryptoError {}
44
45/// Device state needed for pairing operations
46pub struct DeviceState {
47    pub identity_key: KeyPair,
48    pub noise_key: KeyPair,
49    pub adv_secret_key: [u8; 32],
50}
51
52/// Core pairing utilities that are platform-independent
53pub struct PairUtils;
54
55impl PairUtils {
56    /// Constructs the full QR code string from the ref and device keys.
57    pub fn make_qr_data(device_state: &DeviceState, ref_str: String) -> String {
58        let noise_b64 =
59            BASE64_STANDARD.encode(device_state.noise_key.public_key.public_key_bytes());
60        let identity_b64 =
61            BASE64_STANDARD.encode(device_state.identity_key.public_key.public_key_bytes());
62        let adv_b64 = BASE64_STANDARD.encode(device_state.adv_secret_key);
63
64        [ref_str, noise_b64, identity_b64, adv_b64].join(",")
65    }
66
67    /// Builds acknowledgment node for a pairing request
68    pub fn build_ack_node(request_node: &Node) -> Option<Node> {
69        if let (Some(to), Some(id)) = (request_node.attrs.get("from"), request_node.attrs.get("id"))
70        {
71            Some(
72                NodeBuilder::new("iq")
73                    .attrs([
74                        ("to", to.to_string()),
75                        ("id", id.to_string()),
76                        ("type", "result".to_string()),
77                    ])
78                    .build(),
79            )
80        } else {
81            None
82        }
83    }
84
85    /// Builds pair error node
86    pub fn build_pair_error_node(req_id: &str, code: u16, text: &str) -> Node {
87        let error_node = NodeBuilder::new("error")
88            .attrs([("code", code.to_string()), ("text", text.to_string())])
89            .build();
90        NodeBuilder::new("iq")
91            .attrs([
92                ("to", SERVER_JID.to_string()),
93                ("type", "error".to_string()),
94                ("id", req_id.to_string()),
95            ])
96            .children([error_node])
97            .build()
98    }
99
100    /// Performs the cryptographic operations for pairing
101    pub fn do_pair_crypto(
102        device_state: &DeviceState,
103        device_identity_bytes: &[u8],
104    ) -> Result<(Vec<u8>, u32), PairCryptoError> {
105        // 1. Unmarshal HMAC container and verify HMAC
106        let hmac_container = wa::AdvSignedDeviceIdentityHmac::decode(device_identity_bytes)
107            .map_err(|e| PairCryptoError {
108                code: 500,
109                text: "internal-error",
110                source: e.into(),
111            })?;
112
113        // Determine if this is a hosted account
114        let is_hosted_account = hmac_container.account_type.is_some()
115            && hmac_container.account_type() == AdvEncryptionType::Hosted;
116
117        let mut mac =
118            <HmacSha256 as Mac>::new_from_slice(&device_state.adv_secret_key).map_err(|e| {
119                PairCryptoError {
120                    code: 500,
121                    text: "internal-error",
122                    source: e.into(),
123                }
124            })?;
125        // Get details and hmac as slices, handling potential None values
126        let details_bytes = hmac_container
127            .details
128            .as_deref()
129            .ok_or_else(|| PairCryptoError {
130                code: 500,
131                text: "internal-error",
132                source: anyhow::anyhow!("HMAC container missing details"),
133            })?;
134        let _hmac_bytes = hmac_container
135            .hmac
136            .as_deref()
137            .ok_or_else(|| PairCryptoError {
138                code: 500,
139                text: "internal-error",
140                source: anyhow::anyhow!("HMAC container missing hmac"),
141            })?;
142
143        if is_hosted_account {
144            mac.update(ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE);
145        }
146        mac.update(details_bytes);
147        // TODO(security): HMAC verification skipped — adv_secret_key is only
148        // rotated in the pair-code flow (see handle_pair_code_notification() in
149        // pair_code.rs, via DeviceCommand::SetAdvSecretKey). QR pairing uses
150        // the initial random key from Device::new() which won't match.
151        // Re-enable once both pairing paths persist the correct key.
152
153        // 2. Unmarshal inner container and verify account signature
154        let mut signed_identity =
155            wa::AdvSignedDeviceIdentity::decode(details_bytes).map_err(|e| PairCryptoError {
156                code: 500,
157                text: "internal-error",
158                source: e.into(),
159            })?;
160
161        let account_sig_key_bytes = signed_identity.account_signature_key();
162        let account_sig_bytes = signed_identity.account_signature();
163        let inner_details_bytes = signed_identity.details().to_vec();
164
165        let account_sig_prefix = if is_hosted_account {
166            ADV_HOSTED_PREFIX_ACCOUNT_SIGNATURE
167        } else {
168            ADV_PREFIX_ACCOUNT_SIGNATURE
169        };
170
171        let msg_to_verify = Self::concat_bytes(&[
172            account_sig_prefix,
173            &inner_details_bytes,
174            device_state.identity_key.public_key.public_key_bytes(),
175        ]);
176
177        let account_public_key = PublicKey::from_djb_public_key_bytes(account_sig_key_bytes)
178            .map_err(|e| PairCryptoError {
179                code: 401,
180                text: "invalid-key",
181                source: e.into(),
182            })?;
183
184        if !account_public_key.verify_signature(&msg_to_verify, account_sig_bytes) {
185            return Err(PairCryptoError {
186                code: 401,
187                text: "signature-mismatch",
188                source: anyhow::anyhow!("libsignal signature verification failed"),
189            });
190        }
191
192        // 3. Generate our device signature
193        let device_sig_prefix = if is_hosted_account {
194            ADV_HOSTED_PREFIX_DEVICE_SIGNATURE_VERIFICATION
195        } else {
196            ADV_PREFIX_DEVICE_SIGNATURE_GENERATE
197        };
198
199        let msg_to_sign = Self::concat_bytes(&[
200            device_sig_prefix,
201            &inner_details_bytes,
202            device_state.identity_key.public_key.public_key_bytes(),
203            account_sig_key_bytes,
204        ]);
205        let device_signature = device_state
206            .identity_key
207            .private_key
208            .calculate_signature(&msg_to_sign, &mut rand::make_rng::<rand::rngs::StdRng>())
209            .map_err(|e| PairCryptoError {
210                code: 500,
211                text: "internal-error",
212                source: e.into(),
213            })?;
214        signed_identity.device_signature = Some(device_signature.to_vec());
215
216        // 4. Unmarshal final details to get key_index
217        let identity_details =
218            wa::AdvDeviceIdentity::decode(&*inner_details_bytes).map_err(|e| PairCryptoError {
219                code: 500,
220                text: "internal-error",
221                source: e.into(),
222            })?;
223        let key_index = identity_details.key_index();
224
225        // 5. Marshal the modified signed_identity to send back
226        let self_signed_identity_bytes = signed_identity.encode_to_vec();
227
228        Ok((self_signed_identity_bytes, key_index))
229    }
230
231    /// Builds the pair-device-sign response node
232    pub fn build_pair_success_response(
233        req_id: &str,
234        self_signed_identity_bytes: Vec<u8>,
235        key_index: u32,
236    ) -> Node {
237        let response_content = NodeBuilder::new("pair-device-sign")
238            .children([NodeBuilder::new("device-identity")
239                .attr("key-index", key_index.to_string())
240                .bytes(self_signed_identity_bytes)
241                .build()])
242            .build();
243        NodeBuilder::new("iq")
244            .attrs([
245                ("to", SERVER_JID.to_string()),
246                ("id", req_id.to_string()),
247                ("type", "result".to_string()),
248            ])
249            .children([response_content])
250            .build()
251    }
252
253    /// Parses QR code and extracts crypto keys for pairing
254    pub fn parse_qr_code(qr_code: &str) -> Result<(String, [u8; 32], [u8; 32]), anyhow::Error> {
255        let parts: Vec<&str> = qr_code.split(',').collect();
256        if parts.len() != 4 {
257            return Err(anyhow::anyhow!("Invalid QR code format"));
258        }
259        let pairing_ref = parts[0].to_string();
260        let dut_noise_pub_b64 = parts[1];
261        let dut_identity_pub_b64 = parts[2];
262        // The ADV secret is not used by the phone side.
263
264        let dut_noise_pub_bytes = BASE64_STANDARD
265            .decode(dut_noise_pub_b64)
266            .map_err(|e| anyhow::anyhow!(e))?;
267        let dut_identity_pub_bytes = BASE64_STANDARD
268            .decode(dut_identity_pub_b64)
269            .map_err(|e| anyhow::anyhow!(e))?;
270
271        let dut_noise_pub: [u8; 32] = dut_noise_pub_bytes
272            .try_into()
273            .map_err(|_| anyhow::anyhow!("Invalid noise public key length"))?;
274        let dut_identity_pub: [u8; 32] = dut_identity_pub_bytes
275            .try_into()
276            .map_err(|_| anyhow::anyhow!("Invalid identity public key length"))?;
277
278        Ok((pairing_ref, dut_noise_pub, dut_identity_pub))
279    }
280
281    /// Prepares pairing message for master device (phone simulation)
282    pub fn prepare_master_pairing_message(
283        device_state: &DeviceState,
284        pairing_ref: &str,
285        dut_noise_pub: &[u8; 32],
286        dut_identity_pub: &[u8; 32],
287        master_ephemeral: KeyPair,
288    ) -> Result<Vec<u8>, anyhow::Error> {
289        // Perform the cryptographic exchange to create the shared secrets
290        let adv_key = &device_state.adv_secret_key;
291        let identity_key = &device_state.identity_key;
292
293        let mut mac = <HmacSha256 as Mac>::new_from_slice(adv_key)
294            .map_err(|e| anyhow::anyhow!("Failed to init HMAC for master pairing: {e}"))?;
295        mac.update(ADV_PREFIX_ACCOUNT_SIGNATURE);
296        mac.update(dut_identity_pub);
297        mac.update(master_ephemeral.public_key.public_key_bytes());
298        let account_signature = mac.finalize().into_bytes();
299
300        let their_public_key = PublicKey::from_djb_public_key_bytes(dut_noise_pub)?;
301        let shared_secret = master_ephemeral
302            .private_key
303            .calculate_agreement(&their_public_key)?;
304
305        let mut final_message = Vec::new();
306        final_message.extend_from_slice(&account_signature);
307        final_message.extend_from_slice(master_ephemeral.public_key.public_key_bytes());
308        final_message.extend_from_slice(identity_key.public_key.public_key_bytes());
309
310        // Encrypt the final message
311        let encryption_key = {
312            let hk = Hkdf::<Sha256>::new(None, &shared_secret);
313            let mut result = vec![0u8; 32];
314            hk.expand(b"WA-Ads-Key", &mut result)
315                .map_err(|_| anyhow::anyhow!("HKDF expand failed"))?;
316            result
317        };
318        let cipher = Aes256Gcm::new_from_slice(&encryption_key)
319            .map_err(|_| anyhow::anyhow!("Invalid key size for AES-GCM"))?;
320        let nonce: aes_gcm::Nonce<_> = [0u8; 12].into();
321        let payload = Payload {
322            msg: &final_message,
323            aad: pairing_ref.as_bytes(),
324        };
325        let encrypted = cipher
326            .encrypt(&nonce, payload)
327            .map_err(|_| anyhow::anyhow!("AES-GCM encryption failed"))?;
328
329        Ok(encrypted)
330    }
331
332    /// Builds pairing IQ for master device
333    pub fn build_master_pair_iq(
334        master_jid: &Jid,
335        encrypted_message: Vec<u8>,
336        req_id: String,
337    ) -> Node {
338        let response_content = NodeBuilder::new("pair-device-sign")
339            .attr("jid", master_jid.clone())
340            .bytes(encrypted_message)
341            .build();
342        NodeBuilder::new("iq")
343            .attrs([
344                ("to", SERVER_JID.to_string()),
345                ("type", "set".to_string()),
346                ("id", req_id),
347                ("xmlns", "md".to_string()),
348            ])
349            .children([response_content])
350            .build()
351    }
352
353    /// Helper to concatenate multiple byte slices into a single Vec.
354    fn concat_bytes(slices: &[&[u8]]) -> Vec<u8> {
355        slices.iter().flat_map(|s| s.iter().cloned()).collect()
356    }
357}