Skip to main content

layer_crypto/
lib.rs

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