Skip to Content
ConceptsTelegram Desktop tdata

Telegram Desktop tdata

tdata is the folder where Telegram Desktop keeps its logged-in accounts. opentele2 reads and writes the format of Telegram Desktop 7.3.1, passcode-locked folders and several accounts per folder included, with the same Rust code in every ecosystem: TDesktop in Python, loadTdata in Node, TData::load in Rust.

Warning:

A tdata folder holds the auth keys of its accounts. Whoever copies it is logged in as you. Never share it, never commit it, and work on a copy when you write.

What is in a folder

    • key_datas
    • D877F783D5D3EF8Cs
      • maps
      • configs
  • Every file is a TDF$ container: the magic TDF$, the Telegram Desktop version, the payload and an MD5 checksum. On disk a file carries one of the suffixes s, 1 or 0; opentele2 reads them in that order and writes s. Every saved file carries version 7003001 (7.3.1).
  • key_data is the key file of the data name data, the default. It holds the local key, wrapped, and the encrypted list of accounts with the active index. Another data name gives another key file (keyFile / key_file when loading).
  • Per account, a folder named after the MD5 of the account’s data name (D877F783D5D3EF8C for data; the second account is data#2, and so on) holds maps, the account map, and configs, the MTP config. Next to it, the file of the same name holds the MTP data: the user id, the main DC and the auth keys.

The local key encrypts every file except the key file. It is wrapped under a key derived from the local passcode, an empty one when the folder has no passcode.

Passcodes

A folder with a local passcode needs it to load; a wrong or missing one fails the load (TDataBadDecryptKey in Python, OT_TDATA_BAD_DECRYPT_KEY in Node, Error::BadDecryptKey in Rust) and nothing is written.

  • Local keys come from PBKDF2, as Telegram Desktop derives them.
  • Passcode wraps of the 7.3 key file are Argon2id or scrypt. A new wrap is what Telegram Desktop writes: Argon2id with 64 MiB of memory, 8 passes and 1 lane.
  • Reads are capped, so a crafted key file cannot make a load cost unbounded memory or time: Argon2id up to 256 MiB, 64 passes and 16 lanes; scrypt up to N = 2^20, r = 32, p = 16 and 192 MiB. A parameter above a cap, a salt under 8 bytes or an empty passcode is refused.

One Argon2id run holds about 64 MiB while it works. Loading many locked folders at once (load_many / loadMany / batch::load_many) costs about 64 MiB per thread; cap the thread count to bound it.

Legacy key files

A key file written before Telegram Desktop 7.3 has the three-blob layout without the passcode-wrap trailer. opentele2 loads it, and a save migrates it to the 7.3 layout under the same local key, as Telegram Desktop does on start; the account files stay valid.

  • Python: SaveTData migrates a loaded legacy folder once and keeps the result.
  • Node: tdata.legacyKeyFile tells you; migrateTdata(tdata) migrates once, otherwise every saveTdata migrates afresh.
  • Rust: TData::migrate (or migrate_with_draws) installs the 7.3 key data; save of a still-legacy TData migrates on the fly.
Warning:

A folder with a legacy key file is migrated on save; keep a copy of the original.

Several accounts

A folder holds up to six accounts (TDesktop.kMaxAccounts, TDATA_MAX_ACCOUNTS, TData::MAX_ACCOUNTS). A folder that lists more still loads. The main account is the one at the active index, else the first one; conversions use it unless you pick another account.

  • A new account takes the lowest free index, and the active index does not change.
  • Saving refuses two accounts with one index, and more accounts than six or than the folder loaded, whichever is larger.
  • An account whose files fail to read is skipped when loading; a folder where none loads fails.

The MTP config

The configs file of an account is its stored MTP config: the environment (production or test), the DC options with their endpoints, the CDN public keys and the config fields (such as chat_size_max and edit_time_limit). Every session conversion takes the main DC’s endpoint from it when the account has one, and from the built-in production table otherwise. A missing or unreadable config reads as the production default.

mtp.py
from opentele2.td import TDesktop account = TDesktop("tdata").mainAccount config = account.MtpConfig # read only; every read is a fresh copy print(config.environment, config.isTestMode()) print(config.endpoints(account.MainDcId)["selected_ipv4"]) print(sorted(config.dcOptions))

Reading and writing

tdata.py
from opentele2.td import TDesktop tdesk = TDesktop("tdata", passcode="1234") # passcode only for a locked folder for account in tdesk.accounts: print(account.index, account.UserId, account.MainDcId) print("main:", tdesk.mainAccount.UserId) # Write a copy; a new passcode re-wraps the local key. tdesk.SaveTData("tdata_copy", passcode="new passcode")

TDesktop(basePath, api, passcode, keyFile) loads on construction. SaveTData(basePath, passcode, keyFile) writes the accounts listed in tdesk.accounts, so removing one from the list removes it from the saved folder.

Performance mode

Performance mode is on by default. A new folder without a passcode then gets fixed keys instead of a fresh key derivation, which makes saving fast, and no configs files are written. Turn it off to write the configs and derive fresh keys: TDesktop.PerformanceMode(False) in Python, the performanceMode argument of newTdata and saveTdata in Node, the performance_mode field or argument in Rust. A passcode always gets freshly derived keys.

Errors

Each failure has a stable code. Python raises the matching opentele2.exception class, Node rejects with an Error whose code is OT_ plus the core code (for example OT_TDATA_BAD_DECRYPT_KEY, OT_TFILE_NOT_FOUND), and Rust returns opentele2_core::Error, whose code() is the same identifier without the prefix.

Last updated on