Skip to main content

opentele2_core/tdata/
map.rs

1//! The `<folder>/maps` file of one account (`Storage::Account::readMap`).
2//!
3//! The file is a `TDF$` container (see [`crate::tdf`]) whose payload is three
4//! `QByteArray`s: the legacy salt, the legacy local key wrap, and the map
5//! encrypted under the local key. The decrypted map is a stream of entries,
6//! each a `u32` key type followed by a payload whose shape the type decides.
7//! [`MapData::parse_entries`] stops at the first type it does not know and
8//! keeps what it read so far.
9
10use std::collections::{BTreeMap, HashMap};
11use std::path::Path;
12
13use crate::crypto::{self, AuthKey};
14use crate::qt::{QReader, QWriter};
15use crate::tdata::{APP_VERSION, FillRandom, encrypt, write_tdf};
16use crate::{Error, MapReadFailure, Result, tdf};
17
18/// The message of a `maps` file whose three arrays are cut short.
19const MAP_STREAM_FAILED: &str = "Could not stream data from mapData";
20
21/// Name of the map file inside an account folder; on disk `maps`.
22pub const MAP_FILE: &str = "map";
23
24/// `lskDraft`: drafts, as (file key, serialized peer id) pairs.
25pub const LSK_DRAFT: u32 = 0x01;
26/// `lskDraftPosition`: draft cursors, as (file key, serialized peer id) pairs.
27pub const LSK_DRAFT_POSITION: u32 = 0x02;
28/// `lskLegacyImages`: legacy image locations.
29pub const LSK_LEGACY_IMAGES: u32 = 0x03;
30/// `lskLegacyStickerImages`: legacy sticker image locations.
31pub const LSK_LEGACY_STICKER_IMAGES: u32 = 0x05;
32/// `lskLegacyAudios`: legacy audio locations.
33pub const LSK_LEGACY_AUDIOS: u32 = 0x06;
34/// `lskUserSettings`: the user settings file key.
35pub const LSK_USER_SETTINGS: u32 = 0x09;
36/// `lskSelfSerialized`: the serialized self user.
37pub const LSK_SELF_SERIALIZED: u32 = 0x15;
38/// `lskWebviewTokens`: the bots and other web view storage tokens.
39pub const LSK_WEBVIEW_TOKENS: u32 = 0x19;
40/// `lskBotStorages`: bot storages, as (file key, serialized peer id) pairs.
41pub const LSK_BOT_STORAGES: u32 = 0x1D;
42/// `lskWalletEngineStorages`: wallet engine storages, as (file key, name).
43pub const LSK_WALLET_ENGINE_STORAGES: u32 = 0x21;
44
45/// User settings file key in the map of a new account
46/// ([`MapData::new_account`]).
47pub const NEW_ACCOUNT_SETTINGS_KEY: u64 = 1_851_671_142_505_648_812;
48
49/// Number of `u64` file keys stored after `kind`, for the key types that hold
50/// only file keys (the slotted types, the three old skip types, and the two
51/// legacy wallet keys); `None` for every other type.
52#[must_use]
53pub fn file_key_count(kind: u32) -> Option<usize> {
54    match kind {
55        // Locations, recent stickers (old), background (old old), user
56        // settings, recent hashtags, stickers (old), saved peers (old),
57        // report spam statuses (old), saved gifs (old), saved gifs, trusted
58        // bots, faved stickers, export settings, search suggestions, round
59        // placeholder, inline bots downloads, media playback positions,
60        // prefs, wallet key, wallet engine key. The three old skip types and
61        // the two wallet keys are read and dropped.
62        0x04 | 0x07..=0x0F | 0x11..=0x13 | 0x18 | 0x1A..=0x1C | 0x1E..=0x20 => Some(1),
63        // Background (old): day and night.
64        0x14 => Some(2),
65        // Stickers keys: installed, featured, recent, archived.
66        0x10 => Some(4),
67        // Masks keys and custom emoji keys: three each.
68        0x16 | 0x17 => Some(3),
69        _ => None,
70    }
71}
72
73/// One entry of the decrypted map, in file order.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum MapEntry {
76    /// A key type that holds only file keys (see [`file_key_count`]).
77    FileKeys {
78        /// The `lsk` key type.
79        kind: u32,
80        /// The file keys, as many as [`file_key_count`] gives.
81        keys: Vec<u64>,
82    },
83    /// [`LSK_DRAFT`], [`LSK_DRAFT_POSITION`], or [`LSK_BOT_STORAGES`].
84    PeerKeys {
85        /// The `lsk` key type.
86        kind: u32,
87        /// (file key, serialized peer id) pairs in file order.
88        pairs: Vec<(u64, u64)>,
89    },
90    /// [`LSK_LEGACY_IMAGES`], [`LSK_LEGACY_STICKER_IMAGES`], or
91    /// [`LSK_LEGACY_AUDIOS`]: three `u64` and an `i32` per item, which
92    /// [`MapData::encode_entries`] drops.
93    LegacyLocations {
94        /// The `lsk` key type.
95        kind: u32,
96        /// The raw items in file order.
97        items: Vec<(u64, u64, u64, i32)>,
98    },
99    /// [`LSK_SELF_SERIALIZED`]; `None` for a null array.
100    SelfSerialized(Option<Vec<u8>>),
101    /// [`LSK_WEBVIEW_TOKENS`]; `None` for a null array.
102    WebviewTokens {
103        /// Storage token of bot web views.
104        bots: Option<Vec<u8>>,
105        /// Storage token of other web views.
106        other: Option<Vec<u8>>,
107    },
108    /// [`LSK_WALLET_ENGINE_STORAGES`]: (file key, entry name) pairs; `None`
109    /// for a null string.
110    WalletEngineStorages(Vec<(u64, Option<String>)>),
111}
112
113/// A decoded `maps` file.
114#[derive(Debug, Clone, Default, PartialEq, Eq)]
115pub struct MapData {
116    /// Telegram Desktop version stored in the `TDF$` header.
117    pub version: u32,
118    /// The legacy salt; `None` for a null array.
119    pub legacy_salt: Option<Vec<u8>>,
120    /// The legacy local key wrap; `None` for a null array.
121    pub legacy_key_encrypted: Option<Vec<u8>>,
122    /// The decoded entries in file order.
123    pub entries: Vec<MapEntry>,
124    /// The unknown key type the entries stopped at, if any; everything after
125    /// it is ignored.
126    pub stopped_at: Option<u32>,
127}
128
129impl MapData {
130    /// Decodes a decrypted map (the data part of the encrypted blob) into
131    /// its entries and the unknown key type they stopped at.
132    ///
133    /// A key type that cannot be read in full ends the entries like an
134    /// unknown one, since a short value reads as 0 (`lskUserMap`).
135    ///
136    /// # Errors
137    /// [`Error::QDataStreamFailed`] when an entry of a known type is cut
138    /// short or holds an invalid string.
139    pub fn parse_entries(data: &[u8]) -> Result<(Vec<MapEntry>, Option<u32>)> {
140        let mut reader = QReader::new(data);
141        let mut entries = Vec::new();
142        while !reader.is_empty() {
143            let Ok(kind) = reader.read_u32() else {
144                return Ok((entries, Some(0)));
145            };
146            match read_entry(&mut reader, kind)? {
147                Some(entry) => entries.push(entry),
148                None => return Ok((entries, Some(kind))),
149            }
150        }
151        Ok((entries, None))
152    }
153
154    /// Reads and decrypts the `maps` file in the account folder `dir`
155    /// (`MapData.read` with a local key).
156    ///
157    /// # Errors
158    /// - [`Error::ReadMapDataFailed`] when the file is missing or invalid, or
159    ///   the map does not decrypt under `local_key`.
160    /// - [`Error::QDataStreamFailed`] when the three arrays or an entry are
161    ///   cut short.
162    pub fn read(dir: impl AsRef<Path>, local_key: &AuthKey) -> Result<Self> {
163        let file = tdf::read(dir, MAP_FILE).map_err(|_| Error::ReadMapDataFailed {
164            context: MapReadFailure::Open,
165        })?;
166        let mut reader = QReader::new(&file.payload);
167        let mut array = || {
168            reader
169                .read_bytearray()
170                .map_err(|_| Error::QDataStreamFailed {
171                    context: Some(MAP_STREAM_FAILED),
172                })
173        };
174        let legacy_salt = array()?.map(<[u8]>::to_vec);
175        let legacy_key_encrypted = array()?.map(<[u8]>::to_vec);
176        let encrypted = array()?.unwrap_or_default();
177        let map =
178            crypto::decrypt_local(encrypted, local_key).map_err(|_| Error::ReadMapDataFailed {
179                context: MapReadFailure::Decrypt,
180            })?;
181        let (entries, stopped_at) = Self::parse_entries(&map)?;
182        Ok(Self {
183            version: file.version,
184            legacy_salt,
185            legacy_key_encrypted,
186            entries,
187            stopped_at,
188        })
189    }
190
191    /// The map of a new account: only the
192    /// [`NEW_ACCOUNT_SETTINGS_KEY`] user settings key, [`APP_VERSION`], and
193    /// no legacy arrays.
194    #[must_use]
195    pub fn new_account() -> Self {
196        Self {
197            version: APP_VERSION,
198            legacy_salt: None,
199            legacy_key_encrypted: None,
200            entries: vec![MapEntry::FileKeys {
201                kind: LSK_USER_SETTINGS,
202                keys: vec![NEW_ACCOUNT_SETTINGS_KEY],
203            }],
204            stopped_at: None,
205        }
206    }
207
208    /// Encodes the decrypted map, which is not a copy of the entries: they
209    /// are first folded into one field per file key slot.
210    ///
211    /// - File keys land in their named slots, a later entry overwriting an
212    ///   earlier one; `lskStickersOld` fills the installed stickers key and
213    ///   `lskBackgroundOldOld` the day background key. The three old skip
214    ///   types and the two legacy wallet keys are dropped, and so are legacy
215    ///   locations.
216    /// - Draft, draft cursor, and bot storage pairs are keyed by peer id
217    ///   (`PeerId.FromSerialized`, legacy layouts converted); a repeated peer
218    ///   keeps its first position and its last file key. Peers are written
219    ///   with the serialized-id flag set (`PeerId.Serialize`).
220    /// - The self user, the web view tokens, and the wallet engine storages
221    ///   keep the last entry; wallet entries are written sorted by name, a
222    ///   null name as an empty one.
223    ///
224    /// The output follows a fixed type order and writes a key type
225    /// only when one of its keys is non-zero, the self user only when it is
226    /// non-empty, the web view tokens only when one is non-empty, and the
227    /// wallet engine storages only when there is one.
228    ///
229    /// # Errors
230    /// [`Error::QDataStreamFailed`] when a count or blob does not fit a `u32`.
231    pub fn encode_entries(&self) -> Result<Vec<u8>> {
232        MapState::fold(&self.entries).encode()
233    }
234
235    /// Writes the `maps` file into the account folder `dir`
236    /// (`StorageAccount._writeMap`): two null arrays, then the encrypted
237    /// [`MapData::encode_entries`].
238    pub(crate) fn write(
239        &self,
240        dir: &Path,
241        local_key: &AuthKey,
242        rng: &mut FillRandom<'_>,
243    ) -> Result<()> {
244        let entries = self.plaintext()?;
245        let mut payload = QWriter::new();
246        payload.write_bytearray(None)?;
247        payload.write_bytearray(None)?;
248        payload.write_bytearray(Some(&encrypt(&entries, local_key, rng)?))?;
249        write_tdf(dir, MAP_FILE, payload.into_bytes())
250    }
251
252    /// The bytes [`MapData::write`] encrypts: [`MapData::encode_entries`],
253    /// refused with [`Error::SaveFailed`] when empty.
254    pub(crate) fn plaintext(&self) -> Result<Vec<u8>> {
255        let entries = self.encode_entries()?;
256        if entries.is_empty() {
257            // An empty map has no buffer to encrypt.
258            return Err(Error::SaveFailed);
259        }
260        Ok(entries)
261    }
262}
263
264/// File key slots of the folded map, one per named file key.
265mod slot {
266    pub const PREFS: usize = 0;
267    pub const LOCATIONS: usize = 1;
268    pub const TRUSTED_BOTS: usize = 2;
269    pub const RECENT_STICKERS_OLD: usize = 3;
270    pub const INSTALLED_STICKERS: usize = 4;
271    pub const FEATURED_STICKERS: usize = 5;
272    pub const RECENT_STICKERS: usize = 6;
273    pub const FAVED_STICKERS: usize = 7;
274    pub const ARCHIVED_STICKERS: usize = 8;
275    pub const SAVED_GIFS: usize = 9;
276    pub const INSTALLED_MASKS: usize = 10;
277    pub const RECENT_MASKS: usize = 11;
278    pub const ARCHIVED_MASKS: usize = 12;
279    pub const INSTALLED_CUSTOM_EMOJI: usize = 13;
280    pub const FEATURED_CUSTOM_EMOJI: usize = 14;
281    pub const ARCHIVED_CUSTOM_EMOJI: usize = 15;
282    pub const SEARCH_SUGGESTIONS: usize = 16;
283    pub const BACKGROUND_DAY: usize = 17;
284    pub const BACKGROUND_NIGHT: usize = 18;
285    pub const USER_SETTINGS: usize = 19;
286    pub const RECENT_HASHTAGS: usize = 20;
287    pub const EXPORT_SETTINGS: usize = 21;
288    pub const ROUND_PLACEHOLDER: usize = 22;
289    pub const INLINE_BOTS_DOWNLOADS: usize = 23;
290    pub const MEDIA_PLAYBACK_POSITIONS: usize = 24;
291    pub const COUNT: usize = 25;
292}
293
294/// The slots a file-key type reads into, in stream order; empty for the
295/// types that are dropped.
296fn key_slots(kind: u32) -> &'static [usize] {
297    match kind {
298        0x1E => &[slot::PREFS],
299        0x04 => &[slot::LOCATIONS],
300        0x11 => &[slot::TRUSTED_BOTS],
301        0x07 => &[slot::RECENT_STICKERS_OLD],
302        0x08 => &[slot::BACKGROUND_DAY],
303        LSK_USER_SETTINGS => &[slot::USER_SETTINGS],
304        0x0A => &[slot::RECENT_HASHTAGS],
305        0x0B => &[slot::INSTALLED_STICKERS],
306        0x12 => &[slot::FAVED_STICKERS],
307        0x0F => &[slot::SAVED_GIFS],
308        0x13 => &[slot::EXPORT_SETTINGS],
309        0x18 => &[slot::SEARCH_SUGGESTIONS],
310        0x14 => &[slot::BACKGROUND_DAY, slot::BACKGROUND_NIGHT],
311        0x10 => &[
312            slot::INSTALLED_STICKERS,
313            slot::FEATURED_STICKERS,
314            slot::RECENT_STICKERS,
315            slot::ARCHIVED_STICKERS,
316        ],
317        0x16 => &[
318            slot::INSTALLED_MASKS,
319            slot::RECENT_MASKS,
320            slot::ARCHIVED_MASKS,
321        ],
322        0x17 => &[
323            slot::INSTALLED_CUSTOM_EMOJI,
324            slot::FEATURED_CUSTOM_EMOJI,
325            slot::ARCHIVED_CUSTOM_EMOJI,
326        ],
327        0x1A => &[slot::ROUND_PLACEHOLDER],
328        0x1B => &[slot::INLINE_BOTS_DOWNLOADS],
329        0x1C => &[slot::MEDIA_PLAYBACK_POSITIONS],
330        _ => &[],
331    }
332}
333
334/// `UserId::kReservedBit << 48`: set in every modern serialized peer id.
335const PEER_SERIALIZED_FLAG: u64 = 0x80 << 48;
336
337/// `PeerId.FromSerialized`: the peer id of a serialized one, converting the
338/// legacy layout (type in bits 32..36); an unknown legacy type is peer 0.
339fn peer_from_serialized(serialized: u64) -> u64 {
340    if serialized & PEER_SERIALIZED_FLAG != 0 {
341        return serialized & !PEER_SERIALIZED_FLAG;
342    }
343    let bare = serialized & 0xFFFF_FFFF;
344    match serialized & 0xF_0000_0000 {
345        0 => bare,
346        0x1_0000_0000 => bare | (1 << 48),
347        0x2_0000_0000 => bare | (2 << 48),
348        0xF_0000_0000 => bare | (0x7F << 48),
349        _ => 0,
350    }
351}
352
353/// A Python dict from peer id to file key: insertion order, last value wins.
354#[derive(Default)]
355struct PeerMap {
356    pairs: Vec<(u64, u64)>,
357    index: HashMap<u64, usize>,
358}
359
360impl PeerMap {
361    fn insert(&mut self, peer: u64, file_key: u64) {
362        if let Some(&at) = self.index.get(&peer) {
363            self.pairs[at].1 = file_key;
364        } else {
365            self.index.insert(peer, self.pairs.len());
366            self.pairs.push((peer, file_key));
367        }
368    }
369
370    fn encode(&self, writer: &mut QWriter, kind: u32) -> Result<()> {
371        if self.pairs.is_empty() {
372            return Ok(());
373        }
374        let count = u32::try_from(self.pairs.len())
375            .map_err(|_| Error::QDataStreamFailed { context: None })?;
376        writer.write_u32(kind);
377        writer.write_u32(count);
378        for &(peer, file_key) in &self.pairs {
379            writer.write_u64(file_key);
380            writer.write_u64(peer | PEER_SERIALIZED_FLAG);
381        }
382        Ok(())
383    }
384}
385
386/// The map folded into one field per file key slot, as
387/// [`MapData::encode_entries`] writes it.
388struct MapState<'a> {
389    keys: [u64; slot::COUNT],
390    self_serialized: Option<&'a [u8]>,
391    drafts: PeerMap,
392    draft_positions: PeerMap,
393    bot_storages: PeerMap,
394    // Both start null, which is never written.
395    webview_bots: Option<&'a [u8]>,
396    webview_other: Option<&'a [u8]>,
397    wallet: BTreeMap<&'a str, u64>,
398}
399
400impl<'a> MapState<'a> {
401    fn fold(entries: &'a [MapEntry]) -> Self {
402        let mut state = Self {
403            keys: [0; slot::COUNT],
404            self_serialized: None,
405            drafts: PeerMap::default(),
406            draft_positions: PeerMap::default(),
407            bot_storages: PeerMap::default(),
408            webview_bots: None,
409            webview_other: None,
410            wallet: BTreeMap::new(),
411        };
412        for entry in entries {
413            match entry {
414                MapEntry::FileKeys { kind, keys } => {
415                    for (&at, &key) in key_slots(*kind).iter().zip(keys) {
416                        state.keys[at] = key;
417                    }
418                }
419                MapEntry::PeerKeys { kind, pairs } => {
420                    let target = match *kind {
421                        LSK_DRAFT => &mut state.drafts,
422                        LSK_DRAFT_POSITION => &mut state.draft_positions,
423                        LSK_BOT_STORAGES => &mut state.bot_storages,
424                        _ => continue,
425                    };
426                    for &(file_key, peer) in pairs {
427                        target.insert(peer_from_serialized(peer), file_key);
428                    }
429                }
430                MapEntry::LegacyLocations { .. } => {}
431                MapEntry::SelfSerialized(blob) => state.self_serialized = blob.as_deref(),
432                MapEntry::WebviewTokens { bots, other } => {
433                    state.webview_bots = bots.as_deref();
434                    state.webview_other = other.as_deref();
435                }
436                MapEntry::WalletEngineStorages(items) => {
437                    state.wallet = items
438                        .iter()
439                        .map(|(file_key, name)| (name.as_deref().unwrap_or_default(), *file_key))
440                        .collect();
441                }
442            }
443        }
444        state
445    }
446
447    /// `_writeKeyIfSet` / `_writeKeyGroupIfAnySet` for `kind`.
448    fn encode_keys(&self, writer: &mut QWriter, kind: u32) {
449        let slots = key_slots(kind);
450        if slots.iter().any(|&at| self.keys[at] != 0) {
451            writer.write_u32(kind);
452            for &at in slots {
453                writer.write_u64(self.keys[at]);
454            }
455        }
456    }
457
458    fn encode(&self) -> Result<Vec<u8>> {
459        let mut writer = QWriter::new();
460        if let Some(blob) = self.self_serialized.filter(|blob| !blob.is_empty()) {
461            writer.write_u32(LSK_SELF_SERIALIZED);
462            writer.write_bytearray(Some(blob))?;
463        }
464        // Prefs.
465        self.encode_keys(&mut writer, 0x1E);
466        self.drafts.encode(&mut writer, LSK_DRAFT)?;
467        self.draft_positions
468            .encode(&mut writer, LSK_DRAFT_POSITION)?;
469        self.bot_storages.encode(&mut writer, LSK_BOT_STORAGES)?;
470        // Locations, trusted bots, recent stickers (old), stickers keys,
471        // faved stickers, saved gifs, user settings, recent hashtags, export
472        // settings, masks keys, custom emoji keys, search suggestions.
473        for kind in [
474            0x04, 0x11, 0x07, 0x10, 0x12, 0x0F, 0x09, 0x0A, 0x13, 0x16, 0x17, 0x18,
475        ] {
476            self.encode_keys(&mut writer, kind);
477        }
478        let non_empty = |token: Option<&[u8]>| token.is_some_and(|t| !t.is_empty());
479        if non_empty(self.webview_bots) || non_empty(self.webview_other) {
480            writer.write_u32(LSK_WEBVIEW_TOKENS);
481            writer.write_bytearray(self.webview_bots)?;
482            writer.write_bytearray(self.webview_other)?;
483        }
484        // Round placeholder, inline bots downloads, media playback positions.
485        for kind in [0x1A, 0x1B, 0x1C] {
486            self.encode_keys(&mut writer, kind);
487        }
488        if !self.wallet.is_empty() {
489            let count = u32::try_from(self.wallet.len())
490                .map_err(|_| Error::QDataStreamFailed { context: None })?;
491            writer.write_u32(LSK_WALLET_ENGINE_STORAGES);
492            writer.write_u32(count);
493            for (&name, &file_key) in &self.wallet {
494                writer.write_u64(file_key);
495                writer.write_qstring(Some(name))?;
496            }
497        }
498        // Background (old): day and night.
499        self.encode_keys(&mut writer, 0x14);
500        Ok(writer.into_bytes())
501    }
502}
503
504/// Reads `count: u32` items with `item`; the count is never preallocated,
505/// so a forged count fails at the first short read.
506fn read_counted<'a, T>(
507    reader: &mut QReader<'a>,
508    mut item: impl FnMut(&mut QReader<'a>) -> Result<T>,
509) -> Result<Vec<T>> {
510    let count = reader.read_u32()?;
511    let mut items = Vec::new();
512    for _ in 0..count {
513        items.push(item(reader)?);
514    }
515    Ok(items)
516}
517
518fn owned(blob: Option<&[u8]>) -> Option<Vec<u8>> {
519    blob.map(<[u8]>::to_vec)
520}
521
522/// Reads the payload of one entry; `None` for an unknown key type.
523fn read_entry(reader: &mut QReader<'_>, kind: u32) -> Result<Option<MapEntry>> {
524    let entry = match kind {
525        LSK_DRAFT | LSK_DRAFT_POSITION | LSK_BOT_STORAGES => MapEntry::PeerKeys {
526            kind,
527            pairs: read_counted(reader, |r| Ok((r.read_u64()?, r.read_u64()?)))?,
528        },
529        LSK_LEGACY_IMAGES | LSK_LEGACY_STICKER_IMAGES | LSK_LEGACY_AUDIOS => {
530            MapEntry::LegacyLocations {
531                kind,
532                items: read_counted(reader, |r| {
533                    Ok((r.read_u64()?, r.read_u64()?, r.read_u64()?, r.read_i32()?))
534                })?,
535            }
536        }
537        LSK_SELF_SERIALIZED => MapEntry::SelfSerialized(owned(reader.read_bytearray()?)),
538        LSK_WEBVIEW_TOKENS => MapEntry::WebviewTokens {
539            bots: owned(reader.read_bytearray()?),
540            other: owned(reader.read_bytearray()?),
541        },
542        LSK_WALLET_ENGINE_STORAGES => MapEntry::WalletEngineStorages(read_counted(reader, |r| {
543            Ok((r.read_u64()?, r.read_qstring()?))
544        })?),
545        _ => {
546            let Some(count) = file_key_count(kind) else {
547                return Ok(None);
548            };
549            let mut keys = Vec::with_capacity(count);
550            for _ in 0..count {
551                keys.push(reader.read_u64()?);
552            }
553            MapEntry::FileKeys { kind, keys }
554        }
555    };
556    Ok(Some(entry))
557}