Validate a fingerprint
Before a session connects, check that what it will announce in initConnection looks like the
official client it claims to be: an official api_id for the lang_pack, a believable device,
a current app version and well-formed language codes. Then get the values an official client adds
around those fields: tz_offset, the msg_id offset, the layer and the transport.
Validation is local: it checks values, sends nothing and needs no account. The consistency checks are the live counterpart, asking the server how it sees a session.
Build a profile
Start from an official profile, or from one with a device generated for the account. See API profiles and devices.
from opentele2.api import API
api = API.TelegramDesktop.Generate("windows", "my-id")
print(api.api_id, api.device_model, api.system_version, api.app_version, api.lang_pack)import { generate } from 'opentele2'
const api = generate('TelegramDesktop', 'my-id', 'windows')
console.log(api.apiId, api.deviceModel, api.systemVersion, api.appVersion, api.langPack)use opentele2_core::api::{self, Versions};
fn main() -> opentele2_core::Result<()> {
let versions = Versions::offline();
let api = api::generate_desktop(Some("windows"), Some("my-id"), &versions)?;
println!("{} {} {}", api.api_id, api.device_model, api.lang_pack);
Ok(())
}Validate the initConnection parameters
Validation always checks the lang pack, the language codes and that the device and app fields
are not empty. Strict mode adds the official api_id of the lang_pack and the app version
against the known versions.
from opentele2.api import API
from opentele2.fingerprint import validate_init_connection_params
api = API.TelegramDesktop.Generate("windows", "my-id")
print(validate_init_connection_params(
api.api_id, api.device_model, api.system_version, api.app_version,
api.system_lang_code, api.lang_pack, api.lang_code,
strict=True,
)) # []
print(validate_init_connection_params(
1, "Desktop", "Windows 11 x64", "7.3.1 x64", "en-US", "tdesktop", "en", strict=True
)) # ["api_id 1 does not match lang_pack 'tdesktop' (expected 2040)"]validate_init_connection_params returns the messages. For the issue records, use
_native.validate_init_params(profile, versions, mode) on a _native.profile(...) dict.
import { generate, profile, validateInitParams } from 'opentele2'
console.log(validateInitParams(generate('TelegramDesktop', 'my-id', 'windows'), null, 'strict')) // []
const wrong = { ...profile('TelegramDesktop'), apiId: 1 }
console.log(validateInitParams(wrong, null, 'strict'))
// [{ kind: 'ApiIdMismatch', severity: 'error', message: "api_id 1 does not match lang_pack 'tdesktop' (expected 2040)" }]The second argument is the versions to compare with; null uses the built-in set.
use opentele2_core::api::{self, Client, Versions};
use opentele2_core::fingerprint::{self, StrictMode};
fn main() {
let versions = Versions::offline();
let mut desktop = api::profile(Client::Desktop, &versions);
desktop.api_id = 1;
let issues = fingerprint::validate_init_params(&desktop, &versions, StrictMode::Strict);
for issue in &issues {
println!("{:?} {:?} {}", issue.kind, issue.severity, issue.message);
}
}Read the issues
Every issue has a kind, a severity and a message worded the same in all three packages.
| Kind | Checked | Means |
|---|---|---|
UnknownLangPack | always | lang_pack is not an official pack |
ShortLangCode | always | lang_code has fewer than two characters |
MissingRegion | always | an Android or iOS system_lang_code without a region (en instead of en-US) |
EmptyDeviceModel, EmptySystemVersion, EmptyAppVersion | always | a device or app field is empty |
ApiIdMismatch | strict | api_id is not the official id of the lang_pack |
AppVersionMajor | strict | the app version’s major differs from the known one |
AndroidSdkRange | strict | the Android SDK level is outside the known range |
The mode sets the severity, and in Python what FingerprintConfig.validate_params does: off
gives notes, warn (the default) warnings through warnings.warn, strict errors raised as
ValueError. issues_message / issuesMessage joins issues into that text.
from opentele2.fingerprint import FingerprintConfig, StrictMode
params = dict(
api_id=1, device_model="Desktop", system_version="Windows 11 x64",
app_version="7.3.1 x64", system_lang_code="en-US", lang_pack="tdesktop", lang_code="en",
)
try:
FingerprintConfig(strict_mode=StrictMode.STRICT).validate_params(**params)
except ValueError as error:
print(error)
# initConnection fingerprint issues:
# - api_id 1 does not match lang_pack 'tdesktop' (expected 2040)The opentele2 TelegramClient runs validate_params with its fingerprint_config when it is
built, so in warn mode a bad profile shows up as a warning. To stop before connecting, call
validate_params in strict mode yourself, as above.
import { fingerprintConfig, generate, issuesMessage, validateFingerprint } from 'opentele2'
const config = fingerprintConfig({ strictMode: 'strict' })
const issues = validateFingerprint(config, { ...generate('TelegramDesktop', 'my-id'), langCode: 'e' })
if (issues.length > 0) throw new Error(issuesMessage(issues))
// lang_code 'e' looks invalid (too short)use opentele2_core::api::{self, Client, Versions};
use opentele2_core::fingerprint::{self, FingerprintConfig, FingerprintConfigOptions, StrictMode};
fn main() -> opentele2_core::Result<()> {
let versions = Versions::offline();
let options = FingerprintConfigOptions {
strict_mode: Some(StrictMode::Strict),
..FingerprintConfigOptions::default()
};
let config = FingerprintConfig::from_options(options)?;
let issues = config.validate(&api::profile(Client::Android, &versions), &versions);
if !issues.is_empty() {
eprintln!("{}", fingerprint::issues_message(&issues));
}
Ok(())
}Strict validation compares the app version with a versions record. Validate a profile against
the versions it was built with; see Live client versions.
check_versions / checkVersions runs that comparison alone.
Get tz_offset
Telegram Desktop sends the local UTC offset in seconds as params.tz_offset, wrapped by whole days
into −12 h … +14 h and rounded to a multiple of 900 seconds.
from opentele2 import _native
from opentele2.fingerprint import tdesktop_tz_offset
print(tdesktop_tz_offset()) # this machine's offset, for example 10800.0
print(_native.tz_offset(3 * 3600 + 1000)) # 11700.0The opentele2 TelegramClient sends it for the tdesktop lang pack and recomputes it on every
connect().
import { tzOffset } from 'opentele2'
console.log(tzOffset(-new Date().getTimezoneOffset() * 60)) // this machine's offset
console.log(tzOffset(3 * 3600 + 1000)) // 11700initConnectionOptions from opentele2/mtcute puts it in params for you.
fn main() {
println!("{}", opentele2_core::fingerprint::tz_offset(3 * 3600 + 1000)); // 11700
}grammers 0.11 sends no params, so a grammers session has no tz_offset on the wire.
Get a msg_id offset
Official clients add a small random offset to their message ids. Draw one in 0..=0xFFFF from the
operating system’s random source, and check whether an id is plausible: positive, odd from the
client and even from the server, with its upper 32 bits within 300 seconds of now.
import time
from opentele2.fingerprint import generate_msg_id_offset, is_valid_msg_id
print(generate_msg_id_offset()) # 0..65535; random.seed does not reproduce it
msg_id = (int(time.time()) << 32) | 1
print(is_valid_msg_id(msg_id), is_valid_msg_id(msg_id, from_client=False)) # True FalseFingerprintConfig draws its own offset on construction (randomize_msg_id_offset, on by default).
import { isValidMsgId, msgIdOffset } from 'opentele2'
console.log(msgIdOffset()) // 0..65535
const now = Math.floor(Date.now() / 1000)
const msgId = (BigInt(now) << 32n) | 1n
console.log(isValidMsgId(msgId, true, now), isValidMsgId(msgId, false, now)) // true falseuse std::time::{SystemTime, UNIX_EPOCH};
use opentele2_core::fingerprint;
fn main() -> opentele2_core::Result<()> {
println!("{}", fingerprint::msg_id_offset_for_os()?);
let now = SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_secs() as i64;
let msg_id = (now << 32) | 1;
println!("{}", fingerprint::is_valid_msg_id(msg_id, true, now)); // true
Ok(())
}Check the layer and the transport
opentele2 advertises layer 230, the schema of Telegram Desktop 7.3.1. Every official client speaks the obfuscated transport.
from opentele2.fingerprint import (
TransportRecommendation,
get_advertised_layer,
get_recommended_layer,
)
print(get_advertised_layer()) # 230
print(get_recommended_layer()) # the installed Telethon's layer; warns when far from 230
print(TransportRecommendation.get_connection_class().__name__) # ConnectionTcpObfuscated
print(list(TransportRecommendation.get_available_transports()))
# ['full', 'abridged', 'intermediate', 'obfuscated']The opentele2 TelegramClient uses ConnectionTcpObfuscated unless you pass connection=.
FingerprintConfig(layer_override=...) replaces the layer get_effective_layer() reports; a
mismatched layer breaks deserialisation.
The transport recommendation is part of the Python package only: it picks a Telethon connection class. In Node and Rust, configure the transport of mtcute or grammers directly; fingerprintConfig() and FingerprintConfig carry preferredTransport / preferred_transport, 'obfuscated' by default. The layer is LAYER in Node and api::LAYER in Rust.
The transport recommendation is part of the Python package only: it picks a Telethon connection class. In Node and Rust, configure the transport of mtcute or grammers directly; fingerprintConfig() and FingerprintConfig carry preferredTransport / preferred_transport, 'obfuscated' by default. The layer is LAYER in Node and api::LAYER in Rust.
Next
- initConnection fingerprints: every field and check explained.
- Consistency and sessions: check a live session.
- Reference: Python · Node · Rust.