Skip to main content

ferogram_crypto/
lib.rs

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