Skip to main content

layer_crypto/
lib.rs

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