1#[path = "group_mutation.rs"]
2mod group_mutation;
3pub use group_mutation::*;
4#[path = "group_detail.rs"]
5mod group_detail;
6pub use group_detail::*;
7#[path = "group_inventory.rs"]
8mod group_inventory;
9pub use group_inventory::*;
10#[path = "logout.rs"]
11mod logout;
12#[path = "revoke.rs"]
13mod revoke;
14#[path = "status_notify.rs"]
15mod status_notify;
16pub use crate::wire::{
17 AddMessage, AvatarBytes, ContactLookup, ContactRecord, Heartbeat, InitContactPage, RoomMember,
18 RoomRoster, SyncBatch, SyncCommand, TextSendResult, VoiceChunk,
19};
20use crate::{
21 business::{self, AuthenticatedPacket, Prelogin},
22 crypto::Ecdh,
23 mmtls::{self, Session},
24 protobuf as p, wire,
25};
26use anyhow::{bail, ensure, Result};
27pub use logout::*;
28use openssl::{nid::Nid, pkey::PKey, rsa::Rsa};
29pub use revoke::*;
30use serde::{Deserialize, Serialize};
31pub use status_notify::*;
32use std::{fmt, time::Duration};
33use zeroize::Zeroize;
34
35#[derive(Clone, Serialize, Deserialize)]
36#[serde(deny_unknown_fields)]
37pub struct DeviceProfile {
38 pub device_id: Vec<u8>,
39 pub device_type: String,
40 pub client_version: i32,
41}
42impl fmt::Debug for DeviceProfile {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.debug_struct("DeviceProfile")
45 .field("device_id", &"[redacted]")
46 .field("device_type", &self.device_type)
47 .field("client_version", &self.client_version)
48 .finish()
49 }
50}
51impl DeviceProfile {
52 pub fn generate(device_type: String) -> Result<Self> {
53 let bytes = crate::crypto::random::<16>()?;
54 let id = format!("l{}", hex::encode(bytes)).into_bytes()[..16].to_vec();
55 let p = Self {
56 device_id: id,
57 device_type,
58 client_version: -227271398,
59 };
60 p.validate()?;
61 Ok(p)
62 }
63 pub fn validate(&self) -> Result<()> {
64 ensure!(
65 self.device_id.len() == 16
66 && !self.device_type.is_empty()
67 && self.device_type.len() <= 1024
68 && !self.device_type.contains('\0'),
69 "Invalid native device profile"
70 );
71 ensure!(
72 self.client_version == -227271398,
73 "Unsupported native client version"
74 );
75 Ok(())
76 }
77}
78#[derive(Clone, Default, Serialize, Deserialize)]
82#[serde(deny_unknown_fields)]
83pub struct AuthSnapshot {
84 pub update_mask: u32,
85 pub response: Vec<u8>,
86 pub auth: Vec<u8>,
87 pub account: Vec<u8>,
88 pub network: Vec<u8>,
89 pub ax_auth: Vec<u8>,
90}
91impl fmt::Debug for AuthSnapshot {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.debug_struct("AuthSnapshot")
94 .field("sections", &"[redacted]")
95 .finish()
96 }
97}
98impl Drop for AuthSnapshot {
99 fn drop(&mut self) {
100 self.response.zeroize();
101 self.auth.zeroize();
102 self.account.zeroize();
103 self.network.zeroize();
104 self.ax_auth.zeroize();
105 }
106}
107#[derive(Clone, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct AccountState {
111 pub schema: u32,
112 pub device: DeviceProfile,
113 pub uin: u32,
114 pub own_username: String,
115 pub cookie: Vec<u8>,
116 pub session_key: Vec<u8>,
117 pub request_key: Vec<u8>,
118 pub response_key: Vec<u8>,
119 pub auto_auth_key: Vec<u8>,
120 pub key_info: Vec<u8>,
121 #[serde(default)]
124 pub database_password: Vec<u8>,
125 #[serde(default)]
126 pub auth_snapshot: AuthSnapshot,
127 pub long_host: String,
128 pub short_host: String,
129}
130impl fmt::Debug for AccountState {
131 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132 f.debug_struct("AccountState")
133 .field("schema", &self.schema)
134 .field("credentials", &"[redacted]")
135 .finish()
136 }
137}
138impl Drop for AccountState {
139 fn drop(&mut self) {
140 self.cookie.zeroize();
141 self.session_key.zeroize();
142 self.request_key.zeroize();
143 self.response_key.zeroize();
144 self.auto_auth_key.zeroize();
145 self.key_info.zeroize();
146 self.database_password.zeroize();
147 }
148}
149impl AccountState {
150 pub fn validate(&self) -> Result<()> {
151 ensure!(
152 self.schema == 1
153 && self.uin != 0
154 && !self.own_username.is_empty()
155 && self.own_username.len() <= 256
156 && !self.own_username.contains('\0'),
157 "Invalid native account identity"
158 );
159 self.device.validate()?;
160 mmtls::validate_host(&self.long_host)?;
161 mmtls::validate_host(&self.short_host)?;
162 ensure!(
163 self.cookie.len() <= 15
164 && self.session_key.len() == 16
165 && ((self.request_key.is_empty() && self.response_key.is_empty())
166 || (self.request_key.len() == 24 && self.response_key.len() == 24))
167 && self.auto_auth_key.len() <= 1024 * 1024
168 && self.key_info.len() <= 1024 * 1024
169 && (self.database_password.is_empty() || self.database_password.len() == 32)
170 && [
171 &self.auth_snapshot.response,
172 &self.auth_snapshot.auth,
173 &self.auth_snapshot.account,
174 &self.auth_snapshot.network,
175 &self.auth_snapshot.ax_auth
176 ]
177 .iter()
178 .all(|v| v.len() <= 1024 * 1024),
179 "Invalid native account keys"
180 );
181 Ok(())
182 }
183}
184impl AccountState {
185 pub fn from_baseline_json(input: &[u8]) -> Result<Self> {
189 use base64::{engine::general_purpose::STANDARD, Engine};
190 ensure!(
191 input.len() <= 8 * 1024 * 1024,
192 "Baseline checkpoint too large"
193 );
194 let value: serde_json::Value = serde_json::from_slice(input)
195 .map_err(|_| anyhow::anyhow!("Invalid baseline checkpoint JSON"))?;
196 let object = value
197 .as_object()
198 .ok_or_else(|| anyhow::anyhow!("Invalid baseline checkpoint object"))?;
199 let string = |name: &str| -> Result<String> {
200 object
201 .get(name)
202 .and_then(|v| v.as_str())
203 .map(str::to_string)
204 .ok_or_else(|| anyhow::anyhow!("Missing baseline field {name}"))
205 };
206 let decode = |name: &str, required: bool| -> Result<Vec<u8>> {
207 let encoded = match object.get(name) {
208 Some(v) => v
209 .as_str()
210 .ok_or_else(|| anyhow::anyhow!("Invalid baseline field {name}"))?,
211 None if !required => "",
212 _ => bail!("Missing baseline field {name}"),
213 };
214 let bytes = STANDARD
215 .decode(encoded)
216 .map_err(|_| anyhow::anyhow!("Invalid baseline encoding {name}"))?;
217 ensure!(
218 STANDARD.encode(&bytes) == encoded,
219 "Noncanonical baseline encoding {name}"
220 );
221 Ok(bytes)
222 };
223 ensure!(
224 object.get("schema").and_then(|v| v.as_u64()) == Some(1),
225 "Unsupported baseline schema"
226 );
227 let uin = match object.get("uin") {
228 Some(serde_json::Value::String(s)) => s
229 .parse::<u32>()
230 .map_err(|_| anyhow::anyhow!("Invalid baseline UIN"))?,
231 Some(n) => u32::try_from(
232 n.as_u64()
233 .ok_or_else(|| anyhow::anyhow!("Invalid baseline UIN"))?,
234 )?,
235 None => bail!("Missing baseline UIN"),
236 };
237 let account = decode("accountSect", false)?;
238 let section_username = if account.is_empty() {
239 None
240 } else {
241 let f = p::decode(&account)?;
242 p::optional_data(&f, 1)?
243 .map(|v| std::str::from_utf8(v).map(str::to_string))
244 .transpose()
245 .map_err(|_| anyhow::anyhow!("Invalid baseline account username"))?
246 };
247 let own_username = object
248 .get("ownUsername")
249 .or_else(|| object.get("username"))
250 .map(|v| {
251 v.as_str()
252 .map(str::to_string)
253 .ok_or_else(|| anyhow::anyhow!("Invalid baseline username"))
254 })
255 .transpose()?
256 .or(section_username.clone())
257 .ok_or_else(|| anyhow::anyhow!("Missing baseline account identity"))?;
258 ensure!(
259 section_username.is_none_or(|s| s == own_username),
260 "Baseline account identity mismatch"
261 );
262 let state =
263 Self {
264 schema: 1,
265 device: DeviceProfile {
266 device_id: decode("deviceId", true)?,
267 device_type: string("deviceType")?,
268 client_version: i32::try_from(
269 object
270 .get("clientVersion")
271 .and_then(|v| v.as_i64())
272 .ok_or_else(|| anyhow::anyhow!("Invalid baseline client version"))?,
273 )?,
274 },
275 uin,
276 own_username,
277 cookie: decode("cookie", false)?,
278 session_key: decode("sessionKey", true)?,
279 request_key: decode("field23", false)?,
280 response_key: decode("field24", false)?,
281 auto_auth_key: decode("autoAuthKey", false)?,
282 key_info: decode("keyInfo", false)?,
283 database_password: decode("databasePassword", false)?,
284 auth_snapshot: AuthSnapshot {
285 update_mask: match object.get("authSections") {
286 Some(serde_json::Value::String(value)) => value.parse()?,
287 Some(value) => u32::try_from(value.as_u64().ok_or_else(|| {
288 anyhow::anyhow!("Invalid baseline auth section mask")
289 })?)?,
290 None => 0,
291 },
292 response: decode("authResponse", false)?,
293 auth: decode("authSect", false)?,
294 account,
295 network: decode("networkSect", false)?,
296 ax_auth: decode("axAuthSect", false)?,
297 },
298 long_host: object
299 .get("longHost")
300 .and_then(|v| v.as_str())
301 .unwrap_or("long.weixin.qq.com")
302 .to_string(),
303 short_host: object
304 .get("shortHost")
305 .and_then(|v| v.as_str())
306 .unwrap_or("short.weixin.qq.com")
307 .to_string(),
308 };
309 state.validate()?;
310 Ok(state)
311 }
312}
313
314#[derive(Clone, Serialize, Deserialize)]
315#[serde(deny_unknown_fields)]
316pub struct LoginAttempt {
317 pub schema: u32,
318 pub device: DeviceProfile,
319 pub computer_name: String,
320 pub qr_png: Vec<u8>,
321 pub width: u32,
322 pub height: u32,
323 pub created_at: u32,
324 pub expires_at: u32,
325 pub identifier: Vec<u8>,
326 pub notify_key: Vec<u8>,
327 pub cookie: Vec<u8>,
328 pub key_version: u16,
329 pub long_host: String,
330 pub short_host: String,
331 pub status: LoginStatus,
332 #[serde(default)]
333 key_info: Vec<u8>,
334 credential3: Option<Vec<u8>>,
335 credential4: Option<Vec<u8>>,
336 #[serde(default)]
337 lan_upload_attempted: bool,
338 #[serde(skip)]
340 lan_challenge: Option<std::sync::Arc<crate::lan_login::Challenge>>,
341}
342impl fmt::Debug for LoginAttempt {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 f.debug_struct("LoginAttempt")
345 .field("status", &self.status)
346 .field("created_at", &self.created_at)
347 .field("expires_at", &self.expires_at)
348 .field("credentials", &"[redacted]")
349 .finish()
350 }
351}
352impl Drop for LoginAttempt {
353 fn drop(&mut self) {
354 self.stop_lan_login();
355 self.identifier.zeroize();
356 self.notify_key.zeroize();
357 self.cookie.zeroize();
358 self.key_info.zeroize();
359 self.credential3.zeroize();
360 self.credential4.zeroize();
361 }
362}
363impl LoginAttempt {
364 pub fn lan_login_state(&self) -> Option<&'static str> {
365 self.lan_challenge
366 .as_ref()
367 .map(|challenge| challenge.status())
368 }
369 fn stop_lan_login(&mut self) {
370 if let Some(challenge) = self.lan_challenge.take() {
371 challenge.stop();
372 }
373 }
374 pub fn qr_svg(&self) -> Result<String> {
375 use base64::Engine;
376 ensure!(
377 !self.qr_png.is_empty(),
378 "This saved-device attempt has no QR"
379 );
380 let encoded = base64::engine::general_purpose::STANDARD.encode(&self.qr_png);
381 Ok(format!("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"530\" height=\"530\" viewBox=\"0 0 265 265\"><rect width=\"265\" height=\"265\" fill=\"white\"/><image x=\"40\" y=\"40\" width=\"185\" height=\"185\" style=\"image-rendering:pixelated\" href=\"data:image/png;base64,{encoded}\"/></svg>"))
382 }
383 fn validate(&self) -> Result<()> {
384 self.validate_at(wire::unix_time()?)
385 }
386 fn locally_expired(&self, now: u32) -> bool {
387 let hard_deadline = self.created_at.saturating_add(900);
390 let deadline = if self.qr_png.is_empty()
391 || matches!(self.status, LoginStatus::Scanned | LoginStatus::Approved)
392 {
393 hard_deadline
394 } else {
395 self.expires_at.min(hard_deadline)
396 };
397 now >= deadline
398 }
399 fn validate_at(&self, now: u32) -> Result<()> {
400 ensure!(
401 self.schema == 1 && !self.locally_expired(now),
402 "Login attempt expired"
403 );
404 self.device.validate()?;
405 ensure!(
406 !self.identifier.is_empty()
407 && self.identifier.len() <= 4096
408 && !self.notify_key.is_empty()
409 && self.notify_key.len() <= 1024
410 && self.cookie.len() <= 15
411 && self.key_info.len() <= 1024 * 1024,
412 "Invalid login attempt"
413 );
414 mmtls::validate_host(&self.long_host)?;
415 mmtls::validate_host(&self.short_host)?;
416 ensure!(
417 [10014, 50005].contains(&self.key_version),
418 "Unsupported login key version"
419 );
420 Ok(())
421 }
422 fn manual_auth_request(&self, point: &[u8]) -> Result<Vec<u8>> {
423 wire::manual_auth(
424 &self.device,
425 &self.computer_name,
426 self.credential3
427 .as_deref()
428 .ok_or_else(|| anyhow::anyhow!("Missing approval"))?,
429 self.credential4
430 .as_deref()
431 .ok_or_else(|| anyhow::anyhow!("Missing approval"))?,
432 point,
433 &self.key_info,
434 )
435 }
436}
437#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
438#[serde(rename_all = "snake_case")]
439pub enum LoginStatus {
440 Pending,
441 Scanned,
442 Approved,
443 Expired,
444 Cancelled,
445 Rejected,
446 Completed,
447}
448#[derive(Clone, Debug)]
449pub struct NativeOptions {
450 pub long_host: String,
451 pub short_host: String,
452 pub timeout: Duration,
453 pub receive_budget: usize,
454}
455impl Default for NativeOptions {
456 fn default() -> Self {
457 Self {
458 long_host: "long.weixin.qq.com".to_string(),
459 short_host: "short.weixin.qq.com".to_string(),
460 timeout: Duration::from_secs(30),
461 receive_budget: crate::MAX_MESSAGE_BYTES,
462 }
463 }
464}
465pub struct NativeClient {
472 options: NativeOptions,
473}
474pub fn is_retryable_login_poll_error(error: &anyhow::Error) -> bool {
477 if crate::login_lan_error_reason(error).is_some() {
478 return false;
479 }
480 error.chain().any(|cause| {
481 if let Some(io) = cause.downcast_ref::<std::io::Error>() {
482 return matches!(
483 io.kind(),
484 std::io::ErrorKind::TimedOut
485 | std::io::ErrorKind::WouldBlock
486 | std::io::ErrorKind::ConnectionReset
487 | std::io::ErrorKind::ConnectionAborted
488 | std::io::ErrorKind::ConnectionRefused
489 | std::io::ErrorKind::UnexpectedEof
490 | std::io::ErrorKind::BrokenPipe
491 | std::io::ErrorKind::NotConnected
492 );
493 }
494 if let Some(http) = cause.downcast_ref::<reqwest::Error>() {
495 return http.is_timeout() || http.is_connect();
496 }
497 let message = cause.to_string();
498 matches!(
499 message.as_str(),
500 "MMTLS deadline exceeded" | "MMTLS connection closed"
501 ) || message
502 .strip_prefix("Native shortlink HTTP status ")
503 .and_then(|s| s.parse::<u16>().ok())
504 .is_some_and(|status| matches!(status, 408 | 429 | 500 | 502 | 503 | 504))
505 })
506}
507impl NativeClient {
508 pub fn new(options: NativeOptions) -> Result<Self> {
510 mmtls::validate_host(&options.long_host)?;
511 mmtls::validate_host(&options.short_host)?;
512 ensure!(
513 !options.timeout.is_zero() && options.timeout <= Duration::from_secs(120),
514 "Invalid native timeout"
515 );
516 ensure!(
517 (1024..=crate::MAX_MESSAGE_BYTES).contains(&options.receive_budget),
518 "Invalid receive budget"
519 );
520 Ok(Self { options })
521 }
522 fn session(&self, host: &str) -> Result<Session> {
523 Session::connect(host, self.options.timeout, self.options.receive_budget)
524 }
525 pub fn push_connection(&self, state: &AccountState) -> Result<crate::push::Connection> {
526 crate::push::Connection::open(state, self.options.timeout, self.options.receive_budget)
527 }
528 #[allow(clippy::too_many_arguments)]
529 fn prelogin_rpc(
530 &self,
531 long_host: &str,
532 short_host: &str,
533 cgi: u16,
534 path: &str,
535 plain: &[u8],
536 cookie: &[u8],
537 auth_key: &[u8],
538 fixed_version: Option<u16>,
539 ) -> Result<(Vec<u8>, Vec<u8>, u16)> {
540 let session = self.session(long_host)?;
541 let profile = if let Some(version) = fixed_version {
542 business::profile_for_region(if version == 50005 { 2 } else { 1 })?
543 } else {
544 business::profile_for_region(session.certificate_region)?
545 };
546 let prepared = Prelogin::prepare(plain, &profile, auth_key)?;
547 let packet = business::prelogin_packet(&prepared, cgi, profile.version, cookie)?;
548 let reply = mmtls::short_exchange(
549 &session.short,
550 short_host,
551 path,
552 &packet,
553 self.options.timeout,
554 )?;
555 let envelope = business::split(&reply)?;
556 ensure!(
557 envelope.return_code == 0,
558 "Native envelope returned {}",
559 envelope.return_code
560 );
561 ensure!(
562 envelope.cgi == cgi as u64 && envelope.encryption == 12,
563 "Unexpected prelogin response policy"
564 );
565 let plain = prepared.decrypt_reply(&envelope.payload)?;
566 Ok((plain, envelope.cookie, profile.version))
567 }
568 pub fn start_qr_login(
572 &self,
573 device: DeviceProfile,
574 computer_name: &str,
575 ) -> Result<LoginAttempt> {
576 self.start_profile_qr_login(device, computer_name, &[])
577 }
578 pub fn start_profile_qr_login(
581 &self,
582 device: DeviceProfile,
583 computer_name: &str,
584 key_info: &[u8],
585 ) -> Result<LoginAttempt> {
586 device.validate()?;
587 ensure!(key_info.len() <= 1024 * 1024, "Invalid saved key-info");
588 let plain = wire::qr_request(&device, computer_name)?;
589 let (reply, cookie, key_version) = self.prelogin_rpc(
590 &self.options.long_host,
591 &self.options.short_host,
592 502,
593 "/cgi-bin/micromsg-bin/getloginqrcode",
594 &plain,
595 &[],
596 &[],
597 None,
598 )?;
599 let qr = wire::decode_qr(&reply)?;
600 let now = wire::unix_time()?;
601 Ok(LoginAttempt {
602 schema: 1,
603 device,
604 computer_name: computer_name.to_string(),
605 qr_png: qr.image,
606 width: qr.width,
607 height: qr.height,
608 created_at: now,
609 expires_at: now.saturating_add(240),
610 identifier: qr.identifier,
611 notify_key: qr.notify_key,
612 cookie,
613 key_version,
614 long_host: self.options.long_host.clone(),
615 short_host: self.options.short_host.clone(),
616 status: LoginStatus::Pending,
617 key_info: key_info.to_vec(),
618 credential3: None,
619 credential4: None,
620 lan_upload_attempted: false,
621 lan_challenge: None,
622 })
623 }
624 pub fn start_saved_device_login(
627 &self,
628 state: &AccountState,
629 computer_name: &str,
630 ) -> Result<LoginAttempt> {
631 state.validate()?;
632 self.start_saved_login(
633 &state.device,
634 &state.own_username,
635 &state.auto_auth_key,
636 &state.cookie,
637 &state.key_info,
638 &state.long_host,
639 &state.short_host,
640 computer_name,
641 )
642 }
643 pub fn start_profile_saved_device_login(
645 &self,
646 device: &DeviceProfile,
647 username: &str,
648 auto_auth_key: &[u8],
649 cookie: &[u8],
650 key_info: &[u8],
651 computer_name: &str,
652 ) -> Result<LoginAttempt> {
653 self.start_saved_login(
654 device,
655 username,
656 auto_auth_key,
657 cookie,
658 key_info,
659 &self.options.long_host,
660 &self.options.short_host,
661 computer_name,
662 )
663 }
664 #[allow(clippy::too_many_arguments)]
665 fn start_saved_login(
666 &self,
667 device: &DeviceProfile,
668 username: &str,
669 auto_auth_key: &[u8],
670 cookie: &[u8],
671 key_info: &[u8],
672 long_host: &str,
673 short_host: &str,
674 computer_name: &str,
675 ) -> Result<LoginAttempt> {
676 device.validate()?;
677 ensure!(
678 !username.is_empty()
679 && username.len() <= 256
680 && !username.contains('\0')
681 && !auto_auth_key.is_empty()
682 && auto_auth_key.len() <= 1024 * 1024
683 && cookie.len() <= 15
684 && key_info.len() <= 1024 * 1024,
685 "Invalid saved-device credentials"
686 );
687 let rsa = Rsa::generate(1024)?;
688 let public = PKey::from_rsa(rsa)?.public_key_to_pem()?;
689 let request = wire::push_login(device, username, auto_auth_key, &public)?;
690 let (reply, cookie, key_version) = self.prelogin_rpc(
691 long_host,
692 short_host,
693 654,
694 "/cgi-bin/micromsg-bin/pushloginurl",
695 &request,
696 cookie,
697 &[],
698 None,
699 )?;
700 let (identifier, notify_key) = wire::decode_push(&reply)?;
701 let now = wire::unix_time()?;
702 Ok(LoginAttempt {
703 schema: 1,
704 device: device.clone(),
705 computer_name: computer_name.to_string(),
706 qr_png: vec![],
707 width: 0,
708 height: 0,
709 created_at: now,
710 expires_at: now.saturating_add(240),
711 identifier,
712 notify_key,
713 cookie,
714 key_version,
715 long_host: long_host.into(),
716 short_host: short_host.into(),
717 status: LoginStatus::Pending,
718 key_info: key_info.to_vec(),
719 credential3: None,
720 credential4: None,
721 lan_upload_attempted: false,
722 lan_challenge: None,
723 })
724 }
725 pub fn poll_login(&self, attempt: &mut LoginAttempt) -> Result<LoginStatus> {
728 self.poll_login_guarded(
729 attempt,
730 std::time::Instant::now() + Duration::from_secs(900),
731 || false,
732 )
733 }
734 pub fn poll_login_guarded(
737 &self,
738 attempt: &mut LoginAttempt,
739 deadline: std::time::Instant,
740 cancelled: impl Fn() -> bool + Send + Sync + 'static,
741 ) -> Result<LoginStatus> {
742 let now = wire::unix_time()?;
743 if attempt.locally_expired(now) {
744 attempt.stop_lan_login();
745 attempt.status = LoginStatus::Expired;
746 return Ok(attempt.status);
747 }
748 attempt.validate_at(now)?;
749 ensure!(
750 !matches!(
751 attempt.status,
752 LoginStatus::Completed | LoginStatus::Cancelled | LoginStatus::Rejected
753 ),
754 "Login attempt is terminal"
755 );
756 let request = wire::poll_request(&attempt.device, &attempt.identifier)?;
757 let (reply, cookie, _) = self.prelogin_rpc(
758 &attempt.long_host,
759 &attempt.short_host,
760 503,
761 "/cgi-bin/micromsg-bin/checkloginqrcode",
762 &request,
763 &attempt.cookie,
764 &[],
765 Some(attempt.key_version),
766 )?;
767 let response = wire::decode_poll(&reply, &attempt.notify_key)?;
768 if !cookie.is_empty() {
769 attempt.cookie = cookie;
770 }
771 let remaining = attempt
772 .created_at
773 .saturating_add(900)
774 .saturating_sub(wire::unix_time()?);
775 let deadline =
776 deadline.min(std::time::Instant::now() + Duration::from_secs(u64::from(remaining)));
777 let scope = crate::lan_login::Scope::new(deadline, cancelled);
778 apply_login_notification(attempt, response, |attempt| {
779 self.start_lan_login(attempt, scope)
780 })
781 }
782 fn start_lan_login(
783 &self,
784 attempt: &mut LoginAttempt,
785 scope: crate::lan_login::Scope,
786 ) -> Result<std::sync::Arc<crate::lan_login::Challenge>> {
787 use crate::lan_login as lan;
788 let challenge = std::sync::Arc::new(lan::Challenge::bind(scope)?);
789 let random = zeroize::Zeroizing::new(crate::crypto::random::<16>()?);
790 let request = zeroize::Zeroizing::new(challenge.request(
791 &attempt.device,
792 &attempt.identifier,
793 &random,
794 )?);
795 let (reply, cookie, _) = self
796 .prelogin_rpc(
797 &attempt.long_host,
798 &attempt.short_host,
799 lan::CGI,
800 lan::PATH,
801 &request,
802 &attempt.cookie,
803 &[],
804 Some(attempt.key_version),
805 )
806 .map_err(|_| lan::failure("upload_failed"))?;
807 let reply = zeroize::Zeroizing::new(reply);
808 let token =
809 lan::response(&reply).map_err(|_| lan::failure("upload_rejected_or_invalid"))?;
810 challenge.arm(&token)?;
811 if !cookie.is_empty() {
812 attempt.cookie = cookie;
813 }
814 Ok(challenge)
815 }
816 pub fn complete_login(&self, attempt: &mut LoginAttempt) -> Result<AccountState> {
819 attempt.stop_lan_login();
820 attempt.validate()?;
821 ensure!(
822 attempt.status == LoginStatus::Approved,
823 "Login has not been approved"
824 );
825 let inner = Ecdh::generate(Nid::SECP224R1)?;
826 let request = attempt.manual_auth_request(&inner.public()?)?;
827 let mut long_host = attempt.long_host.clone();
828 let mut short_host = attempt.short_host.clone();
829 let mut cookie = attempt.cookie.clone();
830 for redirected in 0..=1 {
831 let (reply, next_cookie, _) = self.prelogin_rpc(
832 &long_host,
833 &short_host,
834 252,
835 "/cgi-bin/micromsg-bin/secmanualauth",
836 &request,
837 &cookie,
838 &[],
839 None,
840 )?;
841 let fields = p::decode(&reply)?;
842 let code = p::base_code(&fields)?;
843 if code == -301 && redirected == 0 {
844 (long_host, short_host) = redirect(&fields)?;
845 cookie = next_cookie;
846 continue;
847 }
848 ensure!(code == 0, "Account authentication returned {code}");
849 let mut state = reduce_auth(&reply, None, &inner, &attempt.device, &next_cookie)?;
850 state.long_host = long_host;
851 state.short_host = short_host;
852 state.validate()?;
853 attempt.status = LoginStatus::Completed;
854 attempt.credential3.zeroize();
855 attempt.credential4.zeroize();
856 return Ok(state);
857 }
858 bail!("Native authentication redirect limit reached")
859 }
860 pub fn refresh_qr_login(&self, attempt: &LoginAttempt) -> Result<LoginAttempt> {
862 ensure!(
863 attempt.status == LoginStatus::Expired && !attempt.qr_png.is_empty(),
864 "Only an expired QR may be refreshed"
865 );
866 let options = NativeOptions {
867 long_host: attempt.long_host.clone(),
868 short_host: attempt.short_host.clone(),
869 ..self.options.clone()
870 };
871 Self::new(options)?.start_profile_qr_login(
872 attempt.device.clone(),
873 &attempt.computer_name,
874 &attempt.key_info,
875 )
876 }
877 pub fn renew_session(
880 &self,
881 previous: &AccountState,
882 computer_name: &str,
883 ) -> Result<AccountState> {
884 previous.validate()?;
885 let inner = Ecdh::generate(Nid::SECP224R1)?;
886 let request = wire::auto_auth(previous, &inner.public()?, computer_name)?;
887 let mut long = previous.long_host.clone();
888 let mut short = previous.short_host.clone();
889 let mut cookie = previous.cookie.clone();
890 for redirected in 0..=1 {
891 let (reply, next_cookie, _) = self.prelogin_rpc(
892 &long,
893 &short,
894 763,
895 "/cgi-bin/micromsg-bin/secautoauth",
896 &request,
897 &cookie,
898 &previous.auto_auth_key,
899 None,
900 )?;
901 let fields = p::decode(&reply)?;
902 let code = p::base_code(&fields)?;
903 if code == -301 && redirected == 0 {
904 (long, short) = redirect(&fields)?;
905 cookie = next_cookie;
906 continue;
907 }
908 ensure!(code == 0, "Automatic authentication returned {code}");
909 let mut next = reduce_auth(
910 &reply,
911 Some(previous),
912 &inner,
913 &previous.device,
914 &next_cookie,
915 )?;
916 next.long_host = long;
917 next.short_host = short;
918 next.validate()?;
919 return Ok(next);
920 }
921 bail!("AutoAuth redirect limit reached")
922 }
923 pub fn experimental_call_rpc(
930 &self,
931 state: &mut AccountState,
932 route: crate::calls::host_policy::CallHostRoute,
933 request: &[u8],
934 ) -> Result<Vec<u8>> {
935 self.experimental_call_rpc_guarded(state, route, request, || Ok(()))
936 }
937
938 pub fn experimental_call_rpc_guarded(
943 &self,
944 state: &mut AccountState,
945 route: crate::calls::host_policy::CallHostRoute,
946 request: &[u8],
947 before_submit: impl FnOnce() -> Result<()>,
948 ) -> Result<Vec<u8>> {
949 self.call_rpc_with_exchange(state, route, request, before_submit, mmtls::short_exchange)
950 }
951
952 #[doc(hidden)]
955 pub fn experimental_call_live_sync_guarded(
956 &self,
957 state: &mut AccountState,
958 request: &[u8],
959 before_submit: impl FnOnce() -> Result<()>,
960 wait: impl for<'a> FnOnce(
961 std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>>> + Send + 'a>>,
962 ) -> Result<Vec<u8>>,
963 ) -> Result<Vec<u8>> {
964 self.call_rpc_with_exchange(
965 state,
966 crate::calls::host_policy::CallHostRoute::LiveSync,
967 request,
968 before_submit,
969 |session, host, path, payload, timeout| {
970 wait(Box::pin(mmtls::short_exchange_interruptible(
971 session, host, path, payload, timeout,
972 )))
973 },
974 )
975 }
976
977 fn call_rpc_with_exchange(
978 &self,
979 state: &mut AccountState,
980 route: crate::calls::host_policy::CallHostRoute,
981 request: &[u8],
982 before_submit: impl FnOnce() -> Result<()>,
983 exchange: impl FnOnce(&mmtls::ShortSession, &str, &str, &[u8], Duration) -> Result<Vec<u8>>,
984 ) -> Result<Vec<u8>> {
985 use crate::calls::{
986 host_policy::{selected_policy, CallHostPolicy},
987 host_transport::{CallHostRpcError, CallHostStage},
988 };
989 mmtls::validate_short_route(route.path())
990 .map_err(|error| CallHostRpcError::from_cause(CallHostStage::Route, &error))?;
991 let policy = selected_policy(&state.auth_snapshot, route.command())
992 .map_err(|_| CallHostRpcError::new(CallHostStage::Route, "policy", None))?;
993 if policy != CallHostPolicy::Ordinary {
994 return Err(CallHostRpcError::new(CallHostStage::Route, "ax_unsupported", None).into());
995 }
996 let session = self
997 .session(&state.long_host)
998 .map_err(|error| CallHostRpcError::from_cause(CallHostStage::Handshake, &error))?;
999 let packet = AuthenticatedPacket::prepare(state, route.command(), request)
1000 .map_err(|error| CallHostRpcError::from_cause(CallHostStage::Prepare, &error))?;
1001 before_submit()?;
1002 let reply = exchange(
1003 &session.short,
1004 &state.short_host,
1005 route.path(),
1006 &packet.packet,
1007 self.options.timeout,
1008 )
1009 .map_err(|error| {
1010 if error.is::<crate::calls::client::CallLiveSyncInterrupted>() {
1011 error
1012 } else {
1013 CallHostRpcError::from_cause(CallHostStage::Exchange, &error).into()
1014 }
1015 })?;
1016 let (plain, cookie) =
1017 crate::calls::host_transport::decode_verified_reply(state, route.command(), &reply)?;
1018 if !cookie.is_empty() {
1019 state.cookie = cookie;
1020 }
1021 Ok(plain)
1022 }
1023
1024 pub(crate) fn authenticated_rpc(
1025 &self,
1026 state: &mut AccountState,
1027 cgi: u16,
1028 path: &str,
1029 request: &[u8],
1030 ) -> Result<Vec<u8>> {
1031 self.authenticated_rpc_guarded(state, cgi, path, request, || Ok(()))
1032 }
1033 fn authenticated_rpc_guarded(
1034 &self,
1035 state: &mut AccountState,
1036 cgi: u16,
1037 path: &str,
1038 request: &[u8],
1039 before_submit: impl FnOnce() -> Result<()>,
1040 ) -> Result<Vec<u8>> {
1041 let session = self.session(&state.long_host)?;
1042 let packet = AuthenticatedPacket::prepare(state, cgi, request)?;
1043 before_submit()?;
1044 let reply = mmtls::short_exchange(
1045 &session.short,
1046 &state.short_host,
1047 path,
1048 &packet.packet,
1049 self.options.timeout,
1050 )?;
1051 let (plain, cookie) = packet.decode_verified_transport_reply(&reply)?;
1052 if !cookie.is_empty() {
1053 state.cookie = cookie;
1054 }
1055 Ok(plain)
1056 }
1057 pub fn sync(&self, state: &mut AccountState, cursor: &[u8]) -> Result<SyncBatch> {
1062 self.sync_scene(state, cursor, 6)
1063 }
1064 pub fn sync_scene(
1065 &self,
1066 state: &mut AccountState,
1067 cursor: &[u8],
1068 scene: u32,
1069 ) -> Result<SyncBatch> {
1070 ensure!([6, 7].contains(&scene), "Unsupported sync scene");
1071 let wire_cursor = if cursor.is_empty() { &[0][..] } else { cursor };
1072 let request = wire::sync_request(wire_cursor, &state.device.device_type, scene)?;
1073 wire::decode_sync(
1074 &self.authenticated_rpc(state, 138, "/cgi-bin/micromsg-bin/newsync", &request)?,
1075 cursor,
1076 )
1077 }
1078 pub fn heartbeat(&self, state: &mut AccountState, cursor: &[u8]) -> Result<Heartbeat> {
1081 let request = wire::heartbeat_request(state, cursor)?;
1082 wire::decode_heartbeat(&self.authenticated_rpc(
1083 state,
1084 518,
1085 "/cgi-bin/micromsg-bin/heartbeat",
1086 &request,
1087 )?)
1088 }
1089 pub fn send_text(
1098 &self,
1099 state: &mut AccountState,
1100 destination: &str,
1101 content: &str,
1102 created_at: u32,
1103 client_message_id: u32,
1104 ) -> Result<TextSendResult> {
1105 self.send_text_guarded(
1106 state,
1107 destination,
1108 content,
1109 created_at,
1110 client_message_id,
1111 || Ok(()),
1112 )
1113 }
1114 #[doc(hidden)]
1116 pub fn send_text_guarded(
1117 &self,
1118 state: &mut AccountState,
1119 destination: &str,
1120 content: &str,
1121 created_at: u32,
1122 client_message_id: u32,
1123 before_submit: impl FnOnce() -> Result<()>,
1124 ) -> Result<TextSendResult> {
1125 let request = wire::text_send_request_with_source(
1126 destination,
1127 content,
1128 created_at,
1129 client_message_id,
1130 None,
1131 )?;
1132 wire::decode_text_send(
1133 &self.authenticated_rpc_guarded(
1134 state,
1135 522,
1136 "/cgi-bin/micromsg-bin/newsendmsg",
1137 &request,
1138 before_submit,
1139 )?,
1140 destination,
1141 client_message_id,
1142 )
1143 }
1144 pub fn send_text_with_source(
1147 &self,
1148 state: &mut AccountState,
1149 destination: &str,
1150 content: &str,
1151 created_at: u32,
1152 client_message_id: u32,
1153 source: Option<&str>,
1154 ) -> Result<TextSendResult> {
1155 let request = wire::text_send_request_with_source(
1156 destination,
1157 content,
1158 created_at,
1159 client_message_id,
1160 source,
1161 )?;
1162 wire::decode_text_send(
1163 &self.authenticated_rpc(state, 522, "/cgi-bin/micromsg-bin/newsendmsg", &request)?,
1164 destination,
1165 client_message_id,
1166 )
1167 }
1168 pub fn lookup_contacts(
1171 &self,
1172 state: &mut AccountState,
1173 usernames: &[String],
1174 ) -> Result<ContactLookup> {
1175 let request = wire::contact_request(state, usernames)?;
1176 wire::decode_contacts(
1177 &self.authenticated_rpc(state, 182, "/cgi-bin/micromsg-bin/getcontact", &request)?,
1178 usernames,
1179 )
1180 }
1181 pub fn lookup_room(
1182 &self,
1183 state: &mut AccountState,
1184 room_id: &str,
1185 version: u32,
1186 ) -> Result<RoomRoster> {
1187 let request = wire::room_request(state, room_id, version)?;
1188 wire::decode_room(
1189 &self.authenticated_rpc(
1190 state,
1191 551,
1192 "/cgi-bin/micromsg-bin/getchatroommemberdetail",
1193 &request,
1194 )?,
1195 room_id,
1196 )
1197 }
1198 pub fn init_contacts(
1199 &self,
1200 state: &mut AccountState,
1201 contact_sequence: u32,
1202 room_sequence: u32,
1203 ) -> Result<InitContactPage> {
1204 let request =
1205 wire::init_contacts_request(&state.own_username, contact_sequence, room_sequence)?;
1206 let page = wire::decode_init_contacts(&self.authenticated_rpc(
1207 state,
1208 851,
1209 "/cgi-bin/micromsg-bin/initcontact",
1210 &request,
1211 )?)?;
1212 ensure!(
1213 !page.more
1214 || page.contact_sequence != contact_sequence
1215 || page.room_sequence != room_sequence,
1216 "Contact bootstrap made no cursor progress"
1217 );
1218 Ok(page)
1219 }
1220 pub fn download_avatars(
1221 &self,
1222 state: &mut AccountState,
1223 usernames: &[String],
1224 ) -> Result<Vec<AvatarBytes>> {
1225 let request = wire::avatar_request(state, usernames)?;
1226 wire::decode_avatars(
1227 &self.authenticated_rpc(
1228 state,
1229 123,
1230 "/cgi-bin/micromsg-bin/batchgetheadimg",
1231 &request,
1232 )?,
1233 usernames,
1234 )
1235 }
1236 pub fn download_voice_chunk(
1237 &self,
1238 state: &mut AccountState,
1239 server_message_id: u64,
1240 total_length: u32,
1241 offset: u32,
1242 ) -> Result<VoiceChunk> {
1243 let request = wire::voice_request(state, server_message_id, total_length, offset)?;
1244 wire::decode_voice(
1245 &self.authenticated_rpc(state, 128, "/cgi-bin/micromsg-bin/downloadvoice", &request)?,
1246 server_message_id,
1247 total_length,
1248 offset,
1249 )
1250 }
1251 pub fn cdn_config(&self, state: &mut AccountState) -> Result<crate::media::CdnConfig> {
1252 let request = p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?);
1253 let reply =
1254 self.authenticated_rpc(state, 379, "/cgi-bin/micromsg-bin/getcdndns", &request)?;
1255 crate::media::decode_cdn_config(&reply, state)
1256 }
1257 pub fn large_cdn_config(
1258 &self,
1259 state: &mut AccountState,
1260 route: crate::media::LargeCdnRoute,
1261 ) -> Result<crate::media::CdnConfig> {
1262 let request = p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?);
1263 let reply =
1264 self.authenticated_rpc(state, 379, "/cgi-bin/micromsg-bin/getcdndns", &request)?;
1265 crate::media::decode_large_cdn_config(&reply, state, route)
1266 }
1267 pub fn send_share(
1270 &self,
1271 state: &mut AccountState,
1272 destination: &str,
1273 client_id: &str,
1274 created_at: u32,
1275 content: &str,
1276 ) -> Result<crate::media_send::MediaSendResult> {
1277 let request = crate::media_send::share_send_request(
1278 state,
1279 destination,
1280 client_id,
1281 created_at,
1282 content,
1283 )?;
1284 crate::media_send::decode_file_send(
1285 &self.authenticated_rpc(state, 222, "/cgi-bin/micromsg-bin/sendappmsg", &request)?,
1286 client_id,
1287 )
1288 }
1289 pub fn send_quote(
1292 &self,
1293 state: &mut AccountState,
1294 destination: &str,
1295 client_id: &str,
1296 created_at: u32,
1297 content: &str,
1298 ) -> Result<crate::media_send::MediaSendResult> {
1299 self.send_quote_with_source(state, destination, client_id, created_at, content, None)
1300 }
1301 pub fn send_quote_with_source(
1302 &self,
1303 state: &mut AccountState,
1304 destination: &str,
1305 client_id: &str,
1306 created_at: u32,
1307 content: &str,
1308 source: Option<&str>,
1309 ) -> Result<crate::media_send::MediaSendResult> {
1310 let request = crate::quote_send::quote_send_request_with_source(
1311 state,
1312 destination,
1313 client_id,
1314 created_at,
1315 content,
1316 source,
1317 )?;
1318 crate::media_send::decode_file_send(
1319 &self.authenticated_rpc(state, 222, "/cgi-bin/micromsg-bin/sendappmsg", &request)?,
1320 client_id,
1321 )
1322 }
1323 pub fn reserve_file_upload(
1326 &self,
1327 state: &mut AccountState,
1328 destination: &str,
1329 progress_client_id: &str,
1330 file_name: &str,
1331 plaintext_bytes: usize,
1332 md5: &str,
1333 ) -> Result<crate::file_upload::FileUploadReservation> {
1334 let request = crate::file_upload::file_upload_request(
1335 state,
1336 destination,
1337 progress_client_id,
1338 file_name,
1339 plaintext_bytes,
1340 md5,
1341 )?;
1342 crate::file_upload::decode_file_upload_reservation(&self.authenticated_rpc(
1343 state,
1344 6691,
1345 "/cgi-bin/micromsg-bin/sendfileuploadmsg",
1346 &request,
1347 )?)
1348 }
1349 pub fn send_file(
1350 &self,
1351 state: &mut AccountState,
1352 destination: &str,
1353 client_id: &str,
1354 created_at: u32,
1355 upload: &crate::media::UploadedMedia,
1356 ) -> Result<crate::media_send::MediaSendResult> {
1357 let request = crate::media_send::file_send_request(
1358 state,
1359 destination,
1360 client_id,
1361 created_at,
1362 upload,
1363 )?;
1364 crate::media_send::decode_file_send(
1365 &self.authenticated_rpc(state, 222, "/cgi-bin/micromsg-bin/sendappmsg", &request)?,
1366 client_id,
1367 )
1368 }
1369 #[allow(clippy::too_many_arguments)]
1370 pub fn send_file_with_reservation(
1371 &self,
1372 state: &mut AccountState,
1373 destination: &str,
1374 client_id: &str,
1375 created_at: u32,
1376 upload: &crate::media::UploadedMedia,
1377 reservation: Option<&crate::file_upload::FileUploadReservation>,
1378 ) -> Result<crate::media_send::MediaSendResult> {
1379 let request = crate::media_send::file_send_request_with_reservation(
1380 state,
1381 destination,
1382 client_id,
1383 created_at,
1384 upload,
1385 reservation,
1386 )?;
1387 let reply =
1388 self.authenticated_rpc(state, 222, "/cgi-bin/micromsg-bin/sendappmsg", &request)?;
1389 if reservation.is_some() {
1390 crate::media_send::decode_file_send_correlated(
1391 &reply,
1392 client_id,
1393 &state.own_username,
1394 destination,
1395 &upload.descriptor.aes_key,
1396 )
1397 } else {
1398 crate::media_send::decode_file_send(&reply, client_id)
1399 }
1400 }
1401 pub fn send_image(
1402 &self,
1403 state: &mut AccountState,
1404 destination: &str,
1405 client_id: &str,
1406 upload: &crate::media::UploadedImage,
1407 ) -> Result<crate::media_send::MediaSendResult> {
1408 let request = crate::media_send::image_send_request(state, destination, client_id, upload)?;
1409 crate::media_send::decode_image_send(
1410 &self.authenticated_rpc(state, 110, "/cgi-bin/micromsg-bin/uploadmsgimg", &request)?,
1411 client_id,
1412 &state.own_username,
1413 destination,
1414 )
1415 }
1416 pub fn send_video(
1417 &self,
1418 state: &mut AccountState,
1419 destination: &str,
1420 client_id: &str,
1421 upload: &crate::media::UploadedVideo,
1422 play_seconds: u32,
1423 ) -> Result<crate::media_send::MediaSendResult> {
1424 let request = crate::media_send::video_send_request(
1425 state,
1426 destination,
1427 client_id,
1428 upload,
1429 play_seconds,
1430 )?;
1431 crate::media_send::decode_video_send(
1432 &self.authenticated_rpc(state, 149, "/cgi-bin/micromsg-bin/uploadvideo", &request)?,
1433 client_id,
1434 )
1435 }
1436 pub fn send_voice(
1440 &self,
1441 state: &mut AccountState,
1442 destination: &str,
1443 client_id: &str,
1444 created_at: u32,
1445 duration_ms: u32,
1446 encoded: &[u8],
1447 ) -> Result<crate::media_send::MediaSendResult> {
1448 let mut upload = crate::media_send::VoiceUpload::new(
1449 destination,
1450 client_id,
1451 created_at,
1452 duration_ms,
1453 encoded,
1454 )?;
1455 loop {
1456 let request = upload.request(state)?;
1457 let reply =
1458 self.authenticated_rpc(state, 127, "/cgi-bin/micromsg-bin/uploadvoice", &request)?;
1459 if let Some(result) = upload.accept_reply(&reply)? {
1460 return Ok(result);
1461 }
1462 }
1463 }
1464}
1465
1466fn redirect(fields: &[p::Field]) -> Result<(String, String)> {
1467 let network = p::decode(p::data(fields, 5)?)?;
1468 let hosts = p::decode(p::data(&network, 1)?)?;
1469 let target = |source: &[u8]| -> Result<String> {
1470 let mut out = None;
1471 for f in hosts.iter().filter(|f| f.number == 2) {
1472 let p::Value::Bytes(raw) = &f.value else {
1473 bail!("Invalid redirect map");
1474 };
1475 let item = p::decode(raw)?;
1476 if p::data(&item, 1)? == source {
1477 ensure!(out.is_none(), "Ambiguous redirect");
1478 let name = std::str::from_utf8(p::data(&item, 2)?)?.to_string();
1479 mmtls::validate_host(&name)?;
1480 out = Some(name);
1481 }
1482 }
1483 out.ok_or_else(|| anyhow::anyhow!("Missing redirect mapping"))
1484 };
1485 Ok((
1486 target(b"long.weixin.qq.com")?,
1487 target(b"short.weixin.qq.com")?,
1488 ))
1489}
1490fn reduce_auth(
1491 plain: &[u8],
1492 previous: Option<&AccountState>,
1493 inner: &Ecdh,
1494 device: &DeviceProfile,
1495 cookie: &[u8],
1496) -> Result<AccountState> {
1497 let f = p::decode(plain)?;
1498 ensure!(p::base_code(&f)? == 0, "Rejected auth response");
1499 let mask = u32::try_from(p::scalar(&f, 2)?)?;
1500 for (bit, n) in [(1, 3), (2, 4), (4, 5)] {
1501 if mask & bit != 0 {
1502 p::data(&f, n)?;
1503 }
1504 }
1505 let mut state = previous.cloned().unwrap_or_else(|| AccountState {
1506 schema: 1,
1507 device: device.clone(),
1508 uin: 0,
1509 own_username: String::new(),
1510 cookie: vec![],
1511 session_key: vec![],
1512 request_key: vec![],
1513 response_key: vec![],
1514 auto_auth_key: vec![],
1515 key_info: vec![],
1516 database_password: vec![],
1517 auth_snapshot: AuthSnapshot::default(),
1518 long_host: "long.weixin.qq.com".into(),
1519 short_host: "short.weixin.qq.com".into(),
1520 });
1521 ensure!(
1522 plain.len() <= 1024 * 1024,
1523 "Auth response snapshot too large"
1524 );
1525 state.auth_snapshot.update_mask = mask;
1526 state.auth_snapshot.response = plain.to_vec();
1527 for (number, target) in [
1528 (3, &mut state.auth_snapshot.auth),
1529 (4, &mut state.auth_snapshot.account),
1530 (5, &mut state.auth_snapshot.network),
1531 (6, &mut state.auth_snapshot.ax_auth),
1532 ] {
1533 if let Some(section) = p::optional_data(&f, number)? {
1534 *target = section.to_vec();
1535 }
1536 }
1537 if let Some(account) = p::optional_data(&f, 4)? {
1538 let a = p::decode(account)?;
1539 if let Some(name) = p::optional_data(&a, 1)? {
1540 let name = std::str::from_utf8(name)?;
1541 ensure!(
1542 previous.is_none_or(|s| s.own_username == name),
1543 "Auth account identity changed"
1544 );
1545 state.own_username = name.to_string();
1546 }
1547 }
1548 if mask & 1 != 0 {
1549 let auth = p::data(&f, 3)?;
1550 let a = p::decode(auth)?;
1551 let uin = u32::try_from(p::scalar(&a, 1)?)?;
1552 ensure!(
1553 uin != 0 && previous.is_none_or(|s| s.uin == uin),
1554 "Auth UIN changed"
1555 );
1556 let (session, request, response) = business::account_keys(inner, auth)?;
1557 state.uin = uin;
1558 state.session_key = session;
1559 state.request_key = request;
1560 state.response_key = response;
1561 state.auto_auth_key = p::optional_data(&a, 4)?
1562 .map(p::unbuffer)
1563 .transpose()?
1564 .unwrap_or_default();
1565 if let Some(password) = p::optional_data(&a, 9)? {
1566 let password = p::unbuffer(password)?;
1567 ensure!(
1568 password.is_empty() || password.len() == 32,
1569 "Invalid native database password"
1570 );
1571 state.database_password.zeroize();
1572 state.database_password = password;
1573 }
1574 state.key_info = p::optional_data(&a, 10)?
1575 .map(p::unbuffer)
1576 .transpose()?
1577 .unwrap_or_default();
1578 } else {
1579 ensure!(previous.is_some(), "Initial auth has no key section");
1580 }
1581 ensure!(cookie.len() <= 15, "Invalid auth cookie");
1582 if !cookie.is_empty() {
1583 state.cookie = cookie.to_vec();
1584 }
1585 state.validate()?;
1586 Ok(state)
1587}
1588
1589fn apply_login_notification(
1590 attempt: &mut LoginAttempt,
1591 response: wire::PollResponse,
1592 start_lan: impl FnOnce(&mut LoginAttempt) -> Result<std::sync::Arc<crate::lan_login::Challenge>>,
1593) -> Result<LoginStatus> {
1594 if response.lan_challenge && matches!(response.status, Some(0..=2)) {
1595 attempt.credential3.zeroize();
1598 attempt.credential4.zeroize();
1599 attempt.credential3 = None;
1600 attempt.credential4 = None;
1601 attempt.status = LoginStatus::Scanned;
1602 if let Some(challenge) = &attempt.lan_challenge {
1603 if challenge.active() {
1604 return Ok(attempt.status);
1605 }
1606 attempt.stop_lan_login();
1607 return Err(crate::lan_login::failure("listener_stopped"));
1608 }
1609 if attempt.lan_upload_attempted {
1610 return Err(crate::lan_login::failure("upload_already_attempted"));
1611 }
1612 attempt.lan_upload_attempted = true;
1615 attempt.lan_challenge = Some(start_lan(attempt)?);
1616 return Ok(attempt.status);
1617 }
1618 if let Some(status) = response.status {
1619 attempt.status = match status {
1620 0 if attempt.status == LoginStatus::Scanned => LoginStatus::Scanned,
1621 0 => LoginStatus::Pending,
1622 1 => LoginStatus::Scanned,
1623 2 => LoginStatus::Approved,
1624 _ => LoginStatus::Rejected,
1626 };
1627 if attempt.status == LoginStatus::Approved {
1628 attempt.credential3 = response.credential3;
1629 attempt.credential4 = response.credential4;
1630 }
1631 if matches!(
1632 attempt.status,
1633 LoginStatus::Approved | LoginStatus::Rejected
1634 ) {
1635 attempt.stop_lan_login();
1636 }
1637 }
1638 if attempt
1639 .lan_challenge
1640 .as_ref()
1641 .is_some_and(|challenge| !challenge.active())
1642 {
1643 attempt.stop_lan_login();
1644 return Err(crate::lan_login::failure("listener_stopped"));
1645 }
1646 Ok(attempt.status)
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652 fn attempt(status: LoginStatus, qr: bool) -> LoginAttempt {
1653 LoginAttempt {
1654 schema: 1,
1655 device: state().device.clone(),
1656 computer_name: "synthetic desktop".into(),
1657 qr_png: if qr { vec![1] } else { Vec::new() },
1658 width: 0,
1659 height: 0,
1660 created_at: 1000,
1661 expires_at: 1240,
1662 identifier: vec![1; 16],
1663 notify_key: vec![2; 16],
1664 cookie: vec![3; 15],
1665 key_version: 10014,
1666 long_host: "long.weixin.qq.com".into(),
1667 short_host: "short.weixin.qq.com".into(),
1668 status,
1669 key_info: b"persisted installation key-info".to_vec(),
1670 credential3: Some(vec![4; 16]),
1671 credential4: Some(vec![5; 16]),
1672 lan_upload_attempted: false,
1673 lan_challenge: None,
1674 }
1675 }
1676 #[test]
1677 fn final_approval_request_restores_key_info_for_both_saved_device_and_qr() {
1678 let inner = Ecdh::generate(Nid::SECP224R1).unwrap();
1679 for qr in [false, true] {
1680 let attempt = attempt(LoginStatus::Approved, qr);
1681 let restored: LoginAttempt =
1682 serde_json::from_slice(&serde_json::to_vec(&attempt).unwrap()).unwrap();
1683 assert_eq!(restored.key_info, attempt.key_info);
1684 let request = p::decode(
1685 &restored
1686 .manual_auth_request(&inner.public().unwrap())
1687 .unwrap(),
1688 )
1689 .unwrap();
1690 let aes = p::decode(p::data(&request, 2).unwrap()).unwrap();
1691 let base = p::decode(p::data(&aes, 1).unwrap()).unwrap();
1692 assert_eq!(p::scalar(&base, 6).unwrap(), 1);
1694 let auth = p::decode(p::data(&aes, 2).unwrap()).unwrap();
1695 assert_eq!(
1696 p::unbuffer(p::data(&auth, 5).unwrap()).unwrap(),
1697 attempt.key_info
1698 );
1699 assert!(!format!("{restored:?}").contains("persisted installation"));
1700 }
1701 }
1702 #[test]
1703 fn scanned_and_saved_device_approval_outlive_image_expiry_but_remain_bounded() {
1704 let qr = attempt(LoginStatus::Pending, true);
1705 assert!(qr.validate_at(1239).is_ok());
1706 assert!(qr.validate_at(1240).is_err());
1707 for (status, qr) in [
1708 (LoginStatus::Scanned, true),
1709 (LoginStatus::Approved, true),
1710 (LoginStatus::Pending, false),
1711 ] {
1712 let attempt = attempt(status, qr);
1713 assert!(attempt.validate_at(1240).is_ok());
1714 assert!(attempt.validate_at(1899).is_ok());
1715 assert!(attempt.validate_at(1900).is_err());
1716 }
1717 }
1718 fn notification(status: u32, lan_challenge: bool) -> wire::PollResponse {
1719 wire::PollResponse {
1720 status: Some(status),
1721 lan_challenge,
1722 credential3: (status == 2).then(|| vec![4; 16]),
1723 credential4: (status == 2).then(|| vec![5; 16]),
1724 }
1725 }
1726 fn synthetic_lan() -> std::sync::Arc<crate::lan_login::Challenge> {
1727 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1728 let challenge = crate::lan_login::Challenge::from_listener(
1729 listener,
1730 vec![std::net::Ipv4Addr::LOCALHOST],
1731 crate::lan_login::Scope::new(
1732 std::time::Instant::now() + Duration::from_secs(5),
1733 || false,
1734 ),
1735 )
1736 .unwrap();
1737 challenge.arm(b"synthetic temporary token").unwrap();
1738 std::sync::Arc::new(challenge)
1739 }
1740 #[test]
1741 fn repeated_risk_notifications_upload_once_and_never_install_approval() {
1742 let mut attempt = attempt(LoginStatus::Pending, true);
1743 let challenge = synthetic_lan();
1744 assert_eq!(
1745 apply_login_notification(&mut attempt, notification(2, true), |_| Ok(
1746 challenge.clone()
1747 ))
1748 .unwrap(),
1749 LoginStatus::Scanned
1750 );
1751 assert!(attempt.credential3.is_none() && attempt.credential4.is_none());
1752 assert_eq!(attempt.lan_login_state(), Some("waiting_for_phone"));
1753 for _ in 0..3 {
1754 assert_eq!(
1755 apply_login_notification(&mut attempt, notification(1, true), |_| panic!(
1756 "duplicate upload"
1757 ))
1758 .unwrap(),
1759 LoginStatus::Scanned
1760 );
1761 }
1762 apply_login_notification(&mut attempt, notification(0, false), |_| {
1764 panic!("ordinary poll started LAN")
1765 })
1766 .unwrap();
1767 assert_eq!(attempt.status, LoginStatus::Scanned);
1768 assert!(challenge.active());
1769 let serialized = serde_json::to_string(&attempt).unwrap();
1770 assert!(!serialized.contains("synthetic temporary token"));
1771 assert!(!format!("{attempt:?}").contains("synthetic temporary token"));
1772 let restored: LoginAttempt = serde_json::from_str(&serialized).unwrap();
1773 assert!(restored.lan_login_state().is_none());
1774 assert!(restored.lan_upload_attempted); assert_eq!(
1776 apply_login_notification(&mut attempt, notification(2, false), |_| panic!(
1777 "approval started LAN"
1778 ))
1779 .unwrap(),
1780 LoginStatus::Approved
1781 );
1782 assert!(attempt.lan_login_state().is_none());
1783 assert!(!challenge.active());
1784 assert!(attempt.credential3.is_some() && attempt.credential4.is_some());
1785 }
1786 #[test]
1787 fn failed_upload_is_not_retried_by_the_repeatable_poll_path() {
1788 let mut attempt = attempt(LoginStatus::Pending, true);
1789 let error = apply_login_notification(&mut attempt, notification(1, true), |_| {
1790 Err(crate::lan_login::failure("upload_failed"))
1791 })
1792 .unwrap_err();
1793 assert!(!is_retryable_login_poll_error(&error));
1794 assert_eq!(crate::login_lan_error_reason(&error), Some("upload_failed"));
1795 assert!(attempt.lan_login_state().is_none());
1796 let error = apply_login_notification(&mut attempt, notification(1, true), |_| {
1797 panic!("resubmitted upload")
1798 })
1799 .unwrap_err();
1800 assert_eq!(
1801 crate::login_lan_error_reason(&error),
1802 Some("upload_already_attempted")
1803 );
1804 assert!(attempt.credential3.is_none() && attempt.credential4.is_none());
1805 }
1806 #[test]
1807 fn rejection_or_attempt_drop_closes_the_shared_listener() {
1808 for reject in [false, true] {
1809 let mut attempt = attempt(LoginStatus::Pending, false);
1810 let challenge = synthetic_lan();
1811 apply_login_notification(&mut attempt, notification(1, true), |_| {
1812 Ok(challenge.clone())
1813 })
1814 .unwrap();
1815 if reject {
1816 assert_eq!(
1817 apply_login_notification(&mut attempt, notification(3, true), |_| panic!(
1818 "rejection started LAN"
1819 ))
1820 .unwrap(),
1821 LoginStatus::Rejected
1822 );
1823 }
1824 drop(attempt);
1825 assert!(!challenge.active());
1826 }
1827 }
1828 #[test]
1829 fn ordinary_login_does_not_open_a_lan_listener() {
1830 let mut attempt = attempt(LoginStatus::Pending, true);
1831 for status in [0, 1, 2] {
1832 apply_login_notification(&mut attempt, notification(status, false), |_| {
1833 panic!("ordinary login opened a socket")
1834 })
1835 .unwrap();
1836 assert!(attempt.lan_login_state().is_none());
1837 assert!(!attempt.lan_upload_attempted);
1838 }
1839 }
1840 fn state() -> AccountState {
1841 AccountState {
1842 schema: 1,
1843 device: DeviceProfile {
1844 device_id: vec![1; 16],
1845 device_type: "UnifiedPCLinux synthetic x86_64".into(),
1846 client_version: -227271398,
1847 },
1848 uin: 123,
1849 own_username: "synthetic-user".into(),
1850 cookie: vec![5],
1851 session_key: vec![6; 16],
1852 request_key: vec![7; 24],
1853 response_key: vec![8; 24],
1854 auto_auth_key: vec![],
1855 key_info: vec![],
1856 database_password: vec![],
1857 auth_snapshot: AuthSnapshot::default(),
1858 long_host: "long.weixin.qq.com".into(),
1859 short_host: "short.weixin.qq.com".into(),
1860 }
1861 }
1862 #[test]
1863 fn challenge_poll_retries_only_transport_availability_failures() {
1864 use std::io::{Error, ErrorKind};
1865 for kind in [
1866 ErrorKind::TimedOut,
1867 ErrorKind::WouldBlock,
1868 ErrorKind::ConnectionReset,
1869 ErrorKind::UnexpectedEof,
1870 ] {
1871 assert!(is_retryable_login_poll_error(
1872 &anyhow::Error::new(Error::from(kind)).context("polling challenge")
1873 ));
1874 }
1875 for message in [
1876 "MMTLS deadline exceeded",
1877 "MMTLS connection closed",
1878 "Native shortlink HTTP status 503",
1879 ] {
1880 assert!(is_retryable_login_poll_error(&anyhow::anyhow!("{message}")));
1881 }
1882 for message in [
1883 "Login poll returned -13",
1884 "Native envelope returned -13",
1885 "Native shortlink HTTP status 403",
1886 "Native shortlink HTTP status 5030",
1887 "Missing authentication tag",
1888 "Unexpected MMTLS header",
1889 "Truncated structure",
1890 ] {
1891 assert!(!is_retryable_login_poll_error(&anyhow::anyhow!(
1892 "{message}"
1893 )));
1894 }
1895 assert!(!is_retryable_login_poll_error(
1896 &Error::from(ErrorKind::PermissionDenied).into()
1897 ));
1898 }
1899 #[test]
1900 fn secrets_not_debugged() {
1901 let s = state();
1902 let text = format!("{s:?}");
1903 assert!(!text.contains("synthetic-user"));
1904 assert!(!text.contains("123"));
1905 assert!(text.contains("redacted"));
1906 }
1907 #[test]
1908 fn account_state_roundtrips_without_number_loss() {
1909 let s = state();
1910 let json = serde_json::to_vec(&s).unwrap();
1911 let restored: AccountState = serde_json::from_slice(&json).unwrap();
1912 restored.validate().unwrap();
1913 assert_eq!(restored.uin, s.uin);
1914 }
1915 #[test]
1916 fn database_password_follows_auth_update_gate_and_optional_presence() {
1917 let mut old = state();
1918 old.database_password = vec![11; 32];
1919 let inner = Ecdh::generate(Nid::SECP224R1).unwrap();
1920 let response = |mask, password: Option<&[u8]>| {
1921 let mut fields = vec![
1922 p::uint(1, old.uin as u64),
1923 p::bytes(3, &p::buffer(&[1; 16])),
1924 p::uint(19, 0),
1925 ];
1926 if let Some(bytes) = password {
1927 fields.push(p::bytes(9, &p::buffer(bytes)));
1928 }
1929 p::message(&[
1930 p::bytes(1, &p::uint(1, 0)),
1931 p::uint(2, mask),
1932 p::bytes(3, &p::message(&fields).unwrap()),
1933 p::bytes(6, b"synthetic-ax"),
1934 ])
1935 .unwrap()
1936 };
1937 let gated_off = response(0, Some(&[22; 32]));
1938 let unchanged = reduce_auth(&gated_off, Some(&old), &inner, &old.device, &[]).unwrap();
1939 assert_eq!(unchanged.database_password, old.database_password);
1940 assert_eq!(unchanged.auth_snapshot.update_mask, 0);
1941 assert_eq!(unchanged.auth_snapshot.response, gated_off);
1942 assert_eq!(unchanged.auth_snapshot.ax_auth, b"synthetic-ax");
1943 let accepted = reduce_auth(
1944 &response(1, Some(&[22; 32])),
1945 Some(&old),
1946 &inner,
1947 &old.device,
1948 &[],
1949 )
1950 .unwrap();
1951 assert_eq!(accepted.database_password, vec![22; 32]);
1952 let absent = reduce_auth(
1953 &response(1, None),
1954 Some(&accepted),
1955 &inner,
1956 &old.device,
1957 &[],
1958 )
1959 .unwrap();
1960 assert_eq!(absent.database_password, accepted.database_password);
1961 let empty = reduce_auth(
1962 &response(1, Some(&[])),
1963 Some(&absent),
1964 &inner,
1965 &old.device,
1966 &[],
1967 )
1968 .unwrap();
1969 assert!(empty.database_password.is_empty());
1970 assert!(reduce_auth(
1971 &response(1, Some(&[22; 31])),
1972 Some(&old),
1973 &inner,
1974 &old.device,
1975 &[]
1976 )
1977 .is_err());
1978 let mut legacy = serde_json::to_value(&old).unwrap();
1979 legacy.as_object_mut().unwrap().remove("database_password");
1980 legacy.as_object_mut().unwrap().remove("auth_snapshot");
1981 let restored: AccountState = serde_json::from_value(legacy).unwrap();
1982 restored.validate().unwrap();
1983 assert!(restored.database_password.is_empty());
1984 }
1985 #[test]
1986 fn refresh_identity_and_clear_tokens() {
1987 let old = state();
1988 let inner = Ecdh::generate(Nid::SECP224R1).unwrap();
1989 let auth = p::message(&[
1990 p::uint(1, old.uin as u64),
1991 p::bytes(3, &p::buffer(&[1; 16])),
1992 p::uint(19, 0),
1993 ])
1994 .unwrap();
1995 let reply = p::message(&[
1996 p::bytes(1, &p::uint(1, 0)),
1997 p::uint(2, 1),
1998 p::bytes(3, &auth),
1999 p::bytes(4, &p::bytes(1, old.own_username.as_bytes())),
2000 ])
2001 .unwrap();
2002 let next = reduce_auth(&reply, Some(&old), &inner, &old.device, &[]).unwrap();
2003 assert!(next.auto_auth_key.is_empty());
2004 assert!(next.request_key.is_empty());
2005 let bad = p::message(&[
2006 p::bytes(1, &p::uint(1, 0)),
2007 p::uint(2, 0),
2008 p::bytes(4, &p::bytes(1, b"foreign")),
2009 ])
2010 .unwrap();
2011 assert!(reduce_auth(&bad, Some(&old), &inner, &old.device, &[]).is_err());
2012 }
2013}