Skip to main content

ferogram_connect/
pfs.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2//
3// ferogram: async Telegram MTProto client in Rust
4// https://github.com/ankit-chaubey/ferogram
5//
6// Licensed under either the MIT License or the Apache License 2.0.
7// See the LICENSE-MIT or LICENSE-APACHE file in this repository:
8// https://github.com/ankit-chaubey/ferogram
9//
10// Feel free to use, modify, and share this code.
11// Please keep this notice when redistributing.
12
13//! PFS (Perfect Forward Secrecy) bind response decoder.
14//!
15//! Pure byte-slice logic; no `tl-api` dependency.
16
17/// Decode one bare MTProto message body for the auth.bindTempAuthKey response.
18///
19/// Returns `Ok(())` if this message body contains boolTrue (success).
20/// Returns `Err("skip")` for informational messages the caller should ignore
21/// (new_session_created, future_salts, msgs_ack, pong, etc.).
22/// Returns `Err(msg)` for real errors.
23pub fn decode_bind_single(body: &[u8]) -> Result<(), String> {
24    const RPC_RESULT: u32 = 0xf35c6d01;
25    const BOOL_TRUE: u32 = 0x9972_75b5;
26    const BOOL_FALSE: u32 = 0xbc79_9737;
27    const RPC_ERROR: u32 = 0x2144_ca19;
28    const BAD_MSG: u32 = 0xa7ef_f811;
29    const BAD_SALT: u32 = 0xedab_447b;
30    const NEW_SESSION: u32 = 0x9ec2_0908;
31    const FUTURE_SALTS: u32 = 0xae50_0895;
32    const MSGS_ACK: u32 = 0x62d6_b459;
33    const PONG: u32 = 0x0347_73c5;
34
35    if body.len() < 4 {
36        return Err("skip".to_string());
37    }
38    let ctor = u32::from_le_bytes(body[..4].try_into().unwrap());
39
40    match ctor {
41        BOOL_TRUE => Ok(()),
42
43        BOOL_FALSE => Err("server returned boolFalse (binding rejected)".to_string()),
44
45        NEW_SESSION | FUTURE_SALTS | MSGS_ACK | PONG => Err("skip".to_string()),
46
47        RPC_RESULT if body.len() >= 16 => {
48            let inner = u32::from_le_bytes(body[12..16].try_into().unwrap());
49            match inner {
50                BOOL_TRUE => Ok(()),
51                BOOL_FALSE => Err("rpc_result{boolFalse} (server rejected binding)".to_string()),
52                RPC_ERROR if body.len() >= 20 => {
53                    let code = i32::from_le_bytes(body[16..20].try_into().unwrap());
54                    let msg = crate::util::tl_read_string(body.get(20..).unwrap_or(&[]))
55                        .unwrap_or_default();
56                    Err(format!("rpc_error code={code} message={msg:?}"))
57                }
58                _ => Err(format!("rpc_result inner ctor={inner:#010x}")),
59            }
60        }
61
62        BAD_MSG if body.len() >= 16 => {
63            let code = u32::from_le_bytes(body[12..16].try_into().unwrap());
64            let desc = match code {
65                16 => "msg_id too low (clock skew)",
66                17 => "msg_id too high (clock skew)",
67                18 => "incorrect lower 2 bits of msg_id",
68                19 => "duplicate msg_id",
69                20 => "message too old (>300s)",
70                32 => "msg_seqno too low",
71                33 => "msg_seqno too high",
72                34 => "even seqno expected, odd received",
73                35 => "odd seqno expected, even received",
74                48 => "incorrect server salt",
75                64 => "invalid container",
76                _ => "unknown code",
77            };
78            Err(format!("bad_msg_notification code={code} ({desc})"))
79        }
80
81        BAD_SALT if body.len() >= 24 => {
82            let new_salt = i64::from_le_bytes(body[16..24].try_into().unwrap());
83            Err(format!(
84                "bad_server_salt, server wants salt={new_salt:#018x}"
85            ))
86        }
87
88        _ => Err(format!("unknown ctor={ctor:#010x}")),
89    }
90}
91
92/// Decode the server response to auth.bindTempAuthKey.
93///
94/// Handles bare messages AND msg_container (the server frequently bundles
95/// new_session_created + rpc_result together in a container on the very first
96/// encrypted message of a fresh temp session).
97pub fn decode_bind_response(body: &[u8]) -> Result<(), String> {
98    const MSG_CONTAINER: u32 = 0x73f1f8dc;
99
100    if body.len() < 4 {
101        return Err(format!("response body too short ({} bytes)", body.len()));
102    }
103    let ctor = u32::from_le_bytes(body[..4].try_into().unwrap());
104
105    if ctor != MSG_CONTAINER {
106        return decode_bind_single(body).map_err(|e| {
107            if e == "skip" {
108                "__need_more__".to_string()
109            } else {
110                e
111            }
112        });
113    }
114
115    if body.len() < 8 {
116        return Err("msg_container too short to read count".to_string());
117    }
118    let count = u32::from_le_bytes(body[4..8].try_into().unwrap()) as usize;
119    let mut pos = 8usize;
120    let mut last_real_err: Option<String> = None;
121
122    for i in 0..count {
123        if pos + 16 > body.len() {
124            return Err(format!(
125                "msg_container truncated at message {i}/{count} (pos={pos} body_len={})",
126                body.len()
127            ));
128        }
129        let msg_bytes = u32::from_le_bytes(body[pos + 12..pos + 16].try_into().unwrap()) as usize;
130        pos += 16;
131
132        if pos + msg_bytes > body.len() {
133            return Err(format!(
134                "msg_container message {i} body overflows (need {msg_bytes}, have {})",
135                body.len() - pos
136            ));
137        }
138        let msg_body = &body[pos..pos + msg_bytes];
139        pos += msg_bytes;
140
141        match decode_bind_single(msg_body) {
142            Ok(()) => return Ok(()),
143            Err(e) if e == "skip" => continue,
144            Err(e) => {
145                last_real_err = Some(e);
146            }
147        }
148    }
149
150    Err(last_real_err.unwrap_or_else(|| "__need_more__".to_string()))
151}