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