Skip to main content

ferogram_connect/
transport_obfuscated.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
13pub use ferogram_crypto::ObfuscatedCipher;
14
15use crate::ConnectError;
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17use tokio::net::TcpStream;
18
19/// Framing mode for `ObfuscatedStream`.
20///
21/// * `Abridged` - Obfuscated2 over Abridged framing (`0xEFEFEFEF` nonce tag).
22///   Used for plain and `0x??` MTProxy secrets.
23/// * `PaddedIntermediate` - Obfuscated2 over Padded Intermediate framing
24///   (`0xDDDDDDDD` nonce tag).  Required for `0xDD` MTProxy secrets.
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ObfuscatedFraming {
27    Abridged,
28    PaddedIntermediate,
29}
30
31pub struct ObfuscatedStream {
32    stream: TcpStream,
33    cipher: ObfuscatedCipher,
34    framing: ObfuscatedFraming,
35}
36
37impl ObfuscatedStream {
38    /// Connect using Abridged framing (plain MTProxy secret, no 0xDD prefix).
39    pub async fn connect(
40        addr: &str,
41        proxy_secret: Option<&[u8; 16]>,
42        dc_id: i16,
43    ) -> Result<Self, ConnectError> {
44        let stream = TcpStream::connect(addr).await?;
45        Self::handshake(stream, proxy_secret, dc_id, ObfuscatedFraming::Abridged).await
46    }
47
48    /// Connect using Padded Intermediate framing (0xDD MTProxy secret).
49    pub async fn connect_padded(
50        addr: &str,
51        proxy_secret: Option<&[u8; 16]>,
52        dc_id: i16,
53    ) -> Result<Self, ConnectError> {
54        let stream = TcpStream::connect(addr).await?;
55        Self::handshake(
56            stream,
57            proxy_secret,
58            dc_id,
59            ObfuscatedFraming::PaddedIntermediate,
60        )
61        .await
62    }
63
64    async fn handshake(
65        mut stream: TcpStream,
66        proxy_secret: Option<&[u8; 16]>,
67        dc_id: i16,
68        framing: ObfuscatedFraming,
69    ) -> Result<Self, ConnectError> {
70        let framing_byte = match framing {
71            ObfuscatedFraming::Abridged => 0xef,
72            ObfuscatedFraming::PaddedIntermediate => 0xdd,
73        };
74        let secret = proxy_secret.map(|s| s.as_ref());
75        let (nonce, cipher) = ferogram_crypto::build_obfuscated_init(framing_byte, dc_id, secret);
76        stream.write_all(&nonce).await?;
77        Ok(Self {
78            stream,
79            cipher,
80            framing,
81        })
82    }
83
84    /// Encrypt and send one frame using whichever [`ObfuscatedFraming`] this
85    /// stream was set up with.
86    pub async fn send(&mut self, data: &[u8]) -> Result<(), ConnectError> {
87        match self.framing {
88            ObfuscatedFraming::Abridged => {
89                debug_assert_eq!(
90                    data.len() % 4,
91                    0,
92                    "obfuscated send: payload must be 4-byte aligned"
93                );
94                let words = data.len() / 4;
95                let mut frame = if words < 0x7f {
96                    let mut v = Vec::with_capacity(1 + data.len());
97                    v.push(words as u8);
98                    v
99                } else {
100                    let mut v = Vec::with_capacity(4 + data.len());
101                    v.extend_from_slice(&[
102                        0x7f,
103                        (words & 0xff) as u8,
104                        ((words >> 8) & 0xff) as u8,
105                        ((words >> 16) & 0xff) as u8,
106                    ]);
107                    v
108                };
109                frame.extend_from_slice(data);
110                self.cipher.encrypt(&mut frame);
111                self.stream.write_all(&frame).await?;
112            }
113            ObfuscatedFraming::PaddedIntermediate => {
114                // Padded intermediate framing: 4-byte LE length of
115                // (payload + random 0-15 padding), then payload, then padding.
116                let mut pad_len_buf = [0u8; 1];
117                ferogram_crypto::fill_random(&mut pad_len_buf);
118                let pad_len = (pad_len_buf[0] & 0x0f) as usize;
119                let total_payload = data.len() + pad_len;
120                let mut frame = Vec::with_capacity(4 + total_payload);
121                frame.extend_from_slice(&(total_payload as u32).to_le_bytes());
122                frame.extend_from_slice(data);
123                let mut pad = vec![0u8; pad_len];
124                ferogram_crypto::fill_random(&mut pad);
125                frame.extend_from_slice(&pad);
126                self.cipher.encrypt(&mut frame);
127                self.stream.write_all(&frame).await?;
128            }
129        }
130        Ok(())
131    }
132
133    /// Read and decrypt one frame. A header word count above `0x7f` (in
134    /// Abridged framing) means the server sent a 4-byte transport error
135    /// code instead of a real frame; that's surfaced as an `Io` error rather
136    /// than parsed as a frame length.
137    pub async fn recv(&mut self) -> Result<Vec<u8>, ConnectError> {
138        match self.framing {
139            ObfuscatedFraming::Abridged => {
140                let mut h = [0u8; 1];
141                self.stream.read_exact(&mut h).await?;
142                self.cipher.decrypt(&mut h);
143
144                let words = if h[0] < 0x7f {
145                    h[0] as usize
146                } else if h[0] == 0x7f {
147                    let mut b = [0u8; 3];
148                    self.stream.read_exact(&mut b).await?;
149                    self.cipher.decrypt(&mut b);
150                    b[0] as usize | (b[1] as usize) << 8 | (b[2] as usize) << 16
151                } else {
152                    let mut rest = [0u8; 3];
153                    self.stream.read_exact(&mut rest).await?;
154                    self.cipher.decrypt(&mut rest);
155                    let code = i32::from_le_bytes([h[0], rest[0], rest[1], rest[2]]);
156                    return Err(ConnectError::Io(std::io::Error::new(
157                        std::io::ErrorKind::ConnectionRefused,
158                        format!("transport error from server: {code}"),
159                    )));
160                };
161
162                let mut buf = vec![0u8; words * 4];
163                self.stream.read_exact(&mut buf).await?;
164                self.cipher.decrypt(&mut buf);
165
166                if buf.len() == 4 {
167                    let code = i32::from_le_bytes(buf[..4].try_into().unwrap());
168                    if code < 0 {
169                        return Err(ConnectError::Io(std::io::Error::new(
170                            std::io::ErrorKind::ConnectionRefused,
171                            format!("transport error from server: {code}"),
172                        )));
173                    }
174                }
175
176                Ok(buf)
177            }
178            ObfuscatedFraming::PaddedIntermediate => {
179                let mut len_buf = [0u8; 4];
180                self.stream.read_exact(&mut len_buf).await?;
181                self.cipher.decrypt(&mut len_buf);
182                let total_len = i32::from_le_bytes(len_buf);
183                if total_len < 0 {
184                    return Err(ConnectError::Io(std::io::Error::new(
185                        std::io::ErrorKind::ConnectionRefused,
186                        format!("transport error from server: {total_len}"),
187                    )));
188                }
189                let mut buf = vec![0u8; total_len as usize];
190                self.stream.read_exact(&mut buf).await?;
191                self.cipher.decrypt(&mut buf);
192                if buf.len() >= 24 {
193                    let pad = (buf.len() - 24) % 16;
194                    buf.truncate(buf.len() - pad);
195                }
196                Ok(buf)
197            }
198        }
199    }
200}