Skip to main content

ferogram_crypto/
lib.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#![cfg_attr(docsrs, feature(doc_cfg))]
14#![doc(html_root_url = "https://docs.rs/ferogram-crypto/0.3.7")]
15//! Cryptographic primitives for Telegram MTProto 2.0.
16//!
17//! This crate is part of [ferogram](https://crates.io/crates/ferogram), an async Rust
18//! MTProto client built by [Ankit Chaubey](https://github.com/ankit-chaubey).
19//!
20//! - Channel: [t.me/Ferogram](https://t.me/Ferogram)
21//! - Chat: [t.me/FerogramChat](https://t.me/FerogramChat)
22//!
23//! Most users do not need this crate directly. The `ferogram` crate wraps
24//! everything. Use `ferogram-crypto` only if you are building your own MTProto
25//! transport layer or need direct access to the primitives.
26//!
27//! # What's in here
28//!
29//! - **AES-256-IGE**: MTProto's symmetric cipher. [`aes::ige_encrypt`] and
30//!   [`aes::ige_decrypt`] operate on 16-byte-aligned buffers.
31//! - **SHA-1 / SHA-256**: Hash macros used throughout key derivation and
32//!   message authentication.
33//! - **Pollard-rho PQ factorization**: Required by the DH handshake:
34//!   Telegram sends a 64-bit semiprime and expects you to factor it.
35//!   [`factorize`] does this.
36//! - **RSA (MTProto RSA-PAD)**: Used during the initial key exchange to
37//!   encrypt the inner request to Telegram's known public keys.
38//!   See [`rsa`].
39//! - **`AuthKey`**: The 256-byte session key derived after a successful DH
40//!   exchange. Wraps the raw bytes and exposes the auxiliary hash needed for
41//!   MTProto 2.0 message encryption.
42//! - **MTProto 2.0 encrypt / decrypt**: [`encrypt_data_v2`] and
43//!   [`decrypt_data_v2`] implement the full AES-IGE + SHA-256 message
44//!   protection scheme from the spec.
45//! - **DH nonce-to-key derivation**: Derives `auth_key` from the DH result
46//!   bytes using the MTProto KDF.
47//! - **Obfuscated transport**: [`ObfuscatedCipher`] implements the random-padding
48//!   + AES-CTR obfuscation layer used by `ObfuscatedAbridged` transport.
49//!
50//! # Example: AES-IGE round-trip
51//!
52//! ```rust
53//! use ferogram_crypto::aes::{ige_encrypt, ige_decrypt};
54//!
55//! let key = [0u8; 32];
56//! let iv  = [0u8; 32];
57//! let mut data = vec![0u8; 48]; // must be 16-byte aligned
58//!
59//! ige_encrypt(&mut data, &key, &iv);
60//! ige_decrypt(&mut data, &key, &iv);
61//! // data is back to zeros
62//! ```
63//!
64//! # Example: factorize
65//!
66//! ```rust
67//! use ferogram_crypto::factorize;
68//!
69//! let (p, q) = factorize(0x17ED48941A08F981);
70//! assert!(p < q);
71//! assert_eq!(p * q, 0x17ED48941A08F981);
72//! ```
73
74#![deny(unsafe_code)]
75
76pub mod aes;
77mod auth_key;
78mod deque_buffer;
79mod factorize;
80mod obfuscated;
81pub mod rsa;
82mod sha;
83
84pub use auth_key::AuthKey;
85pub use deque_buffer::DequeBuffer;
86pub use factorize::factorize;
87pub use obfuscated::ObfuscatedCipher;
88
89/// Errors from [`decrypt_data_v2`].
90#[derive(Clone, Debug, PartialEq)]
91pub enum DecryptError {
92    /// Ciphertext too short or not block-aligned.
93    InvalidBuffer,
94    /// The `auth_key_id` in the ciphertext does not match our key.
95    AuthKeyMismatch,
96    /// The `msg_key` in the ciphertext does not match our computed value.
97    MessageKeyMismatch,
98}
99
100impl std::fmt::Display for DecryptError {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            Self::InvalidBuffer => write!(f, "invalid ciphertext buffer length"),
104            Self::AuthKeyMismatch => write!(f, "auth_key_id mismatch"),
105            Self::MessageKeyMismatch => write!(f, "msg_key mismatch"),
106        }
107    }
108}
109impl std::error::Error for DecryptError {}
110
111enum Side {
112    Client,
113    Server,
114}
115impl Side {
116    fn x(&self) -> usize {
117        match self {
118            Side::Client => 0,
119            Side::Server => 8,
120        }
121    }
122}
123
124fn calc_key(auth_key: &AuthKey, msg_key: &[u8; 16], side: Side) -> ([u8; 32], [u8; 32]) {
125    let x = side.x();
126    let sha_a = sha256!(msg_key, &auth_key.data[x..x + 36]);
127    let sha_b = sha256!(&auth_key.data[40 + x..40 + x + 36], msg_key);
128
129    let mut aes_key = [0u8; 32];
130    aes_key[..8].copy_from_slice(&sha_a[..8]);
131    aes_key[8..24].copy_from_slice(&sha_b[8..24]);
132    aes_key[24..].copy_from_slice(&sha_a[24..]);
133
134    let mut aes_iv = [0u8; 32];
135    aes_iv[..8].copy_from_slice(&sha_b[..8]);
136    aes_iv[8..24].copy_from_slice(&sha_a[8..24]);
137    aes_iv[24..].copy_from_slice(&sha_b[24..]);
138
139    (aes_key, aes_iv)
140}
141
142fn padding_len(len: usize) -> usize {
143    // MTProto 2.0 requires 12–1024 bytes of random padding, and the total
144    // (payload + padding) must be a multiple of 16.
145    // Minimum padding = 12; extra bytes to hit the next 16-byte boundary.
146    let rem = (len + 12) % 16;
147    if rem == 0 { 12 } else { 12 + (16 - rem) }
148}
149
150/// Encrypt `buffer` (in-place, with prepended header) using MTProto 2.0.
151///
152/// After this call `buffer` contains `key_id || msg_key || ciphertext`.
153pub fn encrypt_data_v2(buffer: &mut DequeBuffer, auth_key: &AuthKey) {
154    let mut rnd = [0u8; 32];
155    getrandom::getrandom(&mut rnd).expect("getrandom failed");
156    do_encrypt_data_v2(buffer, auth_key, &rnd);
157}
158
159pub(crate) fn do_encrypt_data_v2(buffer: &mut DequeBuffer, auth_key: &AuthKey, rnd: &[u8; 32]) {
160    let pad = padding_len(buffer.len());
161    buffer.extend(rnd.iter().take(pad).copied());
162
163    let x = Side::Client.x();
164    let msg_key_large = sha256!(&auth_key.data[88 + x..88 + x + 32], buffer.as_ref());
165    let mut msg_key = [0u8; 16];
166    msg_key.copy_from_slice(&msg_key_large[8..24]);
167
168    let (key, iv) = calc_key(auth_key, &msg_key, Side::Client);
169    aes::ige_encrypt(buffer.as_mut(), &key, &iv);
170
171    buffer.extend_front(&msg_key);
172    buffer.extend_front(&auth_key.key_id);
173}
174
175/// Decrypt an MTProto 2.0 ciphertext.
176///
177/// `buffer` must start with `key_id || msg_key || ciphertext`.
178/// On success returns a slice of `buffer` containing the plaintext.
179pub fn decrypt_data_v2<'a>(
180    buffer: &'a mut [u8],
181    auth_key: &AuthKey,
182) -> Result<&'a mut [u8], DecryptError> {
183    if buffer.len() < 24 || !(buffer.len() - 24).is_multiple_of(16) {
184        return Err(DecryptError::InvalidBuffer);
185    }
186    if auth_key.key_id != buffer[..8] {
187        return Err(DecryptError::AuthKeyMismatch);
188    }
189    let mut msg_key = [0u8; 16];
190    msg_key.copy_from_slice(&buffer[8..24]);
191
192    let (key, iv) = calc_key(auth_key, &msg_key, Side::Server);
193    aes::ige_decrypt(&mut buffer[24..], &key, &iv);
194
195    let x = Side::Server.x();
196    let our_key = sha256!(&auth_key.data[88 + x..88 + x + 32], &buffer[24..]);
197    if msg_key != our_key[8..24] {
198        return Err(DecryptError::MessageKeyMismatch);
199    }
200    Ok(&mut buffer[24..])
201}
202
203/// Derive `(key, iv)` from nonces for decrypting `ServerDhParams.encrypted_answer`.
204pub fn generate_key_data_from_nonce(
205    server_nonce: &[u8; 16],
206    new_nonce: &[u8; 32],
207) -> ([u8; 32], [u8; 32]) {
208    let h1 = sha1!(new_nonce, server_nonce);
209    let h2 = sha1!(server_nonce, new_nonce);
210    let h3 = sha1!(new_nonce, new_nonce);
211
212    let mut key = [0u8; 32];
213    key[..20].copy_from_slice(&h1);
214    key[20..].copy_from_slice(&h2[..12]);
215
216    let mut iv = [0u8; 32];
217    iv[..8].copy_from_slice(&h2[12..]);
218    iv[8..28].copy_from_slice(&h3);
219    iv[28..].copy_from_slice(&new_nonce[..4]);
220
221    (key, iv)
222}
223
224/// Derive the AES key and IV for **MTProto v1** (old-style, SHA-1-based).
225///
226/// Used exclusively for `auth.bindTempAuthKey` encrypted_message, which must
227/// be encrypted with the permanent key using the legacy SHA-1 scheme - NOT the
228/// SHA-256 MTProto 2.0 scheme used for all normal messages.
229pub fn derive_aes_key_iv_v1(auth_key: &[u8; 256], msg_key: &[u8; 16]) -> ([u8; 32], [u8; 32]) {
230    let sha1_a = sha1!(msg_key, &auth_key[0..32]);
231    let sha1_b = sha1!(&auth_key[32..48], msg_key, &auth_key[48..64]);
232    let sha1_c = sha1!(&auth_key[64..96], msg_key);
233    let sha1_d = sha1!(msg_key, &auth_key[96..128]);
234
235    let mut key = [0u8; 32];
236    key[..8].copy_from_slice(&sha1_a[..8]);
237    key[8..20].copy_from_slice(&sha1_b[8..20]);
238    key[20..32].copy_from_slice(&sha1_c[4..16]);
239
240    let mut iv = [0u8; 32];
241    iv[..12].copy_from_slice(&sha1_a[8..20]);
242    iv[12..20].copy_from_slice(&sha1_b[..8]);
243    iv[20..24].copy_from_slice(&sha1_c[16..20]);
244    iv[24..32].copy_from_slice(&sha1_d[..8]);
245
246    (key, iv)
247}
248
249/// Telegram's published 2048-bit safe DH prime (big-endian, 256 bytes).
250///
251/// Source: <https://core.telegram.org/mtproto/auth_key>
252#[rustfmt::skip]
253const TELEGRAM_DH_PRIME: [u8; 256] = [
254    0xC7, 0x1C, 0xAE, 0xB9, 0xC6, 0xB1, 0xC9, 0x04,
255    0x8E, 0x6C, 0x52, 0x2F, 0x70, 0xF1, 0x3F, 0x73,
256    0x98, 0x0D, 0x40, 0x23, 0x8E, 0x3E, 0x21, 0xC1,
257    0x49, 0x34, 0xD0, 0x37, 0x56, 0x3D, 0x93, 0x0F,
258    0x48, 0x19, 0x8A, 0x0A, 0xA7, 0xC1, 0x40, 0x58,
259    0x22, 0x94, 0x93, 0xD2, 0x25, 0x30, 0xF4, 0xDB,
260    0xFA, 0x33, 0x6F, 0x6E, 0x0A, 0xC9, 0x25, 0x13,
261    0x95, 0x43, 0xAE, 0xD4, 0x4C, 0xCE, 0x7C, 0x37,
262    0x20, 0xFD, 0x51, 0xF6, 0x94, 0x58, 0x70, 0x5A,
263    0xC6, 0x8C, 0xD4, 0xFE, 0x6B, 0x6B, 0x13, 0xAB,
264    0xDC, 0x97, 0x46, 0x51, 0x29, 0x69, 0x32, 0x84,
265    0x54, 0xF1, 0x8F, 0xAF, 0x8C, 0x59, 0x5F, 0x64,
266    0x24, 0x77, 0xFE, 0x96, 0xBB, 0x2A, 0x94, 0x1D,
267    0x5B, 0xCD, 0x1D, 0x4A, 0xC8, 0xCC, 0x49, 0x88,
268    0x07, 0x08, 0xFA, 0x9B, 0x37, 0x8E, 0x3C, 0x4F,
269    0x3A, 0x90, 0x60, 0xBE, 0xE6, 0x7C, 0xF9, 0xA4,
270    0xA4, 0xA6, 0x95, 0x81, 0x10, 0x51, 0x90, 0x7E,
271    0x16, 0x27, 0x53, 0xB5, 0x6B, 0x0F, 0x6B, 0x41,
272    0x0D, 0xBA, 0x74, 0xD8, 0xA8, 0x4B, 0x2A, 0x14,
273    0xB3, 0x14, 0x4E, 0x0E, 0xF1, 0x28, 0x47, 0x54,
274    0xFD, 0x17, 0xED, 0x95, 0x0D, 0x59, 0x65, 0xB4,
275    0xB9, 0xDD, 0x46, 0x58, 0x2D, 0xB1, 0x17, 0x8D,
276    0x16, 0x9C, 0x6B, 0xC4, 0x65, 0xB0, 0xD6, 0xFF,
277    0x9C, 0xA3, 0x92, 0x8F, 0xEF, 0x5B, 0x9A, 0xE4,
278    0xE4, 0x18, 0xFC, 0x15, 0xE8, 0x3E, 0xBE, 0xA0,
279    0xF8, 0x7F, 0xA9, 0xFF, 0x5E, 0xED, 0x70, 0x05,
280    0x0D, 0xED, 0x28, 0x49, 0xF4, 0x7B, 0xF9, 0x59,
281    0xD9, 0x56, 0x85, 0x0C, 0xE9, 0x29, 0x85, 0x1F,
282    0x0D, 0x81, 0x15, 0xF6, 0x35, 0xB1, 0x05, 0xEE,
283    0x2E, 0x4E, 0x15, 0xD0, 0x4B, 0x24, 0x54, 0xBF,
284    0x6F, 0x4F, 0xAD, 0xF0, 0x34, 0xB1, 0x04, 0x03,
285    0x11, 0x9C, 0xD8, 0xE3, 0xB9, 0x2F, 0xCC, 0x5B,
286];
287
288/// Errors returned by [`check_p_and_g`].
289#[derive(Clone, Debug, PartialEq, Eq)]
290pub enum DhError {
291    /// `dh_prime` is not exactly 256 bytes (2048 bits).
292    PrimeLengthInvalid,
293    /// The most-significant bit of `dh_prime` is zero, so it is actually
294    /// shorter than 2048 bits.
295    PrimeTooSmall,
296    /// `dh_prime` does not match Telegram's published safe prime.
297    PrimeUnknown,
298    /// `g` is outside the set {2, 3, 4, 5, 6, 7}.
299    GeneratorOutOfRange,
300    /// The modular-residue condition required by `g` and the prime is not
301    /// satisfied (see MTProto spec §4.5).
302    GeneratorInvalid,
303}
304
305impl std::fmt::Display for DhError {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        match self {
308            Self::PrimeLengthInvalid => write!(f, "dh_prime must be exactly 256 bytes"),
309            Self::PrimeTooSmall => write!(f, "dh_prime high bit is clear (< 2048 bits)"),
310            Self::PrimeUnknown => {
311                write!(f, "dh_prime does not match any known Telegram safe prime")
312            }
313            Self::GeneratorOutOfRange => write!(f, "generator g must be 2, 3, 4, 5, 6, or 7"),
314            Self::GeneratorInvalid => write!(
315                f,
316                "g fails the required modular-residue check for this prime"
317            ),
318        }
319    }
320}
321
322impl std::error::Error for DhError {}
323
324/// Compute `big_endian_bytes mod modulus` (all values < 2^64).
325fn prime_residue(bytes: &[u8], modulus: u64) -> u64 {
326    bytes
327        .iter()
328        .fold(0u64, |acc, &b| (acc * 256 + b as u64) % modulus)
329}
330
331/// Validate the Diffie-Hellman prime `p` and generator `g` received from
332/// the Telegram server during MTProto key exchange.
333///
334/// Checks performed (per MTProto spec §4.5):
335///
336/// 1. `dh_prime` is exactly 256 bytes (2048 bits).
337/// 2. The most-significant bit is set: the number is truly 2048 bits.
338/// 3. `dh_prime` matches Telegram's published safe prime exactly.
339/// 4. `g` ∈ {2, 3, 4, 5, 6, 7}.
340/// 5. The residue condition for `g` and the prime holds:
341///
342///    | g | condition           |
343///    |---|---------------------|
344///    | 2 | p mod 8 = 7         |
345///    | 3 | p mod 3 = 2         |
346///    | 4 | always valid        |
347///    | 5 | p mod 5 ∈ {1, 4}    |
348///    | 6 | p mod 24 ∈ {19, 23} |
349///    | 7 | p mod 7 ∈ {3, 5, 6} |
350pub fn check_p_and_g(dh_prime: &[u8], g: u32) -> Result<(), DhError> {
351    // 1. Length
352    if dh_prime.len() != 256 {
353        return Err(DhError::PrimeLengthInvalid);
354    }
355
356    // 2. High bit set
357    if dh_prime[0] & 0x80 == 0 {
358        return Err(DhError::PrimeTooSmall);
359    }
360
361    // 3. Known prime: exact match guarantees the residue conditions below
362    //  are deterministic constants, so check 5 is redundant after this.
363    if dh_prime != &TELEGRAM_DH_PRIME[..] {
364        return Err(DhError::PrimeUnknown);
365    }
366
367    // 4. Generator range
368    if !(2..=7).contains(&g) {
369        return Err(DhError::GeneratorOutOfRange);
370    }
371
372    // 5. Residue condition per MTProto spec §4.5.
373    let valid = match g {
374        2 => prime_residue(dh_prime, 8) == 7,
375        3 => prime_residue(dh_prime, 3) == 2,
376        4 => true,
377        5 => {
378            let r = prime_residue(dh_prime, 5);
379            r == 1 || r == 4
380        }
381        6 => {
382            let r = prime_residue(dh_prime, 24);
383            r == 19 || r == 23
384        }
385        7 => {
386            let r = prime_residue(dh_prime, 7);
387            r == 3 || r == 5 || r == 6
388        }
389        _ => unreachable!(),
390    };
391    if !valid {
392        return Err(DhError::GeneratorInvalid);
393    }
394
395    Ok(())
396}
397
398#[cfg(test)]
399mod dh_tests {
400    use super::*;
401
402    #[test]
403    fn known_prime_g3_valid() {
404        // Telegram almost always sends g=3 with this prime.
405        assert_eq!(check_p_and_g(&TELEGRAM_DH_PRIME, 3), Ok(()));
406    }
407
408    #[test]
409    fn wrong_length_rejected() {
410        assert_eq!(
411            check_p_and_g(&[0u8; 128], 3),
412            Err(DhError::PrimeLengthInvalid)
413        );
414    }
415
416    #[test]
417    fn unknown_prime_rejected() {
418        let mut fake = TELEGRAM_DH_PRIME;
419        fake[255] ^= 0x01; // flip last bit
420        assert_eq!(check_p_and_g(&fake, 3), Err(DhError::PrimeUnknown));
421    }
422
423    #[test]
424    fn out_of_range_g_rejected() {
425        assert_eq!(
426            check_p_and_g(&TELEGRAM_DH_PRIME, 1),
427            Err(DhError::GeneratorOutOfRange)
428        );
429        assert_eq!(
430            check_p_and_g(&TELEGRAM_DH_PRIME, 8),
431            Err(DhError::GeneratorOutOfRange)
432        );
433    }
434}