Skip to main content

hermes_tdata/
tdesktop.rs

1//! TDesktop client implementation
2//!
3//! Main entry point for parsing tdata folders.
4
5use std::fmt;
6use std::path::{Path, PathBuf};
7
8use crate::account::Account;
9use crate::crypto::AuthKey;
10use crate::storage::{
11    decrypt_key_data, get_absolute_path, get_default_tdata_path, read_key_data, read_mtp_data,
12    KeyInfo,
13};
14use crate::{Error, Result, DEFAULT_KEY_FILE};
15
16/// Telegram Desktop client representation
17///
18/// Represents a parsed tdata folder with all its accounts.
19pub struct TDesktop {
20    /// Base path to the tdata folder
21    base_path: PathBuf,
22    /// Key file name (usually "data")
23    key_file: String,
24    /// Whether a non-empty passcode was supplied for decryption.
25    ///
26    /// The passcode itself is deliberately not retained after loading.
27    has_passcode: bool,
28    /// Local encryption key
29    local_key: AuthKey,
30    /// List of accounts
31    accounts: Vec<Account>,
32    /// App version from tdata
33    app_version: u32,
34}
35
36impl fmt::Debug for TDesktop {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        f.debug_struct("TDesktop")
39            .field("base_path", &"<redacted>")
40            .field("key_file", &self.key_file)
41            .field("has_passcode", &self.has_passcode)
42            .field("accounts_count", &self.accounts.len())
43            .field("app_version", &self.app_version)
44            .finish()
45    }
46}
47
48impl TDesktop {
49    /// Load TDesktop from the default tdata location
50    ///
51    /// # Returns
52    /// - `Ok(TDesktop)` if loading succeeded
53    /// - `Err(Error::FolderNotFound)` if the default location doesn't exist
54    pub fn from_default() -> Result<Self> {
55        let path = get_default_tdata_path().ok_or_else(|| Error::FolderNotFound {
56            path: PathBuf::from("(default tdata path)"),
57        })?;
58
59        Self::from_path(path)
60    }
61
62    /// Load TDesktop from a specific path
63    ///
64    /// # Arguments
65    /// - `path`: Path to the tdata folder
66    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
67        Self::with_options(path, None, None)
68    }
69
70    /// Load TDesktop with a passcode
71    ///
72    /// Use this when the tdata is protected with a Local Passcode.
73    ///
74    /// # Arguments
75    /// - `path`: Path to the tdata folder
76    /// - `passcode`: The Local Passcode
77    pub fn from_path_with_passcode<P: AsRef<Path>>(path: P, passcode: &str) -> Result<Self> {
78        Self::with_options(path, Some(passcode), None)
79    }
80
81    /// Load TDesktop with all options
82    ///
83    /// # Arguments
84    /// - `path`: Path to the tdata folder
85    /// - `passcode`: Optional Local Passcode
86    /// - `key_file`: Optional key file name (default: "data")
87    pub fn with_options<P: AsRef<Path>>(
88        path: P,
89        passcode: Option<&str>,
90        key_file: Option<&str>,
91    ) -> Result<Self> {
92        let base_path = get_absolute_path(path.as_ref());
93
94        if !base_path.exists() {
95            return Err(Error::FolderNotFound {
96                path: base_path.clone(),
97            });
98        }
99
100        let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE).to_string();
101        let passcode = passcode.unwrap_or("");
102        let has_passcode = !passcode.is_empty();
103
104        // Read and decrypt key data
105        let key_data = read_key_data(&base_path, &key_file)?;
106
107        let KeyInfo {
108            local_key,
109            account_indices,
110        } = decrypt_key_data(&key_data, passcode.as_bytes())?;
111
112        tracing::info!("Loaded key data: {} accounts found", account_indices.len());
113
114        // Load accounts
115        let mut accounts = Vec::new();
116        for index in account_indices {
117            match Self::load_account(&base_path, index, &local_key, &key_file) {
118                Ok(account) => {
119                    tracing::info!("Loaded account {} for DC {}", index, account.dc_id());
120                    accounts.push(account);
121                }
122                Err(e) => {
123                    tracing::warn!("Failed to load account {}: {}", index, e);
124                }
125            }
126        }
127
128        if accounts.is_empty() {
129            return Err(Error::NoAccounts);
130        }
131
132        Ok(Self {
133            base_path,
134            key_file,
135            has_passcode,
136            local_key,
137            accounts,
138            app_version: key_data.version,
139        })
140    }
141
142    /// Load a single account
143    fn load_account(
144        base_path: &Path,
145        index: i32,
146        local_key: &AuthKey,
147        key_file: &str,
148    ) -> Result<Account> {
149        let mtp_data = read_mtp_data(base_path, index, local_key, key_file)?;
150
151        Ok(Account::new(
152            index,
153            mtp_data.dc_id,
154            mtp_data.user_id,
155            mtp_data.auth_key,
156        ))
157    }
158
159    /// Get the base path to the tdata folder.
160    ///
161    /// Local paths can reveal account names and workstation layout. Avoid logging them.
162    pub fn base_path(&self) -> &Path {
163        &self.base_path
164    }
165
166    /// Get the number of accounts
167    pub fn accounts_count(&self) -> usize {
168        self.accounts.len()
169    }
170
171    /// Get all accounts
172    pub fn accounts(&self) -> &[Account] {
173        &self.accounts
174    }
175
176    /// Get the main (first) account
177    pub fn main_account(&self) -> Option<&Account> {
178        self.accounts.first()
179    }
180
181    /// Get an account by index
182    pub fn account(&self, index: usize) -> Option<&Account> {
183        self.accounts.get(index)
184    }
185
186    /// Get the app version
187    pub fn app_version(&self) -> u32 {
188        self.app_version
189    }
190
191    /// Check if the tdata has a passcode
192    pub fn has_passcode(&self) -> bool {
193        self.has_passcode
194    }
195
196    /// Get the key file name
197    pub fn key_file(&self) -> &str {
198        &self.key_file
199    }
200
201    /// Get the local encryption key.
202    ///
203    /// This is credential material. Never log, serialize, or expose it.
204    pub fn local_key(&self) -> &AuthKey {
205        &self.local_key
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use std::path::PathBuf;
213
214    /// Builder for TDesktop with more control over loading
215    struct TDesktopBuilder {
216        path: PathBuf,
217        passcode: Option<String>,
218        key_file: Option<String>,
219    }
220
221    impl TDesktopBuilder {
222        /// Create a new builder with the given path
223        fn new<P: AsRef<Path>>(path: P) -> Self {
224            Self {
225                path: path.as_ref().to_path_buf(),
226                passcode: None,
227                key_file: None,
228            }
229        }
230
231        /// Set the passcode
232        fn passcode(mut self, passcode: impl Into<String>) -> Self {
233            self.passcode = Some(passcode.into());
234            self
235        }
236
237        /// Set the key file name
238        fn key_file(mut self, key_file: impl Into<String>) -> Self {
239            self.key_file = Some(key_file.into());
240            self
241        }
242    }
243
244    #[test]
245    fn test_builder() {
246        let builder = TDesktopBuilder::new("/path/to/tdata")
247            .passcode("secret")
248            .key_file("custom");
249
250        assert_eq!(builder.path, PathBuf::from("/path/to/tdata"));
251        assert_eq!(builder.passcode, Some("secret".to_string()));
252        assert_eq!(builder.key_file, Some("custom".to_string()));
253    }
254
255    #[test]
256    fn debug_redacts_storage_path_and_account_identity() -> Result<()> {
257        let tdesktop = TDesktop {
258            base_path: PathBuf::from("/private/user/TelegramDesktop/tdata"),
259            key_file: "data".to_string(),
260            has_passcode: true,
261            local_key: AuthKey::from_bytes(&[0xCD; crate::AUTH_KEY_SIZE])?,
262            accounts: vec![Account::new(0, 2, 12_345_678, [0xAB; crate::AUTH_KEY_SIZE])],
263            app_version: 6_004_001,
264        };
265        let debug = format!("{tdesktop:?}");
266
267        assert!(debug.contains("<redacted>"));
268        assert!(!debug.contains("/private/user"));
269        assert!(!debug.contains("12345678"));
270        assert!(!debug.contains("171, 171"));
271        assert!(!debug.contains("205, 205"));
272        Ok(())
273    }
274}