Skip to main content

hermes_tdata/
crypto.rs

1//! Cryptographic operations for tdata
2//!
3//! Implements:
4//! - PBKDF2-SHA512 key derivation
5//! - AES-256-IGE encryption/decryption
6//! - SHA1/MD5 checksums
7
8use sha1::{Digest as Sha1Digest, Sha1};
9use sha2::Sha512;
10
11use crate::{AUTH_KEY_SIZE, Error, Result};
12
13/// Size of local encryption salt
14pub const LOCAL_ENCRYPT_SALT_SIZE: usize = 32;
15
16/// AES-256 key size
17pub const AES_KEY_SIZE: usize = 32;
18
19/// AES block size
20pub const AES_BLOCK_SIZE: usize = 16;
21
22/// PBKDF2 iteration count used by Telegram Desktop (with passcode)
23const PBKDF2_ITERATIONS_WITH_PASSCODE: u32 = 100_000;
24
25/// PBKDF2 iteration count used by Telegram Desktop (without passcode)
26const PBKDF2_ITERATIONS_NO_PASSCODE: u32 = 1;
27
28/// Auth key for encryption/decryption
29#[derive(Clone, Copy)]
30pub struct AuthKey {
31    data: [u8; AUTH_KEY_SIZE],
32}
33
34impl AuthKey {
35    /// Create an `AuthKey` from raw bytes
36    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
37        if bytes.len() != AUTH_KEY_SIZE {
38            return Err(Error::invalid_format(format!(
39                "auth key must be {} bytes, got {}",
40                AUTH_KEY_SIZE,
41                bytes.len()
42            )));
43        }
44
45        let mut data = [0u8; AUTH_KEY_SIZE];
46        data.copy_from_slice(bytes);
47        Ok(Self { data })
48    }
49
50    /// Get raw bytes
51    #[must_use]
52    pub const fn as_bytes(&self) -> &[u8; AUTH_KEY_SIZE] {
53        &self.data
54    }
55}
56
57impl std::fmt::Debug for AuthKey {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        // Don't expose key in debug output
60        f.debug_struct("AuthKey").field("len", &self.data.len()).finish()
61    }
62}
63
64/// Create a local encryption key from salt and passcode using PBKDF2-SHA512
65///
66/// Algorithm from opentele/tdesktop:
67/// 1. `hash_key` = SHA512(salt + passcode + salt)
68/// 2. iterations = 1 if no passcode, else 100000
69/// 3. key = PBKDF2-HMAC-SHA512(hash_key, salt, iterations)
70#[must_use]
71pub fn create_local_key(salt: &[u8], passcode: &[u8]) -> AuthKey {
72    let mut key_data = [0u8; AUTH_KEY_SIZE];
73
74    // First compute SHA512(salt + passcode + salt)
75    let mut hasher = Sha512::new();
76    hasher.update(salt);
77    hasher.update(passcode);
78    hasher.update(salt);
79    let hash_key = hasher.finalize();
80
81    // Iterations: 1 if no passcode, 100000 otherwise
82    let iterations = if passcode.is_empty() {
83        PBKDF2_ITERATIONS_NO_PASSCODE
84    } else {
85        PBKDF2_ITERATIONS_WITH_PASSCODE
86    };
87
88    // PBKDF2-HMAC-SHA512
89    pbkdf2::pbkdf2_hmac::<Sha512>(&hash_key, salt, iterations, &mut key_data);
90
91    AuthKey { data: key_data }
92}
93
94/// Decrypt data using AES-256-IGE mode (local tdata format)
95///
96/// Format:
97/// - bytes[0..16]: `encrypted_key` (SHA1 hash of decrypted data, used to derive AES key/IV)
98/// - bytes[16..]: actual encrypted data
99///
100/// After decryption:
101/// - bytes[0..4]: original data length (little endian)
102/// - bytes[4..4+len]: actual data
103/// - bytes[4+len..]: padding
104pub fn decrypt_local(encrypted: &[u8], key: &AuthKey) -> Result<Vec<u8>> {
105    if encrypted.len() <= AES_BLOCK_SIZE {
106        return Err(Error::invalid_format("encrypted data too short"));
107    }
108
109    if !encrypted.len().is_multiple_of(AES_BLOCK_SIZE) {
110        return Err(Error::invalid_format("encrypted data length must be multiple of 16"));
111    }
112
113    // Split: first 16 bytes is the encrypted key (msg_key), rest is encrypted data
114    let encrypted_key = &encrypted[0..16];
115    let encrypted_data = &encrypted[16..];
116
117    tracing::debug!(
118        "decrypt_local: encrypted len={}, msg_key={:02x?}",
119        encrypted.len(),
120        encrypted_key
121    );
122
123    // Prepare AES key and IV using msg_key
124    let (aes_key, aes_iv) = prepare_aes_oldmtp(key.as_bytes(), encrypted_key);
125
126    // Decrypt using AES-256-IGE
127    let decrypted = ige_decrypt(&aes_key, &aes_iv, encrypted_data);
128
129    // Verify: SHA1(decrypted)[0..16] must equal encrypted_key
130    let check_hash = &sha1_hash(&decrypted)[0..16];
131
132    tracing::debug!("SHA1 check: expected={:02x?}, computed={:02x?}", encrypted_key, check_hash);
133
134    if check_hash != encrypted_key {
135        return Err(Error::ChecksumMismatch);
136    }
137
138    // First 4 bytes is the original length (little endian)
139    if decrypted.len() < 4 {
140        return Err(Error::DecryptionFailed);
141    }
142
143    let original_len =
144        u32::from_le_bytes([decrypted[0], decrypted[1], decrypted[2], decrypted[3]]) as usize;
145
146    let full_len = encrypted_data.len();
147
148    // Validate length
149    if original_len > decrypted.len()
150        || original_len <= full_len.saturating_sub(16)
151        || original_len < 4
152    {
153        return Err(Error::invalid_format(format!(
154            "invalid decrypted length: {}, full_len: {}, decrypted size: {}",
155            original_len,
156            full_len,
157            decrypted.len()
158        )));
159    }
160
161    // Skip the length prefix, return actual data
162    Ok(decrypted[4..original_len].to_vec())
163}
164
165/// Prepare AES key and IV from auth key and message key (old `MTProto` 1.0 style)
166///
167/// This matches tdesktop's `prepareAES_oldmtp` with send=false (for decrypt)
168/// For decrypt: x = 8
169fn prepare_aes_oldmtp(auth_key: &[u8], msg_key: &[u8]) -> ([u8; AES_KEY_SIZE], [u8; AES_KEY_SIZE]) {
170    // For decrypt, x = 8 (send=false in tdesktop)
171    let x: usize = 8;
172
173    // sha1_a = SHA1(msgKey + key[x..x+32])
174    let sha1_a = sha1_hash_2(msg_key, &auth_key[x..x + 32]);
175
176    // sha1_b = SHA1(key[32+x..48+x] + msgKey + key[48+x..64+x])
177    let sha1_b = sha1_hash_3(&auth_key[32 + x..48 + x], msg_key, &auth_key[48 + x..64 + x]);
178
179    // sha1_c = SHA1(key[64+x..96+x] + msgKey)
180    let sha1_c = sha1_hash_2(&auth_key[64 + x..96 + x], msg_key);
181
182    // sha1_d = SHA1(msgKey + key[96+x..128+x])
183    let sha1_d = sha1_hash_2(msg_key, &auth_key[96 + x..128 + x]);
184
185    let mut key = [0u8; AES_KEY_SIZE];
186    let mut iv = [0u8; AES_KEY_SIZE];
187
188    // aes_key = sha1_a[0..8] + sha1_b[8..20] + sha1_c[4..16]
189    key[0..8].copy_from_slice(&sha1_a[0..8]);
190    key[8..20].copy_from_slice(&sha1_b[8..20]);
191    key[20..32].copy_from_slice(&sha1_c[4..16]);
192
193    // aes_iv = sha1_a[8..20] + sha1_b[0..8] + sha1_c[16..20] + sha1_d[0..8]
194    iv[0..12].copy_from_slice(&sha1_a[8..20]);
195    iv[12..20].copy_from_slice(&sha1_b[0..8]);
196    iv[20..24].copy_from_slice(&sha1_c[16..20]);
197    iv[24..32].copy_from_slice(&sha1_d[0..8]);
198
199    (key, iv)
200}
201
202/// AES-256-IGE decryption
203fn ige_decrypt(key: &[u8; 32], iv: &[u8; 32], data: &[u8]) -> Vec<u8> {
204    use grammers_crypto::aes::ige_decrypt as grammers_ige_decrypt;
205
206    grammers_ige_decrypt(data, key, iv)
207}
208
209/// Compute SHA-1 hash
210fn sha1_hash(data: &[u8]) -> [u8; 20] {
211    let mut hasher = Sha1::new();
212    hasher.update(data);
213    hasher.finalize().into()
214}
215
216/// Compute SHA-1 hash of two concatenated slices
217fn sha1_hash_2(a: &[u8], b: &[u8]) -> [u8; 20] {
218    let mut hasher = Sha1::new();
219    hasher.update(a);
220    hasher.update(b);
221    hasher.finalize().into()
222}
223
224/// Compute SHA-1 hash of three concatenated slices
225fn sha1_hash_3(a: &[u8], b: &[u8], c: &[u8]) -> [u8; 20] {
226    let mut hasher = Sha1::new();
227    hasher.update(a);
228    hasher.update(b);
229    hasher.update(c);
230    hasher.finalize().into()
231}
232
233#[cfg(test)]
234#[allow(
235    clippy::unwrap_used,
236    clippy::expect_used,
237    clippy::assertions_on_result_states,
238    reason = "test assertions with controlled inputs"
239)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_create_local_key_no_passcode() {
245        let salt = [0u8; LOCAL_ENCRYPT_SALT_SIZE];
246        let passcode = b"";
247
248        let key = create_local_key(&salt, passcode);
249        assert_eq!(key.as_bytes().len(), AUTH_KEY_SIZE);
250    }
251
252    #[test]
253    fn test_create_local_key_with_passcode() {
254        let salt = [0u8; LOCAL_ENCRYPT_SALT_SIZE];
255        let passcode = b"test";
256
257        let key = create_local_key(&salt, passcode);
258        assert_eq!(key.as_bytes().len(), AUTH_KEY_SIZE);
259
260        // Same inputs should produce same key
261        let key2 = create_local_key(&salt, passcode);
262        assert_eq!(key.as_bytes(), key2.as_bytes());
263    }
264
265    #[test]
266    fn test_auth_key_from_bytes() {
267        let bytes = [0xAB; AUTH_KEY_SIZE];
268        let key = AuthKey::from_bytes(&bytes).unwrap();
269        assert_eq!(key.as_bytes(), &bytes);
270    }
271
272    #[test]
273    fn test_auth_key_wrong_size() {
274        let bytes = [0u8; 100];
275        assert!(AuthKey::from_bytes(&bytes).is_err());
276    }
277
278    #[test]
279    fn test_sha1_hash() {
280        let data = b"hello";
281        let hash = sha1_hash(data);
282        // SHA1("hello") = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
283        assert_eq!(hex::encode(hash), "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
284    }
285}