Skip to main content

ferogram_connect/
pfs.rs

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