Skip to main content

layer_crypto/
lib.rs

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