whatsapp_rust/
pair_code.rs1use crate::client::Client;
49use crate::request::{InfoQuery, InfoQueryType, IqError};
50use crate::types::events::Event;
51use log::{error, info, warn};
52
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::make_rng::<rand::rngs::StdRng>());
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 = wacore::runtime::blocking(&*self.runtime, move || {
159 PairCodeUtils::encrypt_ephemeral_pub(&ephemeral_pub, &code_clone)
160 })
161 .await;
162
163 let req_id = self.generate_request_id();
165 let iq_content = PairCodeUtils::build_companion_hello_iq(
166 &phone_number,
167 &noise_static_pub,
168 &wrapped_ephemeral,
169 options.platform_id,
170 &options.platform_display,
171 options.show_push_notification,
172 req_id.clone(),
173 );
174
175 let query = InfoQuery {
177 query_type: InfoQueryType::Set,
178 namespace: "md",
179 to: Jid::new("", SERVER_JID),
180 target: None,
181 content: Some(NodeContent::Nodes(
182 iq_content
183 .children()
184 .map(|c| c.to_vec())
185 .unwrap_or_default(),
186 )),
187 id: Some(req_id),
188 timeout: Some(std::time::Duration::from_secs(30)),
189 };
190
191 let response = self
192 .send_iq(query)
193 .await
194 .map_err(|e: IqError| PairCodeError::RequestFailed(e.to_string()))?;
195
196 let pairing_ref = PairCodeUtils::parse_companion_hello_response(&response)
198 .ok_or(PairCodeError::MissingPairingRef)?;
199
200 info!(
201 target: "Client/PairCode",
202 "Stage 1 complete, waiting for phone confirmation. Code: {}",
203 code
204 );
205
206 *self.pair_code_state.lock().await = PairCodeState::WaitingForPhoneConfirmation {
208 pairing_ref,
209 phone_jid: phone_number,
210 pair_code: code.clone(),
211 ephemeral_keypair: Box::new(ephemeral_keypair),
212 };
213
214 self.core.event_bus.dispatch(&Event::PairingCode {
216 code: code.clone(),
217 timeout: PairCodeUtils::code_validity(),
218 });
219
220 Ok(code)
221 }
222}
223
224pub(crate) async fn handle_pair_code_notification(client: &Arc<Client>, node: &Node) -> bool {
229 let Some(reg_node) = node.get_optional_child_by_tag(&["link_code_companion_reg"]) else {
231 return false;
232 };
233
234 let primary_wrapped_ephemeral = match reg_node
236 .get_optional_child_by_tag(&["link_code_pairing_wrapped_primary_ephemeral_pub"])
237 .and_then(|n| n.content.as_ref())
238 {
239 Some(NodeContent::Bytes(b)) if b.len() == 80 => b.clone(),
240 _ => {
241 warn!(
242 target: "Client/PairCode",
243 "Missing or invalid primary wrapped ephemeral pub in notification"
244 );
245 return false;
246 }
247 };
248
249 let primary_identity_pub: [u8; 32] = match reg_node
251 .get_optional_child_by_tag(&["primary_identity_pub"])
252 .and_then(|n| n.content.as_ref())
253 {
254 Some(NodeContent::Bytes(b)) if b.len() == 32 => match b.as_slice().try_into() {
255 Ok(arr) => arr,
256 Err(_) => {
257 warn!(
258 target: "Client/PairCode",
259 "Failed to convert primary identity pub to array"
260 );
261 return false;
262 }
263 },
264 _ => {
265 warn!(
266 target: "Client/PairCode",
267 "Missing or invalid primary identity pub in notification"
268 );
269 return false;
270 }
271 };
272
273 let mut state_guard = client.pair_code_state.lock().await;
275 let state = std::mem::take(&mut *state_guard);
276 drop(state_guard);
277
278 let (pairing_ref, phone_jid, pair_code, ephemeral_keypair) = match state {
279 PairCodeState::WaitingForPhoneConfirmation {
280 pairing_ref,
281 phone_jid,
282 pair_code,
283 ephemeral_keypair,
284 } => (pairing_ref, phone_jid, pair_code, ephemeral_keypair),
285 _ => {
286 warn!(
287 target: "Client/PairCode",
288 "Received pair code notification but not in waiting state"
289 );
290 return false;
291 }
292 };
293
294 info!(
295 target: "Client/PairCode",
296 "Phone confirmed code entry, processing stage 2"
297 );
298
299 let pair_code_clone = pair_code.clone();
302 let primary_ephemeral_pub = match wacore::runtime::blocking(&*client.runtime, move || {
303 PairCodeUtils::decrypt_primary_ephemeral_pub(&primary_wrapped_ephemeral, &pair_code_clone)
304 })
305 .await
306 {
307 Ok(pub_key) => pub_key,
308 Err(e) => {
309 error!(
310 target: "Client/PairCode",
311 "Failed to decrypt primary ephemeral pub: {e}"
312 );
313 return false;
314 }
315 };
316
317 let device_snapshot = client.persistence_manager.get_device_snapshot().await;
319
320 let (wrapped_bundle, new_adv_secret) = match PairCodeUtils::prepare_key_bundle(
322 &ephemeral_keypair,
323 &primary_ephemeral_pub,
324 &primary_identity_pub,
325 &device_snapshot.identity_key,
326 ) {
327 Ok(result) => result,
328 Err(e) => {
329 error!(target: "Client/PairCode", "Failed to prepare key bundle: {e}");
330 return false;
331 }
332 };
333
334 client
336 .persistence_manager
337 .process_command(crate::store::commands::DeviceCommand::SetAdvSecretKey(
338 new_adv_secret,
339 ))
340 .await;
341
342 let req_id = client.generate_request_id();
344 let identity_pub: [u8; 32] = device_snapshot
345 .identity_key
346 .public_key
347 .public_key_bytes()
348 .try_into()
349 .expect("identity key is 32 bytes");
350
351 let iq = PairCodeUtils::build_companion_finish_iq(
352 &phone_jid,
353 wrapped_bundle,
354 &identity_pub,
355 &pairing_ref,
356 req_id,
357 );
358
359 if let Err(e) = client.send_node(iq).await {
360 error!(target: "Client/PairCode", "Failed to send companion_finish: {e}");
361 return false;
362 }
363
364 info!(
365 target: "Client/PairCode",
366 "Sent companion_finish, waiting for pair-success"
367 );
368
369 *client.pair_code_state.lock().await = PairCodeState::Completed;
371
372 true
373}