Skip to main content

wechat_protocol/calls/
app_frame.rs

1//! Selected call-app task framing. Only the explicitly negotiated crypto0 app
2//! session is supported. Host transport authentication and media AEAD are separate.
3//! These functions prepare/parse bytes; they do not send or retry an operation.
4
5use super::{
6    app_session::{AppAuthority, AppSession},
7    signaling::CallOperation,
8};
9use crate::protobuf::{self as p, Field, Value};
10use anyhow::{bail, ensure, Result};
11use std::collections::BTreeMap;
12
13const MAX_BODY: usize = 1024 * 1024;
14const MAX_HEADER: usize = 0x2800;
15
16/// Selected SDK/device and host configuration. The app's cloud-device string is
17/// distinct from its fresh 32-byte auth device and the host account device ID.
18pub struct AppFrameParameters<'a> {
19    pub sdk_version: u32,
20    pub cloud_device_id: &'a [u8],
21    pub host_client_version: u32,
22    pub app_config_version: u32,
23}
24
25/// A decoded app task header. Values may contain private identities, so no Debug.
26pub struct TaskFrame {
27    pub command: u32,
28    pub integers: BTreeMap<u32, u64>,
29    pub strings: BTreeMap<u32, Vec<u8>>,
30    pub payload: Vec<u8>,
31}
32
33/// Build the Cloud app request, selected AppInstance task frame, then host11421
34/// delegate. The full accepted app UIN goes into header2 and22. It is not the
35/// host UIN, a request counter or a value to narrow through JavaScript floats.
36pub fn prepare_transfer(
37    session: &AppSession,
38    current: AppAuthority,
39    operation: CallOperation,
40    inner: &[u8],
41    parameters: &AppFrameParameters<'_>,
42) -> Result<Vec<u8>> {
43    session.validate_authority(current)?;
44    i32::try_from(parameters.sdk_version)?;
45    std::str::from_utf8(parameters.cloud_device_id)?;
46    ensure!(
47        !inner.is_empty() && inner.len() <= MAX_BODY,
48        "Invalid Cloud operation body"
49    );
50    ensure!(
51        !parameters.cloud_device_id.is_empty() && parameters.cloud_device_id.len() <= 128,
52        "Invalid Cloud device identity"
53    );
54    let base = p::message(&[
55        p::uint(1, session.app_uin()),
56        p::bytes(2, parameters.cloud_device_id),
57        p::uint(3, u64::from(parameters.sdk_version)),
58    ])?;
59    let cloud = p::message(&[p::bytes(1, &base), p::bytes(2, inner)])?;
60    let integers = BTreeMap::from([
61        (1, 1),
62        (2, session.app_uin()),
63        (3, 0),
64        (4, 0),
65        (5, 0xb1400),
66        (6, 15),
67        (7, 0),
68        (8, 0),
69        (9, 0),
70        (10, 1),
71        (11, 0),
72        (12, 0),
73        (13, 0),
74        (17, 0),
75        (18, 1),
76        (20, 1506),
77        (21, 0),
78        (22, session.app_uin()),
79        (23, 0),
80        (25, 0),
81        (26, 4),
82        (28, u64::from(parameters.app_config_version)),
83        (29, 1),
84        (30, u64::from(parameters.host_client_version)),
85        (31, 0),
86    ]);
87    let strings = BTreeMap::from([
88        (14, vec![]),
89        (24, b"wechat".to_vec()),
90        (27, session.server_id().to_vec()),
91    ]);
92    let frame = TaskFrame {
93        command: operation.command_id(),
94        integers,
95        strings,
96        payload: lz4_literal(&cloud)?,
97    }
98    .encode()?;
99    p::message(&[
100        p::uint(1, 1901),
101        p::uint(2, u64::from(operation.command_id())),
102        p::bytes(3, b"wechat"),
103        p::bytes(4, operation.path().as_bytes()),
104        p::bytes(5, &frame),
105    ])
106}
107
108/// Parse an authenticated successful host11421 CommonResponse body. Every inner
109/// result remains independently checked, including delegate/task/app/Cloud.
110/// Return the Cloud operation body only after successful complete framing.
111pub fn decode_transfer_reply(
112    session: &AppSession,
113    current: AppAuthority,
114    operation: CallOperation,
115    delegate: &[u8],
116) -> Result<Vec<u8>> {
117    session.validate_authority(current)?;
118    let fields = decode(delegate)?;
119    ensure!(
120        signed(&fields, 1, Some(0))? == 0,
121        "Cloud delegate operation rejected"
122    );
123    ensure!(
124        p::optional_data(&fields, 3)?.is_none_or(|v| v.is_empty()),
125        "Unsupported Cloud delegate extension"
126    );
127    let task = TaskFrame::decode(required_data(&fields, 2)?, operation.command_id())?;
128    let integer = |key: u32| {
129        task.integers
130            .get(&key)
131            .copied()
132            .ok_or_else(|| anyhow::anyhow!("Missing app task header {key}"))
133    };
134    ensure!(
135        signed_value(integer(4)?)? == 0,
136        "Cloud task operation rejected"
137    );
138    ensure!(
139        signed_value(integer(23)?)? == 0,
140        "Cloud app operation rejected"
141    );
142    ensure!(
143        integer(7)? == 0 && integer(8)? == 0 && integer(21)? == 0 && integer(25)? == 0,
144        "Unsupported app task transform"
145    );
146    if let Some(uin) = task.integers.get(&22) {
147        ensure!(*uin == session.app_uin(), "Cloud app identity changed");
148    }
149    if let Some(app) = task.strings.get(&24) {
150        ensure!(
151            app.is_empty() || app == b"wechat",
152            "Cloud application changed"
153        );
154    }
155    if let Some(server) = task.strings.get(&27) {
156        ensure!(
157            server.is_empty() || server == session.server_id(),
158            "Cloud app session changed"
159        );
160    }
161    let clear = match integer(26)? {
162        0 => task.payload,
163        4 => lz4_decode(&task.payload, MAX_BODY)?,
164        _ => bail!("Unsupported app compression"),
165    };
166    let cloud = decode(&clear)?;
167    let base = decode(required_data(&cloud, 1)?)?;
168    ensure!(signed(&base, 1, None)? == 0, "Cloud operation rejected");
169    if let Some(Value::Varint(uin)) = p::optional(&cloud, 2)? {
170        ensure!(*uin == session.app_uin(), "Cloud response identity changed");
171    } else {
172        ensure!(
173            p::optional(&cloud, 2)?.is_none(),
174            "Invalid Cloud response identity"
175        );
176    }
177    Ok(required_data(&cloud, 3)?.to_vec())
178}
179
180impl TaskFrame {
181    pub fn encode(&self) -> Result<Vec<u8>> {
182        ensure!(
183            self.command != 0 && self.payload.len() <= MAX_BODY,
184            "Invalid app task command or body"
185        );
186        ensure!(
187            self.integers.len() <= 1024 && self.strings.len() <= 1024,
188            "Too many app task headers"
189        );
190        let mut header = vec![1];
191        for (key, value) in &self.integers {
192            ensure!(*key != 0, "Invalid app task header key");
193            header.extend(p::varint(u64::from(*key)));
194            header.extend(p::varint(*value));
195        }
196        header.push(0);
197        for (key, value) in &self.strings {
198            ensure!(
199                *key != 0 && value.len() <= 1024,
200                "Invalid app task byte header"
201            );
202            header.extend(p::varint(u64::from(*key)));
203            header.extend(p::varint(value.len() as u64));
204            header.extend(value);
205        }
206        header.push(0);
207        ensure!(header.len() <= MAX_HEADER, "App task header too large");
208        header.extend(p::varint(header.len() as u64));
209        let total = 16 + header.len() + self.payload.len();
210        ensure!(total <= MAX_BODY, "App task frame too large");
211        let mut out = Vec::with_capacity(total);
212        out.extend((total as u32).to_be_bytes());
213        out.extend([0x11, 0x10, 0x07, 0x6d]);
214        out.extend(self.command.to_be_bytes());
215        out.extend([0; 4]);
216        out.extend(header);
217        out.extend(&self.payload);
218        Ok(out)
219    }
220    pub fn decode(raw: &[u8], expected_command: u32) -> Result<Self> {
221        ensure!(
222            (16..=MAX_BODY).contains(&raw.len()),
223            "Invalid app task size"
224        );
225        ensure!(
226            u32::from_be_bytes(raw[..4].try_into()?) as usize == raw.len()
227                && raw[4..8] == [0x11, 0x10, 0x07, 0x6d]
228                && raw[12..16] == [0; 4],
229            "Invalid app task prefix"
230        );
231        let command = u32::from_be_bytes(raw[8..12].try_into()?);
232        ensure!(command == expected_command, "Unexpected app task command");
233        let header = &raw[16..];
234        let mut pos = 0;
235        ensure!(
236            read(header, &mut pos)? == 1,
237            "Unsupported app task header version"
238        );
239        let mut integers = BTreeMap::new();
240        let mut strings = BTreeMap::new();
241        let mut previous = 0;
242        loop {
243            let key = u32::try_from(read(header, &mut pos)?)?;
244            if key == 0 {
245                break;
246            }
247            ensure!(
248                key > previous && integers.len() < 1024,
249                "Invalid app integer header order"
250            );
251            previous = key;
252            integers.insert(key, read(header, &mut pos)?);
253            ensure!(pos <= MAX_HEADER, "App header too large");
254        }
255        previous = 0;
256        loop {
257            let key = u32::try_from(read(header, &mut pos)?)?;
258            if key == 0 {
259                break;
260            }
261            ensure!(
262                key > previous && strings.len() < 1024,
263                "Invalid app byte header order"
264            );
265            previous = key;
266            let len = usize::try_from(read(header, &mut pos)?)?;
267            ensure!(
268                len <= 1024 && len <= header.len() - pos,
269                "App byte header truncated"
270            );
271            strings.insert(key, header[pos..pos + len].to_vec());
272            pos += len;
273            ensure!(pos <= MAX_HEADER, "App header too large");
274        }
275        let footer = pos;
276        ensure!(
277            read(header, &mut pos)? == footer as u64 && footer <= MAX_HEADER,
278            "Invalid app header footer"
279        );
280        Ok(Self {
281            command,
282            integers,
283            strings,
284            payload: header[pos..].to_vec(),
285        })
286    }
287}
288
289fn read(raw: &[u8], pos: &mut usize) -> Result<u64> {
290    let start = *pos;
291    let value = p::read_varint(raw, pos)?;
292    ensure!(
293        raw[start..*pos] == p::varint(value),
294        "Noncanonical app header integer"
295    );
296    Ok(value)
297}
298fn decode(raw: &[u8]) -> Result<Vec<Field>> {
299    ensure!(raw.len() <= MAX_BODY, "App message too large");
300    super::signaling::decode_fields(raw)
301}
302fn required_data(fields: &[Field], number: u32) -> Result<&[u8]> {
303    p::optional_data(fields, number)?
304        .ok_or_else(|| anyhow::anyhow!("Missing app bytes field {number}"))
305}
306fn signed_value(value: u64) -> Result<i32> {
307    Ok(i32::try_from(value as i64)?)
308}
309fn signed(fields: &[Field], number: u32, default: Option<i32>) -> Result<i32> {
310    match p::optional(fields, number)? {
311        Some(Value::Varint(value)) => signed_value(*value),
312        None => default.ok_or_else(|| anyhow::anyhow!("Missing app result {number}")),
313        _ => bail!("Invalid app result {number}"),
314    }
315}
316
317/// Raw LZ4 literal block. This is legal block syntax, not a framed LZ4 file.
318pub fn lz4_literal(raw: &[u8]) -> Result<Vec<u8>> {
319    ensure!(raw.len() <= MAX_BODY, "App compression input too large");
320    let mut out = vec![(raw.len().min(15) as u8) << 4];
321    if raw.len() >= 15 {
322        let mut remaining = raw.len() - 15;
323        while remaining >= 255 {
324            out.push(255);
325            remaining -= 255;
326        }
327        out.push(remaining as u8);
328    }
329    out.extend(raw);
330    Ok(out)
331}
332pub fn lz4_decode(raw: &[u8], limit: usize) -> Result<Vec<u8>> {
333    ensure!(
334        raw.len() <= MAX_BODY && limit <= MAX_BODY && !raw.is_empty(),
335        "Invalid app LZ4 size"
336    );
337    let mut pos = 0;
338    let mut out = Vec::new();
339    while pos < raw.len() {
340        let token = raw[pos];
341        pos += 1;
342        let literals = length(raw, &mut pos, usize::from(token >> 4), limit)?;
343        ensure!(
344            literals <= raw.len() - pos && literals <= limit - out.len(),
345            "App LZ4 literal exceeds bounds"
346        );
347        out.extend(&raw[pos..pos + literals]);
348        pos += literals;
349        if pos == raw.len() {
350            return Ok(out);
351        }
352        ensure!(raw.len() - pos >= 2, "App LZ4 offset truncated");
353        let offset = usize::from(u16::from_le_bytes(raw[pos..pos + 2].try_into()?));
354        pos += 2;
355        ensure!(
356            offset > 0 && offset <= out.len(),
357            "Invalid app LZ4 backreference"
358        );
359        let repeated = length(raw, &mut pos, usize::from(token & 15), limit)?
360            .checked_add(4)
361            .ok_or_else(|| anyhow::anyhow!("App LZ4 length overflow"))?;
362        ensure!(
363            repeated <= limit - out.len(),
364            "App LZ4 output exceeds bound"
365        );
366        for _ in 0..repeated {
367            out.push(out[out.len() - offset]);
368        }
369    }
370    bail!("App LZ4 final literal missing")
371}
372fn length(raw: &[u8], pos: &mut usize, initial: usize, limit: usize) -> Result<usize> {
373    let mut value = initial;
374    if initial == 15 {
375        loop {
376            let next = *raw
377                .get(*pos)
378                .ok_or_else(|| anyhow::anyhow!("App LZ4 length truncated"))?;
379            *pos += 1;
380            value = value
381                .checked_add(usize::from(next))
382                .ok_or_else(|| anyhow::anyhow!("App LZ4 length overflow"))?;
383            ensure!(value <= limit, "App LZ4 length exceeds bound");
384            if next != 255 {
385                break;
386            }
387        }
388    }
389    Ok(value)
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    #[test]
396    fn literal_and_overlapping_lz4_vectors() {
397        assert_eq!(lz4_decode(&[0x30, b'a', b'b', b'c'], 3).unwrap(), b"abc");
398        assert_eq!(lz4_decode(&[0x10, b'a', 1, 0, 0], 5).unwrap(), b"aaaaa");
399        for n in [0, 1, 15, 270, 4096] {
400            let v = vec![42; n];
401            assert_eq!(lz4_decode(&lz4_literal(&v).unwrap(), n).unwrap(), v);
402        }
403        assert!(lz4_decode(&[0x10, b'a', 0, 0], 20).is_err());
404        assert!(lz4_decode(&[0xf0, 255], 16).is_err());
405    }
406    #[test]
407    fn task_frame_full_width_and_corruption() {
408        let task = TaskFrame {
409            command: 3361,
410            integers: BTreeMap::from([(2, u64::MAX), (22, u64::MAX)]),
411            strings: BTreeMap::from([(24, b"wechat".to_vec())]),
412            payload: vec![1, 2, 3],
413        };
414        let raw = task.encode().unwrap();
415        let decoded = TaskFrame::decode(&raw, 3361).unwrap();
416        assert_eq!(decoded.integers[&2], u64::MAX);
417        assert_eq!(decoded.payload, vec![1, 2, 3]);
418        assert!(TaskFrame::decode(&raw, 3380).is_err());
419        let mut corrupt = raw;
420        corrupt[0] = 1;
421        assert!(TaskFrame::decode(&corrupt, 3361).is_err());
422        let mut noncanonical = vec![0, 0, 0, 21, 0x11, 0x10, 7, 0x6d, 0, 0, 13, 33, 0, 0, 0, 0];
423        noncanonical.extend([0x81, 0, 0, 0, 4]);
424        assert!(TaskFrame::decode(&noncanonical, 3361).is_err());
425    }
426}