1use 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#[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
32pub struct PendingAppAuth {
36 authority: AppAuthority,
37 own_device: [u8; 32],
38}
39
40impl PendingAppAuth {
41 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 pub fn request_body(&self) -> Result<Vec<u8>> {
54 p::message(&[p::bytes(1, &p::bytes(1, &self.own_device))])
55 }
56
57 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
118pub 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 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}