whatsapp_rust/
pair_code.rs1use crate::client::Client;
49use crate::request::{InfoQuery, InfoQueryType, IqError};
50use crate::types::events::Event;
51use log::{error, info, warn};
52use rand::TryRngCore;
53use std::sync::Arc;
54use wacore::libsignal::protocol::KeyPair;
55use wacore::pair_code::{PairCodeError, PairCodeState, PairCodeUtils};
56use wacore_binary::jid::{Jid, SERVER_JID};
57use wacore_binary::node::{Node, NodeContent};
58
59pub use wacore::pair_code::{PairCodeOptions, PlatformId};
61
62impl Client {
63 pub async fn pair_with_code(
99 self: &Arc<Self>,
100 options: PairCodeOptions,
101 ) -> Result<String, PairCodeError> {
102 let phone_number: String = options
104 .phone_number
105 .chars()
106 .filter(|c| c.is_ascii_digit())
107 .collect();
108
109 if phone_number.is_empty() {
111 return Err(PairCodeError::PhoneNumberRequired);
112 }
113 if phone_number.len() < 7 {
114 return Err(PairCodeError::PhoneNumberTooShort);
115 }
116 if phone_number.starts_with('0') {
117 return Err(PairCodeError::PhoneNumberNotInternational);
118 }
119
120 let code = match &options.custom_code {
122 Some(custom) => {
123 if !PairCodeUtils::validate_code(custom) {
124 return Err(PairCodeError::InvalidCustomCode);
125 }
126 custom.to_uppercase()
127 }
128 None => PairCodeUtils::generate_code(),
129 };
130
131 info!(
132 target: "Client/PairCode",
133 "Starting pair code authentication for phone: {}",
134 phone_number
135 );
136
137 let ephemeral_keypair = KeyPair::generate(&mut rand::rngs::OsRng.unwrap_err());
139
140 let device_snapshot = self.persistence_manager.get_device_snapshot().await;
142 let noise_static_pub: [u8; 32] = device_snapshot
143 .noise_key
144 .public_key
145 .public_key_bytes()
146 .try_into()
147 .expect("noise key is 32 bytes");
148
149 let code_clone = code.clone();
152 let ephemeral_pub: [u8; 32] = ephemeral_keypair
153 .public_key
154 .public_key_bytes()
155 .try_into()
156 .expect("ephemeral key is 32 bytes");
157
158 let wrapped_ephemeral = tokio::task::spawn_blocking(move || {
159 PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, &code_clone)
160 })
161 .await
162 .map_err(|e| PairCodeError::CryptoError(format!("spawn_blocking failed: {e}")))?;
163
164 let req_id = self.generate_request_id();
166 let iq_content = PairCodeUtils::build_companion_hello_iq(
167 &phone_number,
168 &noise_static_pub,
169 &wrapped_ephemeral,
170 options.platform_id,
171 &options.platform_display,
172 options.show_push_notification,
173 req_id.clone(),
174 );
175
176 let query = InfoQuery {
178 query_type: InfoQueryType::Set,
179 namespace: "md",
180 to: Jid::new("", SERVER_JID),
181 target: None,
182 content: Some(NodeContent::Nodes(
183 iq_content
184 .children()
185 .map(|c| c.to_vec())
186 .unwrap_or_default(),
187 )),
188 id: Some(req_id),
189 timeout: Some(std::time::Duration::from_secs(30)),
190 };
191
192 let response = self
193 .send_iq(query)
194 .await
195 .map_err(|e: IqError| PairCodeError::RequestFailed(e.to_string()))?;
196
197 let pairing_ref = PairCodeUtils::parse_companion_hello_response(&response)
199 .ok_or(PairCodeError::MissingPairingRef)?;
200
201 info!(
202 target: "Client/PairCode",
203 "Stage 1 complete, waiting for phone confirmation. Code: {}",
204 code
205 );
206
207 *self.pair_code_state.lock().await = PairCodeState::WaitingForPhoneConfirmation {
209 pairing_ref,
210 phone_jid: phone_number,
211 pair_code: code.clone(),
212 ephemeral_keypair,
213 };
214
215 self.core.event_bus.dispatch(&Event::PairingCode {
217 code: code.clone(),
218 timeout: PairCodeUtils::code_validity(),
219 });
220
221 Ok(code)
222 }
223}
224
225pub(crate) async fn handle_pair_code_notification(client: &Arc<Client>, node: &Node) -> bool {
230 let Some(reg_node) = node.get_optional_child_by_tag(&["link_code_companion_reg"]) else {
232 return false;
233 };
234
235 let primary_wrapped_ephemeral = match reg_node
237 .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"])
238 .and_then(|n| n.content.as_ref())
239 {
240 Some(NodeContent::Bytes(b)) if b.len() == 80 => b.clone(),
241 _ => {
242 warn!(
243 target: "Client/PairCode",
244 "Missing or invalid primary wrapped ephemeral pub in notification"
245 );
246 return false;
247 }
248 };
249
250 let primary_identity_pub: [u8; 32] = match reg_node
252 .get_optional_child_by_tag(&["primary_identity_pub"])
253 .and_then(|n| n.content.as_ref())
254 {
255 Some(NodeContent::Bytes(b)) if b.len() == 32 => match b.as_slice().try_into() {
256 Ok(arr) => arr,
257 Err(_) => {
258 warn!(
259 target: "Client/PairCode",
260 "Failed to convert primary identity pub to array"
261 );
262 return false;
263 }
264 },
265 _ => {
266 warn!(
267 target: "Client/PairCode",
268 "Missing or invalid primary identity pub in notification"
269 );
270 return false;
271 }
272 };
273
274 let mut state_guard = client.pair_code_state.lock().await;
276 let state = std::mem::take(&mut *state_guard);
277 drop(state_guard);
278
279 let (pairing_ref, phone_jid, pair_code, ephemeral_keypair) = match state {
280 PairCodeState::WaitingForPhoneConfirmation {
281 pairing_ref,
282 phone_jid,
283 pair_code,
284 ephemeral_keypair,
285 } => (pairing_ref, phone_jid, pair_code, ephemeral_keypair),
286 _ => {
287 warn!(
288 target: "Client/PairCode",
289 "Received pair code notification but not in waiting state"
290 );
291 return false;
292 }
293 };
294
295 info!(
296 target: "Client/PairCode",
297 "Phone confirmed code entry, processing stage 2"
298 );
299
300 let pair_code_clone = pair_code.clone();
303 let primary_ephemeral_pub = match tokio::task::spawn_blocking(move || {
304 PairCodeUtils::decrypt_primary_ephemeral_pub(&primary_wrapped_ephemeral, &pair_code_clone)
305 })
306 .await
307 {
308 Ok(Ok(pub_key)) => pub_key,
309 Ok(Err(e)) => {
310 error!(
311 target: "Client/PairCode",
312 "Failed to decrypt primary ephemeral pub: {e}"
313 );
314 return false;
315 }
316 Err(e) => {
317 error!(
318 target: "Client/PairCode",
319 "spawn_blocking failed: {e}"
320 );
321 return false;
322 }
323 };
324
325 let device_snapshot = client.persistence_manager.get_device_snapshot().await;
327
328 let (wrapped_bundle, _new_adv_secret) = match PairCodeUtils::prepare_key_bundle(
333 &ephemeral_keypair,
334 &primary_ephemeral_pub,
335 &primary_identity_pub,
336 &device_snapshot.identity_key,
337 ) {
338 Ok(result) => result,
339 Err(e) => {
340 error!(target: "Client/PairCode", "Failed to prepare key bundle: {e}");
341 return false;
342 }
343 };
344
345 let req_id = client.generate_request_id();
347 let identity_pub: [u8; 32] = device_snapshot
348 .identity_key
349 .public_key
350 .public_key_bytes()
351 .try_into()
352 .expect("identity key is 32 bytes");
353
354 let iq = PairCodeUtils::build_companion_finish_iq(
355 &phone_jid,
356 wrapped_bundle,
357 &identity_pub,
358 &pairing_ref,
359 req_id,
360 );
361
362 if let Err(e) = client.send_node(iq).await {
363 error!(target: "Client/PairCode", "Failed to send companion_finish: {e}");
364 return false;
365 }
366
367 info!(
368 target: "Client/PairCode",
369 "Sent companion_finish, waiting for pair-success"
370 );
371
372 *client.pair_code_state.lock().await = PairCodeState::Completed;
374
375 true
376}