1use crate::{protobuf as p, AuthSnapshot};
6use anyhow::{ensure, Result};
7use zeroize::Zeroize;
8
9const MAX_AUTH_BYTES: usize = 1024 * 1024;
10const MAX_ENTRIES: usize = 4096;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum CallHostPolicy {
14 Ordinary,
16 AxSelected,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum CallHostRoute {
25 AppAuth,
26 RingingAck,
27 Transfer,
28 Invite,
29 Cancel,
30 LiveSync,
31}
32impl CallHostRoute {
33 pub const fn command(self) -> u16 {
34 match self {
35 Self::AppAuth => 7994,
36 Self::RingingAck => 8069,
37 Self::Transfer => 11421,
38 Self::Invite => 10342,
39 Self::Cancel => 9332,
40 Self::LiveSync => 9262,
41 }
42 }
43 pub const fn path(self) -> &'static str {
44 match self {
45 Self::AppAuth => "/cgi-bin/micromsg-bin/mm_ilink_cloud_app_auth",
46 Self::RingingAck => "/cgi-bin/micromsg-bin/voipilinkack",
47 Self::Transfer => "/cgi-bin/micromsg-bin/mmilinktransfer",
48 Self::Invite => "/cgi-bin/micromsg-bin/voipilinkinvite",
49 Self::Cancel => "/cgi-bin/micromsg-bin/voipilinkcancel",
50 Self::LiveSync => "/cgi-bin/micromsg-bin/voipilinklivesync",
51 }
52 }
53}
54pub fn is_teardown_request(route: CallHostRoute, common: &[u8]) -> Result<bool> {
58 if route == CallHostRoute::Cancel {
59 return Ok(true);
60 }
61 is_terminal_transfer(route, common, super::signaling::CallOperation::Exit)
62}
63pub fn is_reject_request(route: CallHostRoute, common: &[u8]) -> Result<bool> {
66 is_terminal_transfer(route, common, super::signaling::CallOperation::Reject)
67}
68fn is_terminal_transfer(
69 route: CallHostRoute,
70 common: &[u8],
71 operation: super::signaling::CallOperation,
72) -> Result<bool> {
73 if route != CallHostRoute::Transfer {
74 return Ok(false);
75 }
76 let command = operation.command_id();
77 let rejecting = operation == super::signaling::CallOperation::Reject;
78 let common = super::signaling::CommonRequest::decode(common)?;
79 if !common.unknown_fields.is_empty() {
80 return Ok(false);
81 }
82 let body = common
83 .body
84 .ok_or_else(|| anyhow::anyhow!("Missing call transfer body"))?;
85 let delegate = Fields::decode(&body)?;
86 if delegate.0.iter().any(|f| !(1..=5).contains(&f.number))
87 || delegate.required_u64(1)? != 1901
88 || delegate.required_u64(2)? != u64::from(command)
89 || delegate.required_bytes(3)? != b"wechat"
90 || delegate.required_bytes(4)? != operation.path().as_bytes()
91 {
92 return Ok(false);
93 }
94 let frame = super::app_frame::TaskFrame::decode(delegate.required_bytes(5)?, command)?;
95 const INTEGER_KEYS: &[u32] = &[
96 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 17, 18, 20, 21, 22, 23, 25, 26, 28, 29, 30, 31,
97 ];
98 if frame.command != command
99 || frame.integers.keys().any(|n| !INTEGER_KEYS.contains(n))
100 || frame.strings.keys().any(|n| ![14, 24, 27].contains(n))
101 || frame.integers.get(&1) != Some(&1)
102 || frame.integers.get(&26) != Some(&4)
103 || [7, 8, 21, 25]
104 .iter()
105 .any(|n| frame.integers.get(n) != Some(&0))
106 || frame.strings.get(&24).map(Vec::as_slice) != Some(b"wechat")
107 || frame.strings.get(&27).is_none_or(Vec::is_empty)
108 {
109 return Ok(false);
110 }
111 let clear = zeroize::Zeroizing::new(super::app_frame::lz4_decode(&frame.payload, 64 * 1024)?);
112 let cloud = Fields::decode(&clear)?;
113 if cloud.0.iter().any(|f| ![1, 2].contains(&f.number)) {
114 return Ok(false);
115 }
116 let base = Fields::decode(cloud.required_bytes(1)?)?;
117 let uin = base.required_u64(1)?;
118 if base.0.iter().any(|f| ![1, 2, 3].contains(&f.number))
119 || uin == 0
120 || base.required_bytes(2)?.is_empty()
121 || i32::try_from(base.required_u64(3)?).is_err()
122 || frame.integers.get(&2) != Some(&uin)
123 || frame.integers.get(&22) != Some(&uin)
124 {
125 return Ok(false);
126 }
127 let exit = Fields::decode(cloud.required_bytes(2)?)?;
128 let last_field = if rejecting { 6 } else { 8 };
129 if exit.0.iter().any(|f| !(1..=last_field).contains(&f.number))
130 || exit.required_bytes(1)?.is_empty()
131 || exit.required_bytes(2)?.is_empty()
132 || exit.required_u64(3)? == 0
133 || exit.required_u64(4)? == 0
134 || [5, 6].iter().any(|n| {
135 exit.required_u64(*n)
136 .map_or(true, |v| u32::try_from(v).is_err())
137 })
138 || if rejecting {
139 exit.required_u64(6)? != 1
140 } else {
141 u32::try_from(exit.required_u64(7)?).is_err() || exit.required_u64(8)? > 1
142 }
143 {
144 return Ok(false);
145 }
146 let sdk = Fields::decode(exit.required_bytes(1)?)?;
147 if sdk.0.iter().any(|f| ![1, 2].contains(&f.number))
148 || u32::try_from(sdk.required_u64(1)?).is_err()
149 || sdk.required_bytes(2)?.is_empty()
150 || std::str::from_utf8(sdk.required_bytes(2)?).is_err()
151 {
152 return Ok(false);
153 }
154 let group = Fields::decode(exit.required_bytes(2)?)?;
155 if group.0.iter().any(|f| !(1..=7).contains(&f.number))
156 || group.required_bytes(1)? != b"wechat"
157 || group.required_bytes(3)? != b"voip-2p"
158 || group.required_u64(5)? != 1
159 {
160 return Ok(false);
161 }
162 if let Some(value) = crate::protobuf::optional(&group.0, 2)? {
163 if !matches!(value, crate::protobuf::Value::Varint(_)) {
164 return Ok(false);
165 }
166 }
167 for field in [4, 6] {
168 if let Some(value) = group.optional_bytes(field)? {
169 if value.len() > 1024 || std::str::from_utf8(value).is_err() {
170 return Ok(false);
171 }
172 }
173 }
174 group.optional_u32(7)?;
175 Ok(true)
176}
177
178impl From<super::personal::HostOperation> for CallHostRoute {
179 fn from(value: super::personal::HostOperation) -> Self {
180 match value {
181 super::personal::HostOperation::RingingAck => Self::RingingAck,
182 super::personal::HostOperation::Invite => Self::Invite,
183 super::personal::HostOperation::Cancel => Self::Cancel,
184 super::personal::HostOperation::LiveSync => Self::LiveSync,
185 }
186 }
187}
188
189pub fn selected_policy(snapshot: &AuthSnapshot, command: u16) -> Result<CallHostPolicy> {
194 ensure!(
195 matches!(command, 7994 | 8069 | 11421 | 10342 | 9332 | 9262),
196 "Unsupported call host command"
197 );
198 let root = Fields::decode(&snapshot.response)?;
199 let base = Fields::decode(root.required_bytes(1)?)?;
200 ensure!(
201 i32::try_from(base.required_u64(1)? as i64)? == 0,
202 "Rejected call auth snapshot"
203 );
204 let mask = u32::try_from(root.required_u64(2)?)?;
205 ensure!(
206 mask == snapshot.update_mask,
207 "Call auth snapshot mask mismatch"
208 );
209 if mask & 8 == 0 {
210 return Ok(CallHostPolicy::Ordinary);
211 }
212 let Some(section) = root.optional_bytes(6)? else {
213 return Ok(CallHostPolicy::Ordinary);
214 };
215 let list = Fields::decode(section)?;
216 let mut count = 0;
217 let mut selected = CallHostPolicy::Ordinary;
218 for field in &list.0 {
219 if field.number != 2 {
220 continue;
221 }
222 count += 1;
223 ensure!(count <= MAX_ENTRIES, "Too many call host auth entries");
224 let p::Value::Bytes(raw) = &field.value else {
225 anyhow::bail!("Invalid call host auth entry wire type");
226 };
227 let entry = Fields::decode(raw)?;
228 entry.optional_u32(1)?;
230 let cgi = entry.optional_u32(4)?;
231 entry.optional_bytes(3)?;
232 for number in [2, 5] {
233 let nested = Fields::decode(entry.optional_bytes(number)?.unwrap_or_default())?;
234 nested.optional_bytes(2)?;
236 }
237 if cgi == u32::from(command) {
238 selected = CallHostPolicy::AxSelected;
239 }
240 }
241 Ok(selected)
242}
243
244struct Fields(Vec<p::Field>);
248impl Fields {
249 fn decode(raw: &[u8]) -> Result<Self> {
250 ensure!(raw.len() <= MAX_AUTH_BYTES, "Call auth snapshot too large");
251 let result = Self(p::decode(raw)?);
252 ensure!(result.0.len() <= MAX_ENTRIES, "Too many call auth fields");
253 Ok(result)
254 }
255 fn optional_bytes(&self, number: u32) -> Result<Option<&[u8]>> {
256 p::optional_data(&self.0, number)
257 }
258 fn required_bytes(&self, number: u32) -> Result<&[u8]> {
259 self.optional_bytes(number)?
260 .ok_or_else(|| anyhow::anyhow!("Missing call auth bytes field {number}"))
261 }
262 fn required_u64(&self, number: u32) -> Result<u64> {
263 match p::optional(&self.0, number)? {
264 Some(p::Value::Varint(value)) => Ok(*value),
265 _ => anyhow::bail!("Missing or invalid call auth integer {number}"),
266 }
267 }
268 fn optional_u32(&self, number: u32) -> Result<u32> {
269 match p::optional(&self.0, number)? {
270 None => Ok(0),
271 Some(p::Value::Varint(value)) => Ok(u32::try_from(*value)?),
272 _ => anyhow::bail!("Invalid call auth scalar {number}"),
273 }
274 }
275}
276impl Drop for Fields {
277 fn drop(&mut self) {
278 for field in &mut self.0 {
279 if let p::Value::Bytes(bytes) = &mut field.value {
280 bytes.zeroize();
281 }
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289 fn snapshot(mask: u32, selected: u32) -> AuthSnapshot {
290 let entry = p::message(&[p::uint(4, u64::from(selected))]).unwrap();
291 let list = p::bytes(2, &entry);
292 let mut snapshot = AuthSnapshot::default();
293 snapshot.update_mask = mask;
294 snapshot.response = p::message(&[
295 p::bytes(1, &[8, 0]),
296 p::uint(2, u64::from(mask)),
297 p::bytes(6, &list),
298 ])
299 .unwrap();
300 snapshot
301 }
302 #[test]
303 fn active_exact_command_selects_ax_and_unrelated_command_does_not() {
304 let state = snapshot(8, 7994);
305 assert_eq!(
306 selected_policy(&state, 7994).unwrap(),
307 CallHostPolicy::AxSelected
308 );
309 assert_eq!(
310 selected_policy(&state, 10342).unwrap(),
311 CallHostPolicy::Ordinary
312 );
313 let mut retained = snapshot(0, 7994);
314 retained.ax_auth = vec![0xff; 32];
315 assert_eq!(
316 selected_policy(&retained, 7994).unwrap(),
317 CallHostPolicy::Ordinary
318 );
319 retained.update_mask = 8;
320 assert!(selected_policy(&retained, 7994).is_err());
321 assert!(selected_policy(&AuthSnapshot::default(), 7994).is_err());
322 }
323
324 #[test]
325 fn expired_scope_transfer_exception_requires_matching_exit_frames() {
326 fn request(delegate: u64, path: &[u8], task: u32) -> Vec<u8> {
327 request_override(delegate, path, task, None)
328 }
329 fn request_override(
330 delegate: u64,
331 path: &[u8],
332 task: u32,
333 extra: Option<(u32, u64)>,
334 ) -> Vec<u8> {
335 let sdk = crate::protobuf::message(&[
336 crate::protobuf::uint(1, 0x050000cb),
337 crate::protobuf::bytes(2, b"0x1000000"),
338 ])
339 .unwrap();
340 let group = crate::protobuf::message(&[
341 crate::protobuf::bytes(1, b"wechat"),
342 crate::protobuf::uint(2, 99),
343 crate::protobuf::bytes(3, b"voip-2p"),
344 crate::protobuf::bytes(4, b"group"),
345 crate::protobuf::uint(5, 1),
346 crate::protobuf::bytes(6, b""),
347 crate::protobuf::uint(7, 0),
348 ])
349 .unwrap();
350 let mut terminal = vec![
351 crate::protobuf::bytes(1, &sdk),
352 crate::protobuf::bytes(2, &group),
353 crate::protobuf::uint(3, 7),
354 crate::protobuf::uint(4, 9),
355 crate::protobuf::uint(5, 0),
356 crate::protobuf::uint(6, 1),
357 ];
358 if delegate != 3095 {
359 terminal.extend([crate::protobuf::uint(7, 0), crate::protobuf::uint(8, 1)]);
360 }
361 let exit = crate::protobuf::message(&terminal).unwrap();
362 let base = crate::protobuf::message(&[
363 crate::protobuf::uint(1, 17),
364 crate::protobuf::bytes(2, b"Ldevice"),
365 crate::protobuf::uint(3, 1),
366 ])
367 .unwrap();
368 let cloud = crate::protobuf::message(&[
369 crate::protobuf::bytes(1, &base),
370 crate::protobuf::bytes(2, &exit),
371 ])
372 .unwrap();
373 let mut integers = std::collections::BTreeMap::from([
374 (1, 1),
375 (2, 17),
376 (7, 0),
377 (8, 0),
378 (21, 0),
379 (22, 17),
380 (25, 0),
381 (26, 4),
382 ]);
383 if let Some((field, value)) = extra {
384 integers.insert(field, value);
385 }
386 let frame = super::super::app_frame::TaskFrame {
387 command: task,
388 integers,
389 strings: std::collections::BTreeMap::from([
390 (24, b"wechat".to_vec()),
391 (27, b"server".to_vec()),
392 ]),
393 payload: super::super::app_frame::lz4_literal(&cloud).unwrap(),
394 }
395 .encode()
396 .unwrap();
397 let body = crate::protobuf::message(&[
398 crate::protobuf::uint(1, 1901),
399 crate::protobuf::uint(2, delegate),
400 crate::protobuf::bytes(3, b"wechat"),
401 crate::protobuf::bytes(4, path),
402 crate::protobuf::bytes(5, &frame),
403 ])
404 .unwrap();
405 super::super::signaling::CommonRequest {
406 base_request: Some(vec![]),
407 body: Some(body),
408 unknown_fields: vec![],
409 }
410 .encode()
411 .unwrap()
412 }
413 let exit = super::super::signaling::CallOperation::Exit
414 .path()
415 .as_bytes();
416 let join = super::super::signaling::CallOperation::Join
417 .path()
418 .as_bytes();
419 assert!(is_teardown_request(CallHostRoute::Transfer, &request(3380, exit, 3380)).unwrap());
420 for raw in [
421 request(3361, join, 3361),
422 request(3380, join, 3380),
423 request(3380, exit, 3361),
424 request(3361, exit, 3380),
425 ] {
426 assert!(!is_teardown_request(CallHostRoute::Transfer, &raw).unwrap_or(false));
427 }
428 for extra in [(99, 0), (7, 11), (26, 0)] {
429 assert!(!is_teardown_request(
430 CallHostRoute::Transfer,
431 &request_override(3380, exit, 3380, Some(extra))
432 )
433 .unwrap_or(false));
434 }
435 let reject = super::super::signaling::CallOperation::Reject
436 .path()
437 .as_bytes();
438 assert!(is_reject_request(CallHostRoute::Transfer, &request(3095, reject, 3095)).unwrap());
439 assert!(
440 !is_teardown_request(CallHostRoute::Transfer, &request(3095, reject, 3095)).unwrap()
441 );
442 for raw in [
443 request(3380, exit, 3380),
444 request(3095, join, 3095),
445 request(3095, reject, 3361),
446 ] {
447 assert!(!is_reject_request(CallHostRoute::Transfer, &raw).unwrap_or(false));
448 }
449 for extra in [(99, 0), (7, 11), (26, 0)] {
450 assert!(!is_reject_request(
451 CallHostRoute::Transfer,
452 &request_override(3095, reject, 3095, Some(extra))
453 )
454 .unwrap_or(false));
455 }
456 assert!(!is_teardown_request(CallHostRoute::AppAuth, &[]).unwrap());
457 assert!(is_teardown_request(CallHostRoute::Transfer, &[0xff]).is_err());
458 }
459}