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