opentele2-core
The pure core of opentele2: every Telegram Desktop tdata format, session format, device
catalog, API profile, fingerprint rule and the MTProto client crypto, shared by the Python and Node
bindings.
[dependencies]
opentele2-core = "2.0.0"
# opentele2-core = { version = "2.0.0", features = ["telethon-sqlite"] }- Pure: it transforms the bytes and values it is given and walks no filesystem beyond the paths passed in.
- No async: every function is synchronous; there is no runtime such as tokio.
- No network: it opens no sockets and speaks no MTProto transport.
- No FFI: it exports a plain Rust API and forbids
unsafecode.
Features
| Feature | Default | Effect |
|---|---|---|
parallel | yes | Runs the batch entry points on a dedicated rayon pool; without it they run sequentially with identical results. |
telethon-sqlite | no | Writes and reads Telethon .session files (session::, read_session_file) with a bundled SQLite. Off by default so the default core links no C code. |
Modules
| Module | Holds |
|---|---|
tdata | TData, a whole folder; account, key_data, map and mtp_config for its files; DrawSource and draw_plan for its random draws |
session | telethon::, mtcute::StringSession, grammers::SessionData, telethon_file and the built-in DC table dc |
api | Client, Versions, ApiProfile, profile, generate, generate_desktop, LAYER, session_json |
devices | Platform, Device, the weighted catalogs, pick and seed_hash |
fingerprint | tz_offset, validate_init_params, check_versions, FingerprintConfig, StrictMode, Issue, msg id helpers |
auth | QR login URL, srp_check, post-login steps, session rows and tables, Consistency and the consistency report, FlowFailure |
crypto | AuthKey, AES-256-IGE and -CTR, the tdata local encryption, SHA-1/256, HMAC-SHA256, PBKDF2, PQ factorisation, random_fill |
kdf | create_local_key, create_, argon2id, scrypt and their caps |
batch | load_many and map_many over independent folders |
compress | gzip, gunzip |
qt, tdf | The Qt stream encoding and the TDF$ container |
tdata::TData
pub const APP_VERSION: u32 = 7_003_001;
pub const NEW_FOLDER_ACTIVE_INDEX: i32 = -1;
pub struct TData {
pub app_version: u32,
pub key_file: String,
pub key_data: KeyData,
pub local_key: AuthKey,
pub passcode_key: AuthKey,
pub accounts: Vec<Account>,
pub active_index: i32,
pub performance_mode: bool,
// ...
}
impl TData {
pub const MAX_ACCOUNTS: usize = 6;
pub fn load(dir: impl AsRef<Path>, passcode: &[u8], key_file: Option<&str>) -> Result<Self>;
pub fn new(accounts: Vec<Account>, passcode: &[u8], performance_mode: bool,
rng: &mut dyn FnMut(&mut [u8])) -> Result<Self>;
pub fn new_with_draws(accounts: Vec<Account>, passcode: &[u8], performance_mode: bool,
active_index: i32, draws: DrawSource) -> Result<Self>;
pub fn save(&self, dir: impl AsRef<Path>, rng: &mut dyn FnMut(&mut [u8])) -> Result<()>;
pub fn save_random(&self, dir: impl AsRef<Path>) -> Result<()>;
pub fn save_with_draws(&self, dir: impl AsRef<Path>, performance_mode: Option<bool>,
draws: DrawSource) -> Result<()>;
pub fn migrate(&mut self, rng: &mut dyn FnMut(&mut [u8])) -> Result<bool>;
pub fn migrate_with_draws(&mut self, draws: DrawSource) -> Result<bool>;
pub fn set_accounts(&mut self, accounts: Vec<Account>) -> Result<()>;
pub fn add_account(&mut self, user_id: u64, main_dc: i32, auth_key: AuthKey) -> Result<i32>;
pub fn set_key_file(&mut self, name: &str) -> Result<()>;
pub fn main_account(&self) -> Option<&Account>;
}| Item | Description |
|---|---|
TData::load | Loads a folder; an empty passcode for none, None for the default key file data. |
TData::new, new_with_draws | A folder for accounts the way SaveTData creates one, drawing from rng or a DrawSource. |
save, save_random, save_with_draws | Write the folder; save_random draws its padding from the OS random source. |
migrate, migrate_with_draws | Install 7.3 key data in place of a pre-7.3 key file; true when they did. |
set_accounts | Replaces the accounts later saves write, keeping the local key. |
add_account | Adds an account at the lowest free index; Error::MaxAccountLimit for a full folder. |
set_key_file | Renames the key file and account files of later saves. |
main_account | The account at the active index, else the first. |
DrawSource::os(), Draw | The OS random source, or recorded draws replayed in order; draw_plan lists the sizes an operation draws. |
Debug of TData and Account prints key ids only, never key material or the passcode.
tdata::account::Account
pub struct Account {
pub index: i32,
pub user_id: u64,
pub main_dc: i32,
pub keys: Vec<(i32, AuthKey)>,
pub keys_to_destroy: Vec<(i32, AuthKey)>,
pub map: MapData,
pub config: Option<Vec<u8>>,
}
impl Account {
pub fn new(index: i32, user_id: u64, main_dc: i32, keys: Vec<(i32, AuthKey)>) -> Result<Self>;
pub fn auth_key(&self) -> Option<&AuthKey>;
pub fn mtp_config(&self) -> MtpConfig;
}Account::new builds an account as it is before saving; Error::AuthKeyNotFound when no key
belongs to main_dc. auth_key is the key of the main DC. mtp_config parses the stored
config, or gives the production default.
Sessions
pub mod telethon {
pub struct StringSession { pub dc_id: u8, pub addr: SocketAddr, pub auth_key: AuthKey }
impl StringSession {
pub fn from_account(account: &Account, ipv6: bool) -> Result<Self>;
pub fn encode(&self) -> String;
pub fn decode(string: &str) -> Result<Self>;
}
}
pub mod mtcute {
pub struct StringSession {
pub primary_dc: DcOption,
pub media_dc: Option<DcOption>,
pub self_user: Option<(u64, bool)>,
pub test_mode: bool,
pub auth_key: AuthKey,
}
impl StringSession {
pub fn from_account(account: &Account, ipv6: bool) -> Result<Self>;
pub fn encode(&self) -> String;
pub fn decode(string: &str) -> Result<Self>;
}
}
pub mod grammers {
pub struct SessionData { pub home_dc: i32, pub dc_options: HashMap<i32, DcOption>, /* ... */ }
impl SessionData {
pub fn from_account(account: &Account) -> Result<Self>;
}
}pub const VERSION: i64 = 8;
pub struct SessionRows {
pub dc_id: i32,
pub server_address: String,
pub port: u16,
pub auth_key: AuthKey,
pub takeout_id: Option<i64>,
}
impl SessionRows {
pub fn from_account(account: &Account, ipv6: bool) -> Result<Self>;
}
pub fn session_path(path: impl AsRef<Path>) -> PathBuf;
pub fn pair_paths(base: impl AsRef<Path>) -> (PathBuf, PathBuf);
pub fn companion_json(profile: &ApiProfile, extra: Option<&Map<String, Value>>) -> Value;
pub fn companion_text(profile: &ApiProfile, extra: Option<&Map<String, Value>>) -> String;
pub fn read_companion(text: &str) -> Result<(ApiProfile, Map<String, Value>)>;
// feature "telethon-sqlite"
pub fn write_session_file(path: impl AsRef<Path>, rows: &SessionRows) -> Result<PathBuf>;
pub fn read_session_file(path: impl AsRef<Path>) -> Result<SessionRows>;Every from_account takes the endpoints of the account’s stored MTP config when it has config
bytes, and the built-in production table of session::dc otherwise (port 443, DCs 1 to 5). The
types hold their key as an AuthKey, whose Debug prints the key id only. The grammers struct
here has no grammers dependency; opentele2-grammers gives grammers’ own SessionData.
api
pub const LAYER: i32 = 230;
pub enum Client { Desktop, Android, AndroidX, Ios, MacOS, WebA, WebK, Webogram }
pub struct Versions { /* the PlatformVersions fields */ }
impl Versions {
pub fn offline() -> Self;
pub fn set_chrome_version(&mut self, version: impl Into<String>);
}
pub struct ApiProfile {
pub api_id: i32,
pub api_hash: String,
pub device_model: String,
pub system_version: String,
pub app_version: String,
pub lang_code: String,
pub system_lang_code: String,
pub lang_pack: String,
}
impl ApiProfile {
pub fn from_session_json(data: &Value) -> Result<Self>;
pub fn to_session_json(&self, extra: Option<&Map<String, Value>>) -> Value;
}
pub fn profile(client: Client, versions: &Versions) -> ApiProfile;
pub fn generate(client: Client, seed: Option<&str>, versions: &Versions) -> Result<ApiProfile>;
pub fn generate_desktop(system: Option<&str>, seed: Option<&str>, versions: &Versions) -> Result<ApiProfile>;
pub fn format_app_version(version: u32) -> String;| Item | Description |
|---|---|
Client | The eight official clients; Client:: and name() map the binding names. |
Versions::offline() | The built-in versions, used when fetching is off. |
profile( | The client’s default profile with the app versions of versions. |
generate( | The default profile with a device picked for seed; Client::Desktop is generate_desktop without a system. |
generate_ | The desktop profile for "windows", "macos" or "linux" (chosen from the seed when absent); the app version gets x64 on Windows and arm64 on macOS. |
ApiProfile | Equality and hashing use api_id, api_hash, device_model, system_version and app_version only. |
session_json | The session JSON field rules (FIELDS, DEFAULT_EXTRA_FIELDS, read, write) untyped, for bindings. |
devices and fingerprint
pub enum Platform { Windows, Linux, MacOS { swift: bool }, Android, Ios, Web { client: WebClient } }
pub struct Device { pub model: String, pub system_version: String }
pub fn pick(platform: Platform, seed: Option<&str>) -> Result<Device>;
pub fn catalog(platform: Platform) -> Result<&'static Catalog>;
pub fn catalog_entries(platform: Platform) -> Result<Vec<CatalogEntry>>;
pub fn seed_hash(seed: &str) -> u64;pub fn tz_offset(utc_offset_seconds: i32) -> f64;
pub fn validate_init_params(profile: &ApiProfile, versions: &Versions, mode: StrictMode) -> Vec<Issue>;
pub fn check_versions(profile: &ApiProfile, versions: &Versions) -> Vec<Issue>;
pub fn issues_message(issues: &[Issue]) -> String;
pub fn layer_mismatch_warning(library_layer: i32) -> Option<String>;
pub fn msg_id_offset(rng: &mut dyn FnMut(&mut [u8])) -> u16;
pub fn is_valid_msg_id(msg_id: i64, from_client: bool, now_unix: i64) -> bool;
pub enum StrictMode { Off, Warn, Strict }
pub struct FingerprintConfig { /* strict_mode, auto_validate, preferred_transport, ... */ }
impl FingerprintConfig {
pub fn from_os_rng() -> Result<Self>;
pub fn effective_layer(&self) -> i32;
pub fn effective_layer_for(&self, library_layer: i32) -> (i32, Option<String>);
pub fn validate(&self, profile: &ApiProfile, versions: &Versions) -> Vec<Issue>;
}pick indexes the expanded weighted catalog with a hash of the seed, so a seed always gives the
same device; without a seed the pick is random. tz_offset takes the local UTC offset from the
caller, so results do not depend on the machine’s clock or zone.
auth
pub fn qr_login_url(token: &[u8]) -> String;
pub fn srp_inputs(srp_id: Option<i64>, srp_b: Option<&[u8]>) -> (i64, Vec<u8>);
pub fn srp_check(params: &SrpParams<'_>, password: &str,
rng: &mut dyn FnMut(&mut [u8])) -> Result<SrpCheck, SrpError>;
pub fn srp_check_random(params: &SrpParams<'_>, password: &str) -> Result<SrpCheck, SrpError>;
pub fn post_login_steps(lang_pack: &str) -> Vec<PostLoginStep>;
pub fn format_sessions(rows: &[SessionRow]) -> String;
pub fn is_official_api_id(api_id: i32) -> bool;
pub fn consistency_report(outcomes: [CheckOutcome; 7]) -> ConsistencyReport;
pub fn consistency_summary(results: &[CheckResult]) -> String;
pub const OFFICIAL_API_IDS: [i32; 6] = [2040, 6, 21724, 10840, 2834, 2496];
pub const QR_LOGIN_ATTEMPTS: u32 = 5;
pub const LAYER_TOLERANCE: u32 = 15;The client-independent parts of login, session admin and the consistency checks; the flows run on
each ecosystem’s own client. ConsistencyExpectations::new(profile, versions) says what each of
the seven checks (Check::ALL) expects, and FlowFailure gives each flow failure its code and
Python exception class.
crypto, kdf and batch
pub struct AuthKey(/* 256 bytes */);
impl AuthKey {
pub const SIZE: usize = 256;
pub const fn new(bytes: [u8; Self::SIZE]) -> Self;
pub fn from_slice(bytes: &[u8]) -> Result<Self>;
pub const fn as_bytes(&self) -> &[u8; Self::SIZE];
pub fn key_id(&self) -> u64;
}
pub fn aes_ige_encrypt(buf: &mut [u8], key: &[u8], iv: &[u8]) -> Result<()>;
pub fn aes_ige_decrypt(buf: &mut [u8], key: &[u8], iv: &[u8]) -> Result<()>;
pub fn decrypt_local(encrypted: &[u8], local_key: &AuthKey) -> Result<Vec<u8>>;
pub fn encrypt_local_random(data: &[u8], local_key: &AuthKey) -> Result<Vec<u8>>;
pub fn sha1(data: &[u8]) -> [u8; 20];
pub fn sha256(data: &[u8]) -> [u8; 32];
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32];
pub fn factorize_pq(pq: &[u8]) -> Result<(Vec<u8>, Vec<u8>)>;
pub fn random_fill(buf: &mut [u8]) -> Result<()>;
pub struct AesCtr(/* ... */);
impl AesCtr {
pub fn new(key: &[u8], iv: &[u8]) -> Result<Self>;
pub fn apply(&mut self, buf: &mut [u8]);
}pub fn create_local_key(passcode: &[u8], salt: &[u8]) -> AuthKey;
pub fn create_legacy_local_key(passcode: &[u8], salt: &[u8]) -> AuthKey;
pub fn scrypt(passcode: &[u8], salt: &[u8], n: u32, r: u32, p: u32) -> Result<[u8; AuthKey::SIZE]>;pub struct LoadRequest { /* dir, passcode, key_file */ }
impl LoadRequest {
pub fn new(dir: impl Into<PathBuf>, passcode: impl Into<Vec<u8>>, key_file: Option<String>) -> Self;
}
pub struct BatchOptions { pub threads: Option<NonZeroUsize>, /* ... */ }
pub fn load_many(requests: &[LoadRequest], options: &BatchOptions) -> Vec<Result<TData>>;load_many returns results in input order; with parallel each call builds its own rayon pool.
Errors
pub type Result<T> = std::result::Result<T, Error>;
pub enum Error {
FileNotFound { file: String },
InvalidMagic { context: MagicContext },
InvalidCheckSum { file: String, computed: [u8; 16] },
BadDecryptKey,
WrongPasscode,
BadEncryptedDataSize { size: usize },
BadDecryptedDataSize { encrypted_size: usize, full_len: usize, decrypted_size: usize },
BadConfigData { context: ConfigFailure },
QDataStreamFailed { context: Option<&'static str> },
ReadMapDataFailed { context: MapReadFailure },
ReadMapDataIncorrectPasscode,
AuthKeyNotFound,
MaxAccountLimit,
SaveFailed,
KeyDataInvalid { context: KeyDataFailure },
BadLength { what: &'static str, expected: usize, actual: usize },
InvalidInput { what: &'static str, reason: String },
Io(std::io::Error),
}
impl Error {
pub fn code(&self) -> &'static str;
pub fn detail_code(&self) -> Option<&'static str>;
pub fn legacy_message(&self) -> String;
pub fn legacy_class(&self) -> &'static str;
}code() is the stable code the bindings map (TDATA_WRONG_PASSCODE, BAD_LENGTH, …): Node
prefixes it with OT_, Python raises the class legacy_class() names. The codes are listed on
the Node errors page.
use opentele2_core::api::{self, Client, Versions};
use opentele2_core::session::telethon;
use opentele2_core::tdata::TData;
fn main() -> opentele2_core::Result<()> {
let tdata = TData::load("tdata", b"", None)?;
let account = tdata.main_account().expect("folder has an account");
let session = telethon::StringSession::from_account(account, false)?;
println!("dc {} at {}", session.dc_id, session.addr);
let device = api::generate(Client::Desktop, Some("account-42"), &Versions::offline())?;
println!("{} {}", device.device_model, device.app_version);
Ok(())
}