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::{Error, Result, AUTH_KEY_SIZE};
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)]
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 key bytes.
51    ///
52    /// This is credential material. Never log, serialize, or expose it.
53    pub fn as_bytes(&self) -> &[u8; AUTH_KEY_SIZE] {
54        &self.data
55    }
56}
57
58impl std::fmt::Debug for AuthKey {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        // Don't expose key in debug output
61        f.debug_struct("AuthKey")
62            .field("len", &self.data.len())
63            .finish()
64    }
65}
66
67/// Create a local encryption key from salt and passcode using PBKDF2-SHA512
68///
69/// Algorithm from opentele/tdesktop:
70/// 1. hash_key = SHA512(salt + passcode + salt)
71/// 2. iterations = 1 if no passcode, else 100000
72/// 3. key = PBKDF2-HMAC-SHA512(hash_key, salt, iterations)
73pub fn create_local_key(salt: &[u8], passcode: &[u8]) -> AuthKey {
74    let mut key_data = [0u8; AUTH_KEY_SIZE];
75
76    // First compute SHA512(salt + passcode + salt)
77    let mut hasher = Sha512::new();
78    hasher.update(salt);
79    hasher.update(passcode);
80    hasher.update(salt);
81    let hash_key = hasher.finalize();
82
83    // Iterations: 1 if no passcode, 100000 otherwise
84    let iterations = if passcode.is_empty() {
85        PBKDF2_ITERATIONS_NO_PASSCODE
86    } else {
87        PBKDF2_ITERATIONS_WITH_PASSCODE
88    };
89
90    // PBKDF2-HMAC-SHA512
91    pbkdf2::pbkdf2_hmac::<Sha512>(&hash_key, salt, iterations, &mut key_data);
92
93    AuthKey { data: key_data }
94}
95
96/// Decrypt data using AES-256-IGE mode (local tdata format)
97///
98/// Format:
99/// - bytes[0..16]: encrypted_key (SHA1 hash of decrypted data, used to derive AES key/IV)
100/// - bytes[16..]: actual encrypted data
101///
102/// After decryption:
103/// - bytes[0..4]: original data length (little endian)
104/// - bytes[4..4+len]: actual data
105/// - bytes[4+len..]: padding
106pub fn decrypt_local(encrypted: &[u8], key: &AuthKey) -> Result<Vec<u8>> {
107    if encrypted.len() <= AES_BLOCK_SIZE {
108        return Err(Error::invalid_format("encrypted data too short"));
109    }
110
111    if encrypted.len() % AES_BLOCK_SIZE != 0 {
112        return Err(Error::invalid_format(
113            "encrypted data length must be multiple of 16",
114        ));
115    }
116
117    // Split: first 16 bytes is the encrypted key (msg_key), rest is encrypted data
118    let (encrypted_key, encrypted_data) = encrypted.split_at(AES_BLOCK_SIZE);
119    let encrypted_key: &[u8; AES_BLOCK_SIZE] = encrypted_key
120        .try_into()
121        .map_err(|_| Error::invalid_format("invalid encrypted message key"))?;
122
123    tracing::debug!("decrypt_local: encrypted len={}", encrypted.len());
124
125    // Prepare AES key and IV using msg_key
126    let (aes_key, aes_iv) = prepare_aes_oldmtp(key.as_bytes(), encrypted_key)?;
127
128    // Decrypt using AES-256-IGE
129    let decrypted = ige_decrypt(&aes_key, &aes_iv, encrypted_data);
130
131    // Verify: SHA1(decrypted)[0..16] must equal encrypted_key
132    let digest = sha1_hash(&decrypted);
133    let (check_hash, _) = digest.split_at(AES_BLOCK_SIZE);
134
135    tracing::debug!("Computed decrypted payload integrity check");
136
137    if check_hash != encrypted_key {
138        return Err(Error::ChecksumMismatch);
139    }
140
141    // First 4 bytes is the original length (little endian)
142    if decrypted.len() < 4 {
143        return Err(Error::DecryptionFailed);
144    }
145
146    let original_len_bytes: [u8; 4] = decrypted
147        .get(..4)
148        .ok_or(Error::DecryptionFailed)?
149        .try_into()
150        .map_err(|_| Error::DecryptionFailed)?;
151    let original_len = usize::try_from(u32::from_le_bytes(original_len_bytes))
152        .map_err(|_| Error::invalid_format("decrypted payload length is too large"))?;
153
154    let full_len = encrypted_data.len();
155
156    // Validate length
157    if original_len > decrypted.len()
158        || original_len <= full_len.saturating_sub(16)
159        || original_len < 4
160    {
161        return Err(Error::invalid_format(format!(
162            "invalid decrypted length: {}, full_len: {}, decrypted size: {}",
163            original_len,
164            full_len,
165            decrypted.len()
166        )));
167    }
168
169    // Skip the length prefix, return actual data
170    decrypted
171        .get(4..original_len)
172        .map(ToOwned::to_owned)
173        .ok_or_else(|| Error::invalid_format("invalid decrypted payload bounds"))
174}
175
176/// Prepare AES key and IV from auth key and message key (old MTProto 1.0 style)
177///
178/// This matches tdesktop's prepareAES_oldmtp with send=false (for decrypt)
179/// For decrypt: x = 8
180fn prepare_aes_oldmtp(
181    auth_key: &[u8; AUTH_KEY_SIZE],
182    msg_key: &[u8; AES_BLOCK_SIZE],
183) -> Result<([u8; AES_KEY_SIZE], [u8; AES_KEY_SIZE])> {
184    let auth_range = |range| {
185        auth_key
186            .get(range)
187            .ok_or_else(|| Error::invalid_format("auth key is too short for AES derivation"))
188    };
189
190    // sha1_a = SHA1(msgKey + key[8..40])
191    let sha1_a = sha1_hash_2(msg_key, auth_range(8..40)?);
192
193    // sha1_b = SHA1(key[32+x..48+x] + msgKey + key[48+x..64+x])
194    let sha1_b = sha1_hash_3(auth_range(40..56)?, msg_key, auth_range(56..72)?);
195
196    // sha1_c = SHA1(key[72..104] + msgKey)
197    let sha1_c = sha1_hash_2(auth_range(72..104)?, msg_key);
198
199    // sha1_d = SHA1(msgKey + key[104..136])
200    let sha1_d = sha1_hash_2(msg_key, auth_range(104..136)?);
201
202    // aes_key = sha1_a[0..8] + sha1_b[8..20] + sha1_c[4..16]
203    let key_bytes: Vec<u8> = sha1_a
204        .iter()
205        .take(8)
206        .chain(sha1_b.iter().skip(8).take(12))
207        .chain(sha1_c.iter().skip(4).take(12))
208        .copied()
209        .collect();
210    let key = key_bytes
211        .try_into()
212        .map_err(|_| Error::invalid_format("failed to derive AES key"))?;
213
214    // aes_iv = sha1_a[8..20] + sha1_b[0..8] + sha1_c[16..20] + sha1_d[0..8]
215    let iv_bytes: Vec<u8> = sha1_a
216        .iter()
217        .skip(8)
218        .take(12)
219        .chain(sha1_b.iter().take(8))
220        .chain(sha1_c.iter().skip(16).take(4))
221        .chain(sha1_d.iter().take(8))
222        .copied()
223        .collect();
224    let iv = iv_bytes
225        .try_into()
226        .map_err(|_| Error::invalid_format("failed to derive AES IV"))?;
227
228    Ok((key, iv))
229}
230
231/// AES-256-IGE decryption
232fn ige_decrypt(key: &[u8; 32], iv: &[u8; 32], data: &[u8]) -> Vec<u8> {
233    use grammers_crypto::aes::ige_decrypt as grammers_ige_decrypt;
234
235    let mut decrypted = data.to_vec();
236    grammers_ige_decrypt(&mut decrypted, key, iv);
237    decrypted
238}
239
240/// Compute SHA-1 hash
241fn sha1_hash(data: &[u8]) -> [u8; 20] {
242    let mut hasher = Sha1::new();
243    hasher.update(data);
244    hasher.finalize().into()
245}
246
247/// Compute SHA-1 hash of two concatenated slices
248fn sha1_hash_2(a: &[u8], b: &[u8]) -> [u8; 20] {
249    let mut hasher = Sha1::new();
250    hasher.update(a);
251    hasher.update(b);
252    hasher.finalize().into()
253}
254
255/// Compute SHA-1 hash of three concatenated slices
256fn sha1_hash_3(a: &[u8], b: &[u8], c: &[u8]) -> [u8; 20] {
257    let mut hasher = Sha1::new();
258    hasher.update(a);
259    hasher.update(b);
260    hasher.update(c);
261    hasher.finalize().into()
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    fn fixture_salt() -> [u8; LOCAL_ENCRYPT_SALT_SIZE] {
269        // Deterministic test data, constructed at runtime so security scanners do not
270        // mistake a test-only fixture for a production hard-coded cryptographic salt.
271        std::array::from_fn(|index| u8::try_from(index).unwrap_or_default())
272    }
273
274    #[test]
275    fn test_create_local_key_no_passcode() {
276        let salt = fixture_salt();
277        let passcode = b"";
278
279        let key = create_local_key(&salt, passcode);
280        assert_eq!(key.as_bytes().len(), AUTH_KEY_SIZE);
281    }
282
283    #[test]
284    fn test_create_local_key_with_passcode() {
285        let salt = fixture_salt();
286        let passcode = b"test";
287
288        let key = create_local_key(&salt, passcode);
289        assert_eq!(key.as_bytes().len(), AUTH_KEY_SIZE);
290
291        // Same inputs should produce same key
292        let key2 = create_local_key(&salt, passcode);
293        assert_eq!(key.as_bytes(), key2.as_bytes());
294    }
295
296    #[test]
297    fn test_auth_key_from_bytes() -> Result<()> {
298        let bytes = [0xAB; AUTH_KEY_SIZE];
299        let key = AuthKey::from_bytes(&bytes)?;
300        assert_eq!(key.as_bytes(), &bytes);
301        Ok(())
302    }
303
304    #[test]
305    fn test_auth_key_wrong_size() {
306        let bytes = [0u8; 100];
307        assert!(AuthKey::from_bytes(&bytes).is_err());
308    }
309
310    #[test]
311    fn test_sha1_hash() {
312        let data = b"hello";
313        let hash = sha1_hash(data);
314        // SHA1("hello") = aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
315        assert_eq!(
316            hex::encode(hash),
317            "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"
318        );
319    }
320}