Skip to main content

ferogram_crypto/
obfuscated.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#[allow(deprecated)]
16use aes::cipher::{Array, KeyIvInit, StreamCipher};
17
18/// AES-256-CTR stream cipher pair for MTProto obfuscated transport.
19pub struct ObfuscatedCipher {
20    #[allow(deprecated)]
21    rx: ctr::Ctr128BE<aes::Aes256>,
22    #[allow(deprecated)]
23    tx: ctr::Ctr128BE<aes::Aes256>,
24}
25
26impl ObfuscatedCipher {
27    /// Build cipher state from the 64-byte random init buffer.
28    #[allow(deprecated)]
29    pub fn new(init: &[u8; 64]) -> Self {
30        let rev: Vec<u8> = init.iter().copied().rev().collect();
31        let rx_key: [u8; 32] = rev[8..40].try_into().expect("32-byte slice");
32        let rx_iv: [u8; 16] = rev[40..56].try_into().expect("16-byte slice");
33        let tx_key: [u8; 32] = init[8..40].try_into().expect("32-byte slice");
34        let tx_iv: [u8; 16] = init[40..56].try_into().expect("16-byte slice");
35        Self {
36            rx: ctr::Ctr128BE::<aes::Aes256>::new(&Array::from(rx_key), &Array::from(rx_iv)),
37            tx: ctr::Ctr128BE::<aes::Aes256>::new(&Array::from(tx_key), &Array::from(tx_iv)),
38        }
39    }
40
41    /// Build cipher from explicit key/IV pairs (used when MTProxy secret
42    /// mixing has already been applied externally via SHA-256).
43    #[allow(deprecated)]
44    pub fn from_keys(
45        tx_key: &[u8; 32],
46        tx_iv: &[u8; 16],
47        rx_key: &[u8; 32],
48        rx_iv: &[u8; 16],
49    ) -> Self {
50        Self {
51            tx: ctr::Ctr128BE::<aes::Aes256>::new(&Array::from(*tx_key), &Array::from(*tx_iv)),
52            rx: ctr::Ctr128BE::<aes::Aes256>::new(&Array::from(*rx_key), &Array::from(*rx_iv)),
53        }
54    }
55
56    /// Encrypt outgoing bytes in-place (TX direction).
57    pub fn encrypt(&mut self, buf: &mut [u8]) {
58        self.tx.apply_keystream(buf);
59    }
60
61    /// Decrypt incoming bytes in-place (RX direction).
62    pub fn decrypt(&mut self, buf: &mut [u8]) {
63        self.rx.apply_keystream(buf);
64    }
65}
66
67/// Generate the 64-byte obfuscated init buffer and build the cipher for it.
68///
69/// `framing_byte`: 0xef = Abridged, 0xdd = PaddedIntermediate.
70/// `proxy_secret`: if present, SHA-256 mixes the key with the secret (MTProxy).
71///
72/// Returns `(nonce, cipher)`. The caller writes `nonce` to the stream; the
73/// cipher is used for all subsequent I/O on that connection.
74pub fn build_obfuscated_init(
75    framing_byte: u8,
76    dc_id: i16,
77    proxy_secret: Option<&[u8]>,
78) -> ([u8; 64], ObfuscatedCipher) {
79    use sha2::Digest;
80
81    let mut nonce = [0u8; 64];
82    loop {
83        crate::fill_random(&mut nonce);
84        let first = u32::from_le_bytes(nonce[0..4].try_into().expect("4-byte slice"));
85        let second = u32::from_le_bytes(nonce[4..8].try_into().expect("4-byte slice"));
86        let bad = nonce[0] == 0xEF
87            || first == 0x44414548 // HEAD
88            || first == 0x54534F50 // POST
89            || first == 0x20544547 // GET
90            || first == 0x4954504f // OPTIONS
91            || first == 0xEEEEEEEE
92            || first == 0xDDDDDDDD
93            || first == 0x02010316
94            || second == 0x00000000;
95        if !bad {
96            break;
97        }
98    }
99
100    let tx_raw: [u8; 32] = nonce[8..40].try_into().expect("32-byte slice");
101    let tx_iv: [u8; 16] = nonce[40..56].try_into().expect("16-byte slice");
102    let mut rev48 = nonce[8..56].to_vec();
103    rev48.reverse();
104    let rx_raw: [u8; 32] = rev48[0..32].try_into().expect("32-byte slice");
105    let rx_iv: [u8; 16] = rev48[32..48].try_into().expect("16-byte slice");
106
107    let (tx_key, rx_key): ([u8; 32], [u8; 32]) = if let Some(s) = proxy_secret {
108        let mut h = sha2::Sha256::new();
109        h.update(tx_raw);
110        h.update(s);
111        let tx: [u8; 32] = h.finalize().into();
112
113        let mut h = sha2::Sha256::new();
114        h.update(rx_raw);
115        h.update(s);
116        let rx: [u8; 32] = h.finalize().into();
117        (tx, rx)
118    } else {
119        (tx_raw, rx_raw)
120    };
121
122    nonce[56] = framing_byte;
123    nonce[57] = framing_byte;
124    nonce[58] = framing_byte;
125    nonce[59] = framing_byte;
126    let dc_bytes = dc_id.to_le_bytes();
127    nonce[60] = dc_bytes[0];
128    nonce[61] = dc_bytes[1];
129
130    let mut cipher = ObfuscatedCipher::from_keys(&tx_key, &tx_iv, &rx_key, &rx_iv);
131    let mut skip = [0u8; 56];
132    cipher.encrypt(&mut skip);
133    cipher.encrypt(&mut nonce[56..64]);
134
135    (nonce, cipher)
136}
137
138/// Compute the FakeTLS ClientHello random-field value.
139///
140/// `secret` is the 16-byte MTProxy key. `record` must be the fully assembled
141/// ClientHello TLS record with the 32-byte random field zeroed.
142///
143/// Returns `HMAC-SHA256(secret, record)` with its last 4 bytes XORed with the
144/// current unix timestamp (little-endian) -- the anti-replay scheme real
145/// MTProxy FakeTLS servers check for. The caller writes this value into the
146/// record's random field, then sends the record as-is.
147pub fn fake_tls_client_digest(secret: &[u8; 16], record: &[u8]) -> [u8; 32] {
148    use hmac::{Hmac, KeyInit, Mac};
149    type HmacSha256 = Hmac<sha2::Sha256>;
150
151    let mut mac =
152        HmacSha256::new_from_slice(secret).expect("HMAC key error: secret must be non-empty");
153    mac.update(record);
154    let mut digest: [u8; 32] = mac.finalize().into_bytes().into();
155
156    let now = std::time::SystemTime::now()
157        .duration_since(std::time::UNIX_EPOCH)
158        .map(|d| d.as_secs() as u32)
159        .unwrap_or(0);
160    let last4 = u32::from_le_bytes(digest[28..32].try_into().expect("4-byte slice")) ^ now;
161    digest[28..32].copy_from_slice(&last4.to_le_bytes());
162    digest
163}
164
165/// Verify a FakeTLS ServerHello digest.
166///
167/// `packet_with_digest_zeroed` must be the concatenated ServerHello +
168/// ChangeCipherSpec + first Application-Data ("cert") record bytes, exactly
169/// as read off the wire, with the server's own 32-byte digest field zeroed
170/// in place. `client_digest` is the raw value this client sent on the wire
171/// in its own ClientHello. `expected_digest` is what the server actually
172/// sent (extracted before zeroing). Returns `true` if the server proves
173/// knowledge of the secret.
174pub fn fake_tls_verify_server_digest(
175    secret: &[u8; 16],
176    client_digest: &[u8; 32],
177    packet_with_digest_zeroed: &[u8],
178    expected_digest: &[u8; 32],
179) -> bool {
180    use hmac::{Hmac, KeyInit, Mac};
181    type HmacSha256 = Hmac<sha2::Sha256>;
182
183    let mut mac =
184        HmacSha256::new_from_slice(secret).expect("HMAC key error: secret must be non-empty");
185    mac.update(client_digest);
186    mac.update(packet_with_digest_zeroed);
187    let computed: [u8; 32] = mac.finalize().into_bytes().into();
188    computed == *expected_digest
189}