Skip to main content

layer_crypto/
lib.rs

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