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
//! Account representation

use std::net::{IpAddr, Ipv4Addr, SocketAddr};

use crate::{AUTH_KEY_SIZE, Result};

/// Telegram datacenter addresses (production)
const DC_ADDRESSES: [(i32, Ipv4Addr, u16); 5] = [
    (1, Ipv4Addr::new(149, 154, 175, 53), 443),
    (2, Ipv4Addr::new(149, 154, 167, 51), 443),
    (3, Ipv4Addr::new(149, 154, 175, 100), 443),
    (4, Ipv4Addr::new(149, 154, 167, 91), 443),
    (5, Ipv4Addr::new(91, 108, 56, 130), 443),
];

/// Get socket address for a datacenter
fn get_dc_addr(dc_id: i32) -> SocketAddr {
    DC_ADDRESSES.iter().find(|(id, _, _)| *id == dc_id).map_or_else(
        || SocketAddr::new(IpAddr::V4(Ipv4Addr::new(149, 154, 167, 51)), 443),
        |(_, ip, port)| SocketAddr::new(IpAddr::V4(*ip), *port),
    )
}

/// A Telegram account extracted from tdata
#[derive(Debug)]
pub struct Account {
    /// Account index (0-2)
    index: i32,
    /// Datacenter ID (1-5)
    dc_id: i32,
    /// User ID
    user_id: i64,
    /// Authorization key (256 bytes)
    auth_key: [u8; AUTH_KEY_SIZE],
}

impl Account {
    /// Create a new account
    pub(crate) const fn new(
        index: i32,
        dc_id: i32,
        user_id: i64,
        auth_key: [u8; AUTH_KEY_SIZE],
    ) -> Self {
        Self { index, dc_id, user_id, auth_key }
    }

    /// Get the account index (0-2)
    #[must_use]
    pub const fn index(&self) -> i32 {
        self.index
    }

    /// Get the datacenter ID (1-5)
    #[must_use]
    pub const fn dc_id(&self) -> i32 {
        self.dc_id
    }

    /// Get the user ID
    #[must_use]
    pub const fn user_id(&self) -> i64 {
        self.user_id
    }

    /// Get the raw auth key bytes
    #[must_use]
    pub const fn auth_key_bytes(&self) -> &[u8; AUTH_KEY_SIZE] {
        &self.auth_key
    }

    /// Convert to grammers session data
    ///
    /// Returns the session data that can be used with grammers-client
    pub fn to_grammers_session(&self) -> Result<grammers_session::Session> {
        use grammers_session::Session;

        let session = Session::new();

        // Insert datacenter with the auth key
        let addr = get_dc_addr(self.dc_id);
        session.insert_dc(self.dc_id, addr, self.auth_key);

        // Set the user as the "self" chat
        if self.user_id != 0 {
            session.set_user(self.user_id, self.dc_id, false);
        }

        Ok(session)
    }

    /// Export session as a base64 string (portable format)
    pub fn to_session_string(&self) -> Result<String> {
        let session = self.to_grammers_session()?;

        // Serialize session to bytes and base64 encode
        let data = session.save();
        Ok(base64_encode(&data))
    }
}

/// Base64 encode without external dependency
fn base64_encode(data: &[u8]) -> String {
    const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    let mut result = String::new();
    let mut i = 0;

    while i < data.len() {
        let b0 = data[i] as usize;
        let b1 = if i + 1 < data.len() { data[i + 1] as usize } else { 0 };
        let b2 = if i + 2 < data.len() { data[i + 2] as usize } else { 0 };

        result.push(ALPHABET[b0 >> 2] as char);
        result.push(ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)] as char);

        if i + 1 < data.len() {
            result.push(ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] as char);
        } else {
            result.push('=');
        }

        if i + 2 < data.len() {
            result.push(ALPHABET[b2 & 0x3f] as char);
        } else {
            result.push('=');
        }

        i += 3;
    }

    result
}

#[cfg(test)]
#[allow(
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::indexing_slicing,
    clippy::unreadable_literal,
    reason = "test assertions with controlled inputs"
)]
mod tests {
    use super::*;

    #[test]
    fn test_account_creation() {
        let auth_key = [0xAB; AUTH_KEY_SIZE];
        let account = Account::new(0, 2, 12345678, auth_key);

        assert_eq!(account.index(), 0);
        assert_eq!(account.dc_id(), 2);
        assert_eq!(account.user_id(), 12345678);
        assert_eq!(account.auth_key_bytes(), &auth_key);
    }

    #[test]
    fn test_base64_encode() {
        assert_eq!(base64_encode(b""), "");
        assert_eq!(base64_encode(b"f"), "Zg==");
        assert_eq!(base64_encode(b"fo"), "Zm8=");
        assert_eq!(base64_encode(b"foo"), "Zm9v");
        assert_eq!(base64_encode(b"foob"), "Zm9vYg==");
        assert_eq!(base64_encode(b"fooba"), "Zm9vYmE=");
        assert_eq!(base64_encode(b"foobar"), "Zm9vYmFy");
    }
}