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