hermes-tdata 0.1.1

Pure Rust parser for Telegram Desktop's tdata storage format. Decrypts local storage and extracts auth keys without Qt/C++ dependencies.
Documentation
//! Key data structures and operations
//!
//! Handles reading, parsing, and decrypting `key_data` files.

use std::path::Path;

use crate::crypto::{AuthKey, create_local_key, decrypt_local};
use crate::qdatastream::QDataStream;
use crate::{Error, MAX_ACCOUNTS, Result};

use super::file_io::read_file;

/// Key data parsed from `key_data` file
#[derive(Debug)]
pub struct KeyData {
    pub salt: Vec<u8>,
    pub key_encrypted: Vec<u8>,
    pub info_encrypted: Vec<u8>,
    pub version: u32,
}

/// Decrypted key info containing account indices
#[derive(Debug)]
pub struct KeyInfo {
    pub local_key: AuthKey,
    pub account_indices: Vec<i32>,
}

/// Parse the `key_data` file
pub fn read_key_data(base_path: &Path, key_file: &str) -> Result<KeyData> {
    let name = format!("key_{key_file}");
    let file = read_file(&name, base_path)?;

    let mut stream = QDataStream::new(&file.data);

    let salt = stream.read_qbytearray()?;
    let key_encrypted = stream.read_qbytearray()?;
    let info_encrypted = stream.read_qbytearray()?;

    Ok(KeyData { salt, key_encrypted, info_encrypted, version: file.version })
}

/// Decrypt the key data
pub fn decrypt_key_data(key_data: &KeyData, passcode: &[u8]) -> Result<KeyInfo> {
    // Create passcode key from salt
    let passcode_key = create_local_key(&key_data.salt, passcode);

    // Decrypt the key_encrypted to get the local key
    let decrypted_key = decrypt_local(&key_data.key_encrypted, &passcode_key)?;

    if decrypted_key.len() < 256 {
        return Err(Error::invalid_format(format!(
            "decrypted key too short: {} bytes",
            decrypted_key.len()
        )));
    }

    let local_key = AuthKey::from_bytes(&decrypted_key[..256])?;

    // Decrypt info to get account indices
    let decrypted_info = decrypt_local(&key_data.info_encrypted, &local_key)?;
    let mut info_stream = QDataStream::new(&decrypted_info);

    let count = info_stream.read_i32()?;

    if count <= 0 || count > MAX_ACCOUNTS as i32 {
        return Err(Error::invalid_format(format!("invalid account count: {count}")));
    }

    let mut account_indices = Vec::with_capacity(count as usize);
    for _ in 0..count {
        let index = info_stream.read_i32()?;
        if index >= 0 && index < MAX_ACCOUNTS as i32 {
            account_indices.push(index);
        }
    }

    Ok(KeyInfo { local_key, account_indices })
}