Skip to main content

opentele2_core/tdata/
mod.rs

1//! Telegram Desktop `tdata` folder formats, built on the [`crate::tdf`]
2//! container, the [`crate::qt`] stream encoding, and the [`crate::crypto`]
3//! and [`crate::kdf`] primitives.
4//!
5//! - [`key_data`]: the `key_<dataName>` file that holds the local key and the
6//!   list of accounts.
7//! - [`account`]: the per-account files and the `MtpAuthorization` blob.
8//! - [`map`]: the per-account `maps` file.
9//! - [`mtp_config`]: the MTP config and DC options of the per-account
10//!   `configs` file.
11//! - [`TData`]: a whole folder, loaded and saved the way the bindings'
12//!   `TDesktop` does; [`draw_plan`] lists the random draws its writers make, and
13//!   [`DrawSource`] serves them from the OS or replays recorded ones.
14
15pub mod account;
16pub mod key_data;
17pub mod map;
18pub mod mtp_config;
19
20use std::collections::HashSet;
21use std::fmt;
22use std::path::Path;
23
24use crate::crypto::{self, AuthKey};
25use crate::kdf;
26use crate::tdata::account::Account;
27use crate::tdata::key_data::{
28    AccountsInfo, DEFAULT_KEY_FILE, KeyData, KeyDataTrailer, LOCAL_ENCRYPT_SALT_SIZE,
29};
30use crate::tdf::TdfFile;
31use crate::{Error, Result, tdf};
32
33/// Telegram Desktop version written into the `TDF$` header of every saved
34/// file (`APP_VERSION`), whatever version the folder was read with.
35pub const APP_VERSION: u32 = 7_003_001;
36
37/// The magic every `TDF$` file starts with (`TDF_MAGIC`), re-exported
38/// from [`crate::tdf::MAGIC`] next to [`APP_VERSION`].
39pub use crate::tdf::MAGIC as TDF_MAGIC;
40
41/// Index stored as the active account of a newly created folder; loading
42/// it falls back to the first account.
43pub const NEW_FOLDER_ACTIVE_INDEX: i32 = -1;
44
45/// A fallible fill of a buffer with random bytes; the writers draw padding,
46/// salts, and generated keys through it in a fixed order ([`draw_plan`]).
47pub(crate) type FillRandom<'a> = dyn FnMut(&mut [u8]) -> Result<()> + 'a;
48
49/// Decodes a lowercase hex literal at compile time; a bad literal fails the
50/// build.
51pub(crate) const fn hex_bytes<const N: usize>(hex: &str) -> [u8; N] {
52    const fn nibble(digit: u8) -> u8 {
53        match digit {
54            b'0'..=b'9' => digit - b'0',
55            b'a'..=b'f' => digit - b'a' + 10,
56            _ => panic!("hex constant holds a non-hex digit"),
57        }
58    }
59    let hex = hex.as_bytes();
60    assert!(hex.len() == 2 * N, "hex constant has the wrong length");
61    let mut out = [0u8; N];
62    let mut i = 0;
63    while i < N {
64        out[i] = (nibble(hex[2 * i]) << 4) | nibble(hex[2 * i + 1]);
65        i += 1;
66    }
67    out
68}
69
70// The four performance-mode constants replace a fresh PBKDF2 local key when
71// performance mode is on and no passcode is set, so such folders are
72// written without key derivation.
73const PERF_LOCAL_KEY: [u8; AuthKey::SIZE] = hex_bytes(concat!(
74    "d8745944519e0d2d71309d6c8d272dc64948f5e3eba7685324d5c691ad810c20",
75    "3b31d19d29aed6ac33c014be6e09843293f6fa32dbe42b6a04e00481fae99511",
76    "4caf6342bd98e96d293dd062c458689b3abd23a5cf230c75527c05bf5f90f38c",
77    "d93952cf61aaac1cfeaae4608592e363ded35f8d8c45234def53231decb35592",
78    "afc40d0601bbed11090969f74d9ab0cc97827546f441242d2cfb8e05a0610e97",
79    "669c0da1adccb56e39e10c69e2942387ff4922f8c55dcb8890e345ef318266f4",
80    "b3831430ea210c863c17624c0494cdead81f523430b5f74c15da323d766bd01c",
81    "b5b88b9d2a731f6d853380ad306a8647fa614cc4017f08902c1e1f997ee12e3c",
82));
83
84const PERF_PASSCODE_KEY_SALT: [u8; LOCAL_ENCRYPT_SALT_SIZE] =
85    hex_bytes("aed1e082994281d975760e729560d2c8d008f2a9dd3ff4d83245e22eedb66716");
86
87const PERF_PASSCODE_KEY: [u8; AuthKey::SIZE] = hex_bytes(concat!(
88    "273e6483b313c9dbc4acd9173864c0422f172881bfe1c6649ca5538654d0bd6e",
89    "bcfbcc8dabbb669717ce53b41ca8afbf9d15c23fa2b06a6e166cc61f62e99805",
90    "58cd42cf1022e05b466811ea29e9359fc1f94668e5c85155bce638fe7aa42aa8",
91    "80ca83e0d2e534198a0a890139c8f679a57d3ba06d75fe5faa6e250101a58bc6",
92    "e42b9696604de1e34ae40f6bba144a284f3ad8843253ec9b3971863a2c409208",
93    "c2563967b3587e509b42a42a6040d23ff696ad552a240084fa3f950240f499b2",
94    "3cd6d27e7010cbde07daae0667a7df8a5115a60b265c58f5d9291f7a98fc3c60",
95    "1e2a4a32f1881b8218c855239d7b532959609e6ab52e48ad691c2583b566c8f9",
96));
97
98const PERF_PASSCODE_KEY_ENCRYPTED: [u8; 288] = hex_bytes(concat!(
99    "97df0cd2e3109149b77b5287994d9c1ca240c51e87488e79dd029bea65fb9d27",
100    "89bb5abcfe65e871d752bd938d83313c794c8993a734ce1216f2e660473f3143",
101    "af9a333610a17995876e1721ce1f611d1c69d8c1a2f59f9493119704274e2cb5",
102    "f36c20df439d156deff7a34371dc44bc86f8730cebf9b028eb7a1ed6621d99ad",
103    "b62b3b2cf2295dbbb24bf132d37fffc17a0bdccc84bbea6ea34737a236b58248",
104    "a7ab4c14363c20541cb45338677f339782b205e355189658dd45ea3e8005f851",
105    "148e7e15f431904fa79c6827ee426d3ab9cba936eb33d485db88a6f0ff9722a6",
106    "d62ff788347e27c82e9e139eb03ae521539bf3d363b4baea76e5e884cf66fe6b",
107    "cd8a9e089d36405db99d01db20464fb6cabbdce4f67e4ec3742f913a1dd2dac5",
108));
109
110/// `len` bytes from `rng` (`Storage.RandomGenerate`).
111pub(crate) fn random_bytes(rng: &mut FillRandom<'_>, len: usize) -> Result<Vec<u8>> {
112    let mut bytes = vec![0u8; len];
113    rng(&mut bytes)?;
114    Ok(bytes)
115}
116
117/// [`crypto::encrypt_local`] with padding from `rng`; like tdesktop's
118/// `PrepareEncrypted`, no draw is made when no padding is needed.
119pub(crate) fn encrypt(
120    data: &[u8],
121    local_key: &AuthKey,
122    rng: &mut FillRandom<'_>,
123) -> Result<Vec<u8>> {
124    let len = crypto::local_padding_len(data.len());
125    let padding = if len == 0 {
126        Vec::new()
127    } else {
128        random_bytes(rng, len)?
129    };
130    crypto::encrypt_local(data, local_key, move |_| padding)
131}
132
133/// Writes `payload` to `dir` as the `TDF$` file `name` with [`APP_VERSION`].
134pub(crate) fn write_tdf(dir: &Path, name: &str, payload: Vec<u8>) -> Result<()> {
135    tdf::write(
136        dir,
137        name,
138        &TdfFile {
139            version: APP_VERSION,
140            payload,
141        },
142    )
143}
144
145/// The local passcode a [`TData`] was loaded or created with. It has no
146/// `Debug`, and its bytes are overwritten when it is dropped.
147#[derive(Clone)]
148struct Passcode(Vec<u8>);
149
150impl Drop for Passcode {
151    fn drop(&mut self) {
152        self.0.fill(0);
153        // Keeps the fill from being removed as a store nobody reads.
154        std::hint::black_box(&mut self.0);
155    }
156}
157
158/// A tdata folder, loaded from disk or created for writing.
159///
160/// `Debug` prints key ids only, never key material, and never the passcode.
161#[derive(Clone)]
162pub struct TData {
163    /// Telegram Desktop version stored in the key file's `TDF$` header;
164    /// [`TData::save`] always writes [`APP_VERSION`].
165    pub app_version: u32,
166    /// The data name of the key file, for example [`DEFAULT_KEY_FILE`];
167    /// [`TData::set_key_file`] changes it after checking the name.
168    pub key_file: String,
169    /// The parsed key file; only the live passcode wraps are kept, so stale
170    /// wraps are not written back. [`TData::save`] writes it
171    /// with a freshly encrypted accounts info, migrated first when it has
172    /// the legacy layout.
173    pub key_data: KeyData,
174    /// The local key every other file is encrypted with.
175    pub local_key: AuthKey,
176    /// The open or passcode key the local key was wrapped under.
177    pub passcode_key: AuthKey,
178    /// The accounts that loaded, in accounts-info order without duplicates.
179    pub accounts: Vec<Account>,
180    /// The active index stored in the accounts info, 0 when absent.
181    pub active_index: i32,
182    /// `TDesktop.kPerformanceMode`: when on, [`TData::save`] writes no
183    /// `configs` file. [`TData::load`] turns it on, the default.
184    pub performance_mode: bool,
185    /// The passcode given to [`TData::new`] or [`TData::load`]; a legacy
186    /// key file is migrated under it on [`TData::save`].
187    passcode: Passcode,
188    /// How many accounts [`TData::load`] loaded; 0 for [`TData::new`].
189    /// [`TData::set_accounts`] accepts up to this many when it is above
190    /// [`TData::MAX_ACCOUNTS`].
191    loaded_accounts: usize,
192}
193
194impl fmt::Debug for TData {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.debug_struct("TData")
197            .field("app_version", &self.app_version)
198            .field("key_file", &self.key_file)
199            .field("key_data", &self.key_data)
200            .field("local_key", &self.local_key)
201            .field("passcode_key", &self.passcode_key)
202            .field("accounts", &self.accounts)
203            .field("active_index", &self.active_index)
204            .field("performance_mode", &self.performance_mode)
205            .finish_non_exhaustive()
206    }
207}
208
209impl TData {
210    /// Most accounts one folder holds (`TDesktop.kMaxAccounts`);
211    /// [`TData::add_account`] refuses more. [`TData::load`] still reads a
212    /// folder that lists more.
213    pub const MAX_ACCOUNTS: usize = 6;
214
215    /// Creates a folder for `accounts` the way `SaveTData` does for a
216    /// `TDesktop` that was not loaded (`__generateLocalKey`), drawing random
217    /// bytes from `rng` in [`draw_plan`] order.
218    ///
219    /// - Performance mode on and an empty passcode: the fixed
220    ///   performance-mode local key and open wrap; nothing is drawn.
221    /// - Otherwise: a 256-byte random pass and a 32-byte salt make the local
222    ///   key (`create_local_key(pass, salt)`), then a 32-byte open salt is
223    ///   drawn. Without a passcode the local key is wrapped under
224    ///   `create_local_key("", open salt)`; with one, a 32-byte wrap salt is
225    ///   drawn, the open wrap is null, and the only wrap is a
226    ///   [`key_data::default_passcode_kdf`] wrap of generation 1, committed.
227    ///   Each wrap draws its 12 padding bytes.
228    ///
229    /// The folder uses [`DEFAULT_KEY_FILE`] and [`NEW_FOLDER_ACTIVE_INDEX`];
230    /// both fields can be changed before saving.
231    ///
232    /// # Errors
233    /// As [`key_data::create_passcode_key`] and [`key_data::wrap_local_key`].
234    pub fn new(
235        accounts: Vec<Account>,
236        passcode: &[u8],
237        performance_mode: bool,
238        rng: &mut dyn FnMut(&mut [u8]),
239    ) -> Result<Self> {
240        Self::generate(accounts, passcode, performance_mode, &mut |buf| {
241            rng(buf);
242            Ok(())
243        })
244    }
245
246    fn generate(
247        accounts: Vec<Account>,
248        passcode: &[u8],
249        performance_mode: bool,
250        rng: &mut FillRandom<'_>,
251    ) -> Result<Self> {
252        let (key_data, local_key, passcode_key) = if performance_mode && passcode.is_empty() {
253            let data = KeyData {
254                salt: PERF_PASSCODE_KEY_SALT.to_vec(),
255                key_encrypted: Some(PERF_PASSCODE_KEY_ENCRYPTED.to_vec()),
256                info_encrypted: Vec::new(),
257                trailer: Some(KeyDataTrailer {
258                    committed: 0,
259                    wraps: Vec::new(),
260                }),
261            };
262            (
263                data,
264                AuthKey::new(PERF_LOCAL_KEY),
265                AuthKey::new(PERF_PASSCODE_KEY),
266            )
267        } else {
268            let pass = random_bytes(rng, AuthKey::SIZE)?;
269            let salt = random_bytes(rng, LOCAL_ENCRYPT_SALT_SIZE)?;
270            let local_key = kdf::create_local_key(&pass, &salt);
271            let (data, passcode_key) = key_data::install(&local_key, passcode, rng)?;
272            (data, local_key, passcode_key)
273        };
274        Ok(Self {
275            app_version: APP_VERSION,
276            key_file: DEFAULT_KEY_FILE.to_owned(),
277            key_data,
278            local_key,
279            passcode_key,
280            accounts,
281            active_index: NEW_FOLDER_ACTIVE_INDEX,
282            performance_mode,
283            passcode: Passcode(passcode.to_vec()),
284            loaded_accounts: 0,
285        })
286    }
287
288    /// Writes the folder into `dir`, drawing every padding from `rng` in
289    /// [`draw_plan`] order.
290    ///
291    /// Per account, in order: `<folder>/maps` (two null arrays and the
292    /// encrypted map, see [`map::MapData::encode_entries`]), then, with
293    /// performance mode off, `<folder>/configs` (the loaded config or
294    /// [`account::MTP_CONFIG_PRODUCTION`]), then the `<folder>s` MTP data
295    /// ([`Account::encode_mtp_data`]). Last the key file: the open salt, the
296    /// open wrap, the accounts info ([`AccountsInfo::encode`]) encrypted
297    /// under the local key, and the 7.3 trailer. Every file carries
298    /// [`APP_VERSION`].
299    ///
300    /// A legacy key file (no trailer) is migrated first: before any other
301    /// draw, the same
302    /// local key is wrapped in a fresh 7.3 key file under the passcode this
303    /// [`TData`] was loaded or created with, as [`TData::new`] describes
304    /// after the local key (open salt, then wrap salt with a passcode, then
305    /// the wrap padding). The account files stay valid. `self` is not
306    /// changed, so every save of a still-legacy [`TData`] migrates afresh;
307    /// [`TData::migrate`] first keeps one migration for every later save.
308    ///
309    /// # Errors
310    /// - [`Error::SaveFailed`] when there is no account, or when an account
311    ///   map encodes to nothing, which cannot be encrypted.
312    /// - As [`key_data::create_passcode_key`] when migrating under a
313    ///   passcode.
314    /// - [`Error::Io`] when a directory or file cannot be written.
315    /// - [`Error::QDataStreamFailed`] when a blob does not fit a `u32` length.
316    pub fn save(&self, dir: impl AsRef<Path>, rng: &mut dyn FnMut(&mut [u8])) -> Result<()> {
317        self.save_with(dir.as_ref(), self.performance_mode, &mut |buf| {
318            rng(buf);
319            Ok(())
320        })
321    }
322
323    /// [`TData::save`] with padding from the operating system's CSPRNG.
324    ///
325    /// # Errors
326    /// [`Error::Io`] when the system random source fails, otherwise as
327    /// [`TData::save`].
328    pub fn save_random(&self, dir: impl AsRef<Path>) -> Result<()> {
329        self.save_with(
330            dir.as_ref(),
331            self.performance_mode,
332            &mut crypto::random_fill,
333        )
334    }
335
336    /// [`TData::new`] with its draws from `draws`, storing `active_index`
337    /// ([`NEW_FOLDER_ACTIVE_INDEX`] when the caller has none).
338    ///
339    /// A failed or mismatched draw leaves the keys unspecified, so the folder
340    /// is discarded and the draw failure is returned, before any failure of
341    /// [`TData::new`] itself.
342    ///
343    /// # Errors
344    /// As [`DrawSource::finish`], otherwise as [`TData::new`].
345    pub fn new_with_draws(
346        accounts: Vec<Account>,
347        passcode: &[u8],
348        performance_mode: bool,
349        active_index: i32,
350        mut draws: DrawSource,
351    ) -> Result<Self> {
352        let created = Self::new(accounts, passcode, performance_mode, &mut |buf| {
353            draws.fill(buf);
354        });
355        draws.finish()?;
356        let mut created = created?;
357        created.active_index = active_index;
358        Ok(created)
359    }
360
361    /// [`TData::save`] with its padding from `draws`, and with
362    /// `performance_mode`, when given, in place of
363    /// [`TData::performance_mode`] for this save only (on: no `configs`
364    /// files are written). `self` is not changed.
365    ///
366    /// The operating system source fails fast as [`TData::save_random`]
367    /// does. A replay runs the whole save; on a mismatch the written files
368    /// are not usable and the draw failure is returned, before any failure
369    /// of the save itself.
370    ///
371    /// # Errors
372    /// As [`DrawSource::finish`], otherwise as [`TData::save_random`].
373    pub fn save_with_draws(
374        &self,
375        dir: impl AsRef<Path>,
376        performance_mode: Option<bool>,
377        mut draws: DrawSource,
378    ) -> Result<()> {
379        let dir = dir.as_ref();
380        let performance_mode = performance_mode.unwrap_or(self.performance_mode);
381        if !draws.is_replay() {
382            return self.save_with(dir, performance_mode, &mut crypto::random_fill);
383        }
384        let saved = self.save_with(dir, performance_mode, &mut |buf| {
385            draws.fill(buf);
386            Ok(())
387        });
388        draws.finish()?;
389        saved
390    }
391
392    /// [`TData::migrate`] with its draws from `draws`. A failed or
393    /// mismatched draw leaves the new wrap unspecified, so `self` stays as
394    /// it was and the draw failure is returned.
395    ///
396    /// # Errors
397    /// As [`DrawSource::finish`], otherwise as [`TData::migrate`]; `self` is
398    /// then unchanged.
399    pub fn migrate_with_draws(&mut self, mut draws: DrawSource) -> Result<bool> {
400        let migration = self.migration(&mut |buf| {
401            draws.fill(buf);
402            Ok(())
403        });
404        draws.finish()?;
405        let Some(migration) = migration? else {
406            return Ok(false);
407        };
408        self.commit_migration(migration);
409        Ok(true)
410    }
411
412    /// Installs 7.3 key data in place of a legacy key file (no trailer),
413    /// drawing from `rng` exactly what [`TData::save`] draws to migrate, and
414    /// returns whether it did. The local key and the accounts stay as they
415    /// are; the passcode key becomes the new wrap key, and the stored
416    /// accounts info, still valid under the same local key, is kept.
417    ///
418    /// Later saves then write this key data instead of migrating afresh,
419    /// so two saves with the same padding write the same files. A key file
420    /// that already has the 7.3 layout is left alone and nothing is drawn.
421    ///
422    /// # Errors
423    /// As [`key_data::create_passcode_key`] when migrating under a
424    /// passcode; `self` is then unchanged.
425    pub fn migrate(&mut self, rng: &mut dyn FnMut(&mut [u8])) -> Result<bool> {
426        let Some(migration) = self.migration(&mut |buf| {
427            rng(buf);
428            Ok(())
429        })?
430        else {
431            return Ok(false);
432        };
433        self.commit_migration(migration);
434        Ok(true)
435    }
436
437    /// The 7.3 key data and wrap key [`TData::migrate`] installs, drawn
438    /// from `rng`; `None` for a key file that already has the 7.3 layout.
439    fn migration(&self, rng: &mut FillRandom<'_>) -> Result<Option<(KeyData, AuthKey)>> {
440        if self.key_data.trailer.is_some() {
441            return Ok(None);
442        }
443        key_data::install(&self.local_key, &self.passcode.0, rng).map(Some)
444    }
445
446    /// Installs a [`TData::migration`], keeping the stored accounts info.
447    fn commit_migration(&mut self, (data, passcode_key): (KeyData, AuthKey)) {
448        self.key_data = KeyData {
449            info_encrypted: std::mem::take(&mut self.key_data.info_encrypted),
450            ..data
451        };
452        self.passcode_key = passcode_key;
453    }
454
455    fn save_with(
456        &self,
457        dir: &Path,
458        performance_mode: bool,
459        rng: &mut FillRandom<'_>,
460    ) -> Result<()> {
461        if self.accounts.is_empty() {
462            return Err(Error::SaveFailed);
463        }
464        // tdesktop 7.3 migrates a pre-7.3 key file on start; the save writes
465        // the migrated shape directly, before the accounts draw padding.
466        let migrated = if self.key_data.trailer.is_none() {
467            Some(key_data::install(&self.local_key, &self.passcode.0, rng)?.0)
468        } else {
469            None
470        };
471        let base = migrated.as_ref().unwrap_or(&self.key_data);
472        for account in &self.accounts {
473            account.write(dir, &self.key_file, &self.local_key, performance_mode, rng)?;
474        }
475        let info = self.accounts_info();
476        let key_data = KeyData {
477            info_encrypted: encrypt(&info.encode()?, &self.local_key, rng)?,
478            ..base.clone()
479        };
480        write_tdf(
481            dir,
482            &key_data::file_name(&self.key_file),
483            key_data.encode()?,
484        )
485    }
486
487    /// Loads the folder `dir` (`TDesktop.__loadFromTData`).
488    ///
489    /// `key_file` defaults to [`DEFAULT_KEY_FILE`]. Each listed index is
490    /// loaded once; negative indices are skipped, and an account whose files
491    /// fail to read is skipped silently.
492    ///
493    /// # Errors
494    /// - As [`tdf::read`] and [`KeyData::parse`] for the key file.
495    /// - As [`key_data::open_local_key`] and
496    ///   [`key_data::decrypt_accounts_info`]; a wrong passcode is
497    ///   [`Error::BadDecryptKey`].
498    /// - [`Error::AuthKeyNotFound`] when no account loads.
499    pub fn load(dir: impl AsRef<Path>, passcode: &[u8], key_file: Option<&str>) -> Result<Self> {
500        let dir = dir.as_ref();
501        let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE);
502        let file = tdf::read(dir, &key_data::file_name(key_file))?;
503        let mut data = KeyData::parse(&file.payload)?;
504        let opened = key_data::open_local_key(&data, passcode)?;
505        let info = key_data::decrypt_accounts_info(&data, &opened.local_key, passcode)?;
506        // startModern keeps only the committed generation.
507        data.trailer = data.trailer.map(|trailer| KeyDataTrailer {
508            committed: trailer.committed,
509            wraps: trailer.live_wraps().cloned().collect(),
510        });
511
512        let mut seen = HashSet::new();
513        let accounts: Vec<Account> = info
514            .indices
515            .iter()
516            .filter(|&&index| seen.insert(index) && index >= 0)
517            .filter_map(|&index| Account::read(dir, key_file, index, &opened.local_key).ok())
518            .collect();
519        if accounts.is_empty() {
520            return Err(Error::AuthKeyNotFound);
521        }
522        Ok(Self {
523            app_version: file.version,
524            key_file: key_file.to_owned(),
525            key_data: data,
526            local_key: opened.local_key,
527            passcode_key: opened.passcode_key,
528            loaded_accounts: accounts.len(),
529            accounts,
530            active_index: info.active_index,
531            performance_mode: true,
532            passcode: Passcode(passcode.to_vec()),
533        })
534    }
535
536    /// Replaces the accounts later saves write, keeping the local key, the
537    /// key data with its salts, and the active index, so an edited
538    /// `accounts` list is written under the loaded local key.
539    ///
540    /// Up to [`TData::MAX_ACCOUNTS`] accounts are accepted, or as many as
541    /// [`TData::load`] loaded when that is more, so a folder that lists more
542    /// than six saves back under the same local key.
543    ///
544    /// # Errors
545    /// - [`Error::MaxAccountLimit`] for more accounts than that limit.
546    /// - [`Error::SaveFailed`] when two accounts share an index, whose files
547    ///   one save would overwrite.
548    ///
549    /// `self` is then unchanged.
550    pub fn set_accounts(&mut self, accounts: Vec<Account>) -> Result<()> {
551        if accounts.len() > Self::MAX_ACCOUNTS.max(self.loaded_accounts) {
552            return Err(Error::MaxAccountLimit);
553        }
554        let mut seen = HashSet::new();
555        if !accounts.iter().all(|account| seen.insert(account.index)) {
556            return Err(Error::SaveFailed);
557        }
558        self.accounts = accounts;
559        Ok(())
560    }
561
562    /// Adds an account with `auth_key` as the key of `main_dc`, as
563    /// `Account.FromTelethon` adds one to an owner: it gets the map of
564    /// [`MapData::new_account`](map::MapData::new_account) and no config,
565    /// and it is appended (`_addSingleAccount`). The active index is not
566    /// changed, so [`TData::main_account`] stays the first account of a
567    /// folder that had none. Returns the new index.
568    ///
569    /// The index is the lowest one in `0..MAX_ACCOUNTS` that no account
570    /// holds, tdesktop's own rule. The account count is not used as the
571    /// index: with loaded indices `[0, 2]` it would give the new account 2,
572    /// whose files would overwrite an existing one; here the new account
573    /// gets 1. Without gaps both rules agree.
574    ///
575    /// # Errors
576    /// [`Error::MaxAccountLimit`] when the folder already holds
577    /// [`TData::MAX_ACCOUNTS`] accounts or every index below it is taken
578    /// (`You can't have more than 6 accounts in one TDesktop client`);
579    /// `self` is then unchanged.
580    pub fn add_account(&mut self, user_id: u64, main_dc: i32, auth_key: AuthKey) -> Result<i32> {
581        if self.accounts.len() >= Self::MAX_ACCOUNTS {
582            return Err(Error::MaxAccountLimit);
583        }
584        let index = (0..)
585            .take(Self::MAX_ACCOUNTS)
586            .find(|index| self.accounts.iter().all(|account| account.index != *index))
587            .ok_or(Error::MaxAccountLimit)?;
588        let account = Account::new(index, user_id, main_dc, vec![(main_dc, auth_key)])?;
589        self.accounts.push(account);
590        Ok(index)
591    }
592
593    /// Changes the data name of the key file, as
594    /// `SaveTData(keyFile=name)` does: [`TData::save`] then writes
595    /// `key_<name>s` and every account under the data name
596    /// [`account::data_name`]`(name, index)`.
597    ///
598    /// # Errors
599    /// [`Error::SaveFailed`] when `name` is empty; `self` is then unchanged.
600    pub fn set_key_file(&mut self, name: &str) -> Result<()> {
601        if name.is_empty() {
602            return Err(Error::SaveFailed);
603        }
604        name.clone_into(&mut self.key_file);
605        Ok(())
606    }
607
608    /// The main account: the one whose index is the active index, else the
609    /// first one; `None` only when [`TData::accounts`] was emptied.
610    #[must_use]
611    pub fn main_account(&self) -> Option<&Account> {
612        self.accounts
613            .iter()
614            .find(|account| account.index == self.active_index)
615            .or_else(|| self.accounts.first())
616    }
617}
618
619/// Recorded draws served in order.
620struct Replay {
621    draws: Vec<Vec<u8>>,
622    next: usize,
623}
624
625/// The random source of [`TData::new_with_draws`],
626/// [`TData::save_with_draws`] and [`TData::migrate_with_draws`]: the
627/// operating system's CSPRNG, or, as a testing hook, recorded draws (sized
628/// by [`draw_plan`]) replayed in order.
629///
630/// The first failure is kept and every later draw is zero-filled, so the
631/// caller discards whatever was built; [`DrawSource::finish`] reports it.
632/// `Debug` prints draw counts only, never draw bytes, and replayed bytes are
633/// overwritten when the source is dropped.
634pub struct DrawSource {
635    replay: Option<Replay>,
636    failure: Option<Error>,
637}
638
639impl DrawSource {
640    /// The operating system's CSPRNG.
641    #[must_use]
642    pub fn os() -> Self {
643        Self {
644            replay: None,
645            failure: None,
646        }
647    }
648
649    /// A replay of exactly `draws`, in order.
650    #[must_use]
651    pub fn replay(draws: Vec<Vec<u8>>) -> Self {
652        Self {
653            replay: Some(Replay { draws, next: 0 }),
654            failure: None,
655        }
656    }
657
658    /// The replay of `draws` when given, else the operating system's CSPRNG:
659    /// the optional `draws` argument of the bindings.
660    #[must_use]
661    pub fn new(draws: Option<Vec<Vec<u8>>>) -> Self {
662        draws.map_or_else(Self::os, Self::replay)
663    }
664
665    /// Whether the draws are a replay rather than the operating system's
666    /// CSPRNG.
667    #[must_use]
668    pub fn is_replay(&self) -> bool {
669        self.replay.is_some()
670    }
671
672    /// Fills `buf` with the next draw; after a failure, and on the failing
673    /// draw, `buf` is zero-filled instead.
674    pub fn fill(&mut self, buf: &mut [u8]) {
675        if self.failure.is_some() {
676            buf.fill(0);
677            return;
678        }
679        let Some(replay) = self.replay.as_mut() else {
680            if let Err(error) = crypto::random_fill(buf) {
681                self.failure = Some(error);
682                buf.fill(0);
683            }
684            return;
685        };
686        match replay.draws.get(replay.next) {
687            Some(draw) if draw.len() == buf.len() => {
688                buf.copy_from_slice(draw);
689                replay.next += 1;
690            }
691            Some(draw) => {
692                self.failure = Some(replay_mismatch(format!(
693                    "draw {} has {} bytes, but {} were drawn",
694                    replay.next,
695                    draw.len(),
696                    buf.len()
697                )));
698                buf.fill(0);
699            }
700            None => {
701                self.failure = Some(replay_mismatch(format!(
702                    "draw {} of {} bytes was not given ({} draws given)",
703                    replay.next,
704                    buf.len(),
705                    replay.draws.len()
706                )));
707                buf.fill(0);
708            }
709        }
710    }
711
712    /// The first failure, else a replay whose draws were not all used.
713    ///
714    /// # Errors
715    /// - [`Error::Io`] when the operating system's CSPRNG failed.
716    /// - [`Error::InvalidInput`] with `what` `"draws"` when the replay did
717    ///   not match what was drawn; `reason` is one of `draw {i} has {n}
718    ///   bytes, but {m} were drawn`, `draw {i} of {m} bytes was not given
719    ///   ({n} draws given)` and `{k} of {n} draws were left unused`, the
720    ///   texts the bindings show after `draws: `.
721    pub fn finish(mut self) -> Result<()> {
722        if let Some(failure) = self.failure.take() {
723            return Err(failure);
724        }
725        match &self.replay {
726            Some(replay) if replay.next != replay.draws.len() => Err(replay_mismatch(format!(
727                "{} of {} draws were left unused",
728                replay.draws.len() - replay.next,
729                replay.draws.len()
730            ))),
731            _ => Ok(()),
732        }
733    }
734}
735
736impl fmt::Debug for DrawSource {
737    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738        let mut out = f.debug_struct("DrawSource");
739        match &self.replay {
740            Some(replay) => out
741                .field("replay_draws", &replay.draws.len())
742                .field("next", &replay.next),
743            None => out.field("source", &"os"),
744        };
745        out.field("failed", &self.failure.is_some()).finish()
746    }
747}
748
749impl Drop for DrawSource {
750    fn drop(&mut self) {
751        if let Some(replay) = self.replay.as_mut() {
752            for draw in &mut replay.draws {
753                draw.fill(0);
754            }
755            // Keeps the fill from being removed as a store nobody reads.
756            std::hint::black_box(&mut replay.draws);
757        }
758    }
759}
760
761/// The [`Error::InvalidInput`] of a replay that does not match the draws.
762fn replay_mismatch(reason: String) -> Error {
763    Error::InvalidInput {
764        what: "draws",
765        reason,
766    }
767}
768
769/// A call whose random draws [`draw_plan`] lists.
770#[derive(Clone, Copy, Debug)]
771pub enum DrawOperation<'a> {
772    /// [`TData::new`] with or without a passcode, and with performance mode
773    /// on or off.
774    New {
775        /// Whether the passcode is non-empty.
776        has_passcode: bool,
777        /// The `performance_mode` argument.
778        performance_mode: bool,
779    },
780    /// [`TData::migrate`] of this folder.
781    Migrate(&'a TData),
782    /// [`TData::save`] of this folder, in its current state.
783    Save(&'a TData),
784}
785
786impl<'a> DrawOperation<'a> {
787    /// The operation names [`DrawOperation::from_name`] accepts, in order.
788    pub const NAMES: [&'static str; 3] = ["new", "migrate", "save"];
789
790    /// The operation the bindings name `name`: `"new"` with `has_passcode`
791    /// (default `false`) and `performance_mode` (default `true`), the
792    /// arguments of [`TData::new`], or `"migrate"` or `"save"` of `folder`.
793    /// `has_passcode` and `performance_mode` are ignored for `"migrate"`
794    /// and `"save"`.
795    ///
796    /// # Errors
797    /// [`Error::InvalidInput`], whose `reason` is:
798    /// - `unknown operation {name:?}; expected one of new, migrate, save`
799    ///   (`what` `"operation"`) for any other name;
800    /// - `the {name:?} plan takes a TData` (`what` `"target"`) for
801    ///   `"migrate"` or `"save"` without `folder`;
802    /// - `the "new" plan takes has_passcode and performance_mode, not a
803    ///   TData` (`what` `"target"`) for `"new"` with `folder`.
804    pub fn from_name(
805        name: &str,
806        has_passcode: Option<bool>,
807        performance_mode: Option<bool>,
808        folder: Option<&'a TData>,
809    ) -> Result<Self> {
810        match (name, folder) {
811            ("new", None) => Ok(Self::New {
812                has_passcode: has_passcode.unwrap_or(false),
813                performance_mode: performance_mode.unwrap_or(true),
814            }),
815            ("new", Some(_)) => Err(Error::InvalidInput {
816                what: "target",
817                reason: "the \"new\" plan takes has_passcode and performance_mode, not a TData"
818                    .to_owned(),
819            }),
820            ("migrate", Some(folder)) => Ok(Self::Migrate(folder)),
821            ("save", Some(folder)) => Ok(Self::Save(folder)),
822            ("migrate" | "save", None) => Err(Error::InvalidInput {
823                what: "target",
824                reason: format!("the {name:?} plan takes a TData"),
825            }),
826            _ => Err(Error::InvalidInput {
827                what: "operation",
828                reason: format!(
829                    "unknown operation {name:?}; expected one of {}",
830                    Self::NAMES.join(", ")
831                ),
832            }),
833        }
834    }
835
836    /// The name [`DrawOperation::from_name`] accepts for this operation.
837    #[must_use]
838    pub fn name(&self) -> &'static str {
839        match self {
840            Self::New { .. } => "new",
841            Self::Migrate(_) => "migrate",
842            Self::Save(_) => "save",
843        }
844    }
845}
846
847/// The sizes, in bytes and in order, of the buffers `operation` will hand
848/// its random source; a replay of recorded `os.urandom` draws needs no
849/// hand-coded counts. Nothing is drawn and no file is touched.
850///
851/// - [`DrawOperation::New`]: none in performance mode without a passcode;
852///   otherwise the 256-byte pass, the 32-byte salt, then the key data
853///   install: the 32-byte open salt, a 32-byte wrap salt with a passcode,
854///   and the 12-byte wrap padding.
855/// - [`DrawOperation::Migrate`]: the key data install under the folder's
856///   passcode when its key file has the legacy layout, else none.
857/// - [`DrawOperation::Save`]: the migration draws of a legacy key file,
858///   then per account the padding of its map, of its config unless
859///   performance mode is on, and of its MTP data, then the padding of the
860///   accounts info. A padding of zero bytes is not drawn.
861///
862/// # Errors
863/// [`DrawOperation::Save`] fails as [`TData::save`] would before writing:
864/// [`Error::SaveFailed`] for a folder without accounts or with an account
865/// whose map encodes to nothing, [`Error::QDataStreamFailed`] when a blob
866/// does not fit a `u32` length.
867pub fn draw_plan(operation: DrawOperation<'_>) -> Result<Vec<usize>> {
868    match operation {
869        DrawOperation::New {
870            has_passcode,
871            performance_mode,
872        } => {
873            if performance_mode && !has_passcode {
874                return Ok(Vec::new());
875            }
876            let mut draws = vec![AuthKey::SIZE, LOCAL_ENCRYPT_SALT_SIZE];
877            draws.extend(key_data::install_draws(has_passcode));
878            Ok(draws)
879        }
880        DrawOperation::Migrate(tdata) => Ok(tdata.migration_draws()),
881        DrawOperation::Save(tdata) => {
882            if tdata.accounts.is_empty() {
883                return Err(Error::SaveFailed);
884            }
885            let mut draws = tdata.migration_draws();
886            let mut padding = |len: usize| {
887                let padding = crypto::local_padding_len(len);
888                // `encrypt` draws nothing for a zero padding.
889                if padding > 0 {
890                    draws.push(padding);
891                }
892            };
893            for account in &tdata.accounts {
894                for plaintext in account.write_plaintexts(tdata.performance_mode)? {
895                    padding(plaintext.len());
896                }
897            }
898            padding(tdata.accounts_info().encode()?.len());
899            Ok(draws)
900        }
901    }
902}
903
904impl TData {
905    /// The draws [`TData::migrate`] makes, which [`TData::save`] makes first
906    /// for a legacy key file.
907    fn migration_draws(&self) -> Vec<usize> {
908        if self.key_data.trailer.is_some() {
909            Vec::new()
910        } else {
911            key_data::install_draws(!self.passcode.0.is_empty())
912        }
913    }
914
915    /// The accounts info [`TData::save`] encrypts into the key file.
916    fn accounts_info(&self) -> AccountsInfo {
917        AccountsInfo {
918            indices: self.accounts.iter().map(|account| account.index).collect(),
919            active_index: self.active_index,
920        }
921    }
922}