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}