Skip to main content

wechat_protocol/calls/
app_session.rs

1//! Generic call-app authentication messages and the negotiated unencrypted app
2//! session. Host MMTLS authentication remains required. No socket or credential
3//! persistence is performed here; app encryption is distinct from media AEAD.
4
5use super::signaling::CommonResponse;
6use crate::protobuf::{self as p, Field, Value};
7use anyhow::{bail, ensure, Result};
8
9pub const AUTH_COMMAND: u16 = 7994;
10pub const AUTH_PATH: &str = "/cgi-bin/micromsg-bin/mm_ilink_cloud_app_auth";
11const MAX_AUTH: usize = 64 * 1024;
12
13/// Accepted account/logout binding chosen by the account owner, not values
14/// recovered from an unauthenticated server message. It is not a session key.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct AppAuthority {
17    pub account_generation: u64,
18    pub logout_epoch: u64,
19    pub credential_digest: [u8; 32],
20}
21
22impl AppAuthority {
23    fn validate(self) -> Result<()> {
24        ensure!(
25            self.account_generation != 0 && self.credential_digest != [0; 32],
26            "Inactive app account authority"
27        );
28        Ok(())
29    }
30}
31
32/// One first-auth request. Consuming completion prevents accidental reuse after
33/// success, malformed data or rejection. The host must retain uncertain intent
34/// outcomes separately instead of automatically preparing another attempt.
35pub struct PendingAppAuth {
36    authority: AppAuthority,
37    own_device: [u8; 32],
38}
39
40impl PendingAppAuth {
41    /// Device identity is a fresh, independent 32-byte value supplied by a secure
42    /// host RNG. It is not the account's ordinary host device ID.
43    pub fn new(authority: AppAuthority, own_device: [u8; 32]) -> Result<Self> {
44        authority.validate()?;
45        ensure!(own_device != [0; 32], "Missing app device identity");
46        Ok(Self {
47            authority,
48            own_device,
49        })
50    }
51
52    /// Inner CGI7994 body, to place in the host CommonRequest's field2.
53    pub fn request_body(&self) -> Result<Vec<u8>> {
54        p::message(&[p::bytes(1, &p::bytes(1, &self.own_device))])
55    }
56
57    /// The input must be the corresponding host-authenticated CommonResponse.
58    /// This selected implementation supports the explicit no-app-encryption
59    /// branch only. It never falls back from failed encrypted processing.
60    pub fn complete(
61        self,
62        authenticated_common: &[u8],
63        current: AppAuthority,
64    ) -> Result<AppSession> {
65        ensure!(current == self.authority, "App account authority changed");
66        ensure!(
67            authenticated_common.len() <= MAX_AUTH,
68            "App auth response too large"
69        );
70        let common = CommonResponse::decode(authenticated_common)?;
71        ensure!(
72            common.return_code()? == 0,
73            "App auth host operation rejected"
74        );
75        let inner = decode(
76            common
77                .body
78                .as_deref()
79                .ok_or_else(|| anyhow::anyhow!("Missing app auth body"))?,
80        )?;
81        let base = decode(data(&inner, 1)?)?;
82        ensure!(
83            signed(&base, 1, None)? == 0,
84            "App auth base operation rejected"
85        );
86        let unify = decode(data(&inner, 2)?)?;
87        ensure!(
88            signed(&unify, 4, Some(0))? == 0,
89            "App auth detail operation rejected"
90        );
91        let auth = decode(data(&unify, 2)?)?;
92        let account = decode(data(&unify, 3)?)?;
93        let flag = scalar(&auth, 11, Some(0))?;
94        ensure!(
95            u32::try_from(flag)? & 1 != 0,
96            "Unsupported app encryption negotiation"
97        );
98        for number in [1, 2, 4] {
99            ensure!(
100                p::optional_data(&auth, number)?.is_none_or(|v| v.is_empty()),
101                "Contradictory app encryption keys"
102            );
103        }
104        let app_uin = scalar(&account, 1, None)?;
105        ensure!(app_uin != 0, "Missing app identity");
106        let server_id = p::optional_data(&auth, 9)?.unwrap_or_default();
107        ensure!(server_id.len() <= 1024, "App server identity too large");
108        Ok(AppSession {
109            authority: self.authority,
110            own_device: self.own_device,
111            app_uin,
112            server_id: server_id.to_vec(),
113            auth_flags: flag as u32,
114        })
115    }
116}
117
118/// Server-selected app identity. No app keys exist in this branch. The server
119/// does not install the encrypted branch's default expiry. Host account/logout
120/// authority still fences every use; hosts may impose a separate local deadline.
121/// Private app identities intentionally do not implement Debug or Serialize.
122pub struct AppSession {
123    authority: AppAuthority,
124    own_device: [u8; 32],
125    app_uin: u64,
126    server_id: Vec<u8>,
127    auth_flags: u32,
128}
129
130impl AppSession {
131    pub fn validate_authority(&self, current: AppAuthority) -> Result<()> {
132        ensure!(self.authority == current, "App account authority changed");
133        Ok(())
134    }
135
136    pub fn app_uin(&self) -> u64 {
137        self.app_uin
138    }
139    pub fn server_id(&self) -> &[u8] {
140        &self.server_id
141    }
142    pub fn own_device(&self) -> &[u8; 32] {
143        &self.own_device
144    }
145    pub fn auth_flags(&self) -> u32 {
146        self.auth_flags
147    }
148
149    /// Source CommonSession fields4,5,6. Account UIN and app UIN are different
150    /// namespaces; never use the host account's UIN here.
151    pub fn common_session(&self, current: AppAuthority) -> Result<Vec<u8>> {
152        self.validate_authority(current)?;
153        p::message(&[
154            p::uint(4, self.app_uin),
155            p::bytes(5, &self.server_id),
156            p::uint(6, 1),
157        ])
158    }
159}
160
161fn decode(input: &[u8]) -> Result<Vec<Field>> {
162    ensure!(input.len() <= MAX_AUTH, "App auth message too large");
163    super::signaling::decode_fields(input)
164}
165fn data(fields: &[Field], number: u32) -> Result<&[u8]> {
166    p::optional_data(fields, number)?
167        .ok_or_else(|| anyhow::anyhow!("Missing app auth message {number}"))
168}
169fn scalar(fields: &[Field], number: u32, default: Option<u64>) -> Result<u64> {
170    match p::optional(fields, number)? {
171        Some(Value::Varint(value)) => Ok(*value),
172        None => default.ok_or_else(|| anyhow::anyhow!("Missing app auth integer {number}")),
173        _ => bail!("Invalid app auth integer {number}"),
174    }
175}
176fn signed(fields: &[Field], number: u32, default: Option<i32>) -> Result<i32> {
177    Ok(i32::try_from(
178        scalar(fields, number, default.map(|v| v as i64 as u64))? as i64,
179    )?)
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    fn authority() -> AppAuthority {
186        AppAuthority {
187            account_generation: 1,
188            logout_epoch: 0,
189            credential_digest: [9; 32],
190        }
191    }
192    fn reply(auth: &[u8], app_uin: u64) -> Vec<u8> {
193        let inner = p::message(&[
194            p::bytes(1, &[8, 0]),
195            p::bytes(
196                2,
197                &p::message(&[p::bytes(2, auth), p::bytes(3, &p::uint(1, app_uin))]).unwrap(),
198            ),
199        ])
200        .unwrap();
201        p::message(&[p::bytes(1, &[8, 0]), p::bytes(2, &inner)]).unwrap()
202    }
203    #[test]
204    fn encrypted_or_contradictory_branch_is_not_plain_fallback() {
205        for auth in [vec![88, 0], vec![10, 1, 4, 88, 1], vec![88, 1, 88, 1]] {
206            let pending = PendingAppAuth::new(authority(), [7; 32]).unwrap();
207            assert!(pending.complete(&reply(&auth, 7), authority()).is_err());
208        }
209    }
210    #[test]
211    fn account_or_logout_change_invalidates_completion_and_session_use() {
212        let pending = PendingAppAuth::new(authority(), [7; 32]).unwrap();
213        let mut changed = authority();
214        changed.logout_epoch = 1;
215        assert!(pending.complete(&reply(&[88, 1], 7), changed).is_err());
216        let pending = PendingAppAuth::new(authority(), [7; 32]).unwrap();
217        let result = pending.complete(&reply(&[88, 1], 7), authority()).unwrap();
218        assert!(result.common_session(changed).is_err());
219    }
220}