Skip to main content

opentele2_core/tdata/
key_data.rs

1//! The `key_<dataName>` file of Telegram Desktop 7.3 (`Storage::Domain`).
2//!
3//! The file is a `TDF$` container (see [`crate::tdf`]) whose payload is a
4//! `QDataStream` of `QByteArray` blobs:
5//!
6//! 1. the open salt, [`LOCAL_ENCRYPT_SALT_SIZE`] bytes;
7//! 2. the local key wrapped under `create_local_key("", salt)` (or, before
8//!    7.3, under the passcode), null when the app lock is on;
9//! 3. the accounts info encrypted under the local key (see [`AccountsInfo`]);
10//! 4. since 7.3, an optional trailer holding the passcode wraps
11//!    ([`KeyDataTrailer`]). Files without it are the legacy three-blob layout.
12//!
13//! The trailer is itself a `QDataStream`: magic [`KEY_DATA_MAGIC`], format
14//! version, committed generation, wrap count, then per wrap the KDF kind,
15//! generation, memory, time, and parallelism as `u32`, followed by the wrap
16//! salt and the wrapped local key as `QByteArray`s.
17
18use crate::crypto::{self, AuthKey};
19use crate::kdf;
20use crate::qt::{QReader, QWriter};
21use crate::tdata::{FillRandom, random_bytes};
22use crate::{Error, KeyDataFailure, Result};
23
24/// Default `dataName` of a tdata folder; the key file is `key_data`.
25pub const DEFAULT_KEY_FILE: &str = "data";
26
27/// Magic number that starts the 7.3 trailer.
28pub const KEY_DATA_MAGIC: u32 = 0x4B44_3200;
29/// Trailer format version this crate writes and fully understands.
30pub const KEY_DATA_FORMAT_VERSION: u32 = 2;
31/// Highest wrap count accepted for a format up to [`KEY_DATA_FORMAT_VERSION`].
32pub const MAX_WRAP_COUNT: u32 = 2;
33/// Sanity bound on the wrap count of a newer trailer format, whose known
34/// prefix is still read.
35pub const MAX_NEWER_FORMAT_WRAP_COUNT: u32 = 16;
36/// Size of the open salt, in bytes.
37pub const LOCAL_ENCRYPT_SALT_SIZE: usize = 32;
38
39/// KDF kind of the PBKDF2 local key; never valid inside a passcode wrap.
40pub const KDF_PBKDF2: u32 = 0;
41/// KDF kind of an Argon2id passcode wrap.
42pub const KDF_ARGON2ID: u32 = 1;
43/// KDF kind of a scrypt passcode wrap.
44pub const KDF_SCRYPT: u32 = 2;
45
46/// Argon2id memory cost [`default_passcode_kdf`] writes, in KiB.
47pub const ARGON2_MEMORY_KIB: u32 = 65_536;
48/// Argon2id time cost [`default_passcode_kdf`] writes.
49pub const ARGON2_TIME: u32 = 8;
50/// Argon2id lane count [`default_passcode_kdf`] writes.
51pub const ARGON2_LANES: u32 = 1;
52/// Generation [`default_passcode_kdf`] gives a fresh wrap.
53pub const DEFAULT_WRAP_GENERATION: u32 = 1;
54
55/// The name of the key file for `key_file`, for example `key_data`.
56#[must_use]
57pub fn file_name(key_file: &str) -> String {
58    format!("key_{key_file}")
59}
60
61/// Parameters of one passcode wrap.
62///
63/// For Argon2id `memory` is the memory cost in KiB, `time` the pass count,
64/// and `parallelism` the lane count; for scrypt they are `N`, `r`, and `p`.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct PasscodeKdf {
67    /// KDF family: [`KDF_ARGON2ID`] or [`KDF_SCRYPT`].
68    pub kind: u32,
69    /// Generation of the wrap; only wraps of the committed generation are live.
70    pub generation: u32,
71    /// Argon2id memory in KiB, or scrypt `N`.
72    pub memory: u32,
73    /// Argon2id passes, or scrypt `r`.
74    pub time: u32,
75    /// Argon2id lanes, or scrypt `p`.
76    pub parallelism: u32,
77}
78
79impl PasscodeKdf {
80    /// Whether the parameters describe a derivation the KDF accepts.
81    #[must_use]
82    pub fn valid(&self) -> bool {
83        if self.time == 0 || self.parallelism == 0 {
84            return false;
85        }
86        match self.kind {
87            KDF_ARGON2ID => u64::from(self.memory) >= 8 * u64::from(self.parallelism),
88            KDF_SCRYPT => self.memory >= 2 && self.memory.is_power_of_two(),
89            _ => false,
90        }
91    }
92
93    /// Whether the cost is inside the read caps of [`crate::kdf`]; other
94    /// kinds have no cost and are always within limits.
95    #[must_use]
96    pub fn cost_within_limits(&self) -> bool {
97        match self.kind {
98            KDF_ARGON2ID => {
99                self.memory <= kdf::ARGON2_MAX_MEMORY_KIB
100                    && self.time <= kdf::ARGON2_MAX_TIME
101                    && self.parallelism <= kdf::ARGON2_MAX_LANES
102            }
103            // Stricter than tdesktop, which bounds scrypt only by memory.
104            KDF_SCRYPT => {
105                self.time <= kdf::SCRYPT_MAX_R
106                    && self.parallelism <= kdf::SCRYPT_MAX_P
107                    && self.memory <= kdf::SCRYPT_MAX_N
108            }
109            _ => true,
110        }
111    }
112}
113
114/// The KDF Telegram Desktop picks for a new passcode wrap: Argon2id with
115/// [`ARGON2_MEMORY_KIB`], [`ARGON2_TIME`], [`ARGON2_LANES`], generation
116/// [`DEFAULT_WRAP_GENERATION`].
117#[must_use]
118pub fn default_passcode_kdf() -> PasscodeKdf {
119    PasscodeKdf {
120        kind: KDF_ARGON2ID,
121        generation: DEFAULT_WRAP_GENERATION,
122        memory: ARGON2_MEMORY_KIB,
123        time: ARGON2_TIME,
124        parallelism: ARGON2_LANES,
125    }
126}
127
128/// One passcode wrap of the local key.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct PasscodeWrap {
131    /// KDF parameters and generation.
132    pub kdf: PasscodeKdf,
133    /// Salt of the passcode derivation, at least
134    /// [`kdf::PASSCODE_SALT_MIN_SIZE`] bytes.
135    pub salt: Vec<u8>,
136    /// The local key encrypted under the passcode key.
137    pub key_encrypted: Vec<u8>,
138}
139
140/// The 7.3 trailer: the committed generation and the passcode wraps.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct KeyDataTrailer {
143    /// Generation whose wraps are live; others are crash leftovers.
144    pub committed: u32,
145    /// Every wrap in file order, live or not.
146    pub wraps: Vec<PasscodeWrap>,
147}
148
149impl KeyDataTrailer {
150    /// Decodes the trailer blob (`ReadKeyData`).
151    ///
152    /// A newer format is read by its known prefix with up to
153    /// [`MAX_NEWER_FORMAT_WRAP_COUNT`] wraps. A wrap whose cost is above the
154    /// read caps is kept; [`open_local_key`] refuses it later.
155    ///
156    /// # Errors
157    /// - [`Error::QDataStreamFailed`] when the blob is cut short.
158    /// - [`Error::KeyDataInvalid`] for a bad magic, a wrap count above the
159    ///   format limit, an invalid KDF (kind 0 included), a wrap salt shorter
160    ///   than [`kdf::PASSCODE_SALT_MIN_SIZE`], or a duplicate generation.
161    pub fn parse(blob: &[u8]) -> Result<Self> {
162        let mut reader = QReader::new(blob);
163        let [magic, format, committed, count] = streamed(
164            (|| {
165                Ok([
166                    reader.read_u32()?,
167                    reader.read_u32()?,
168                    reader.read_u32()?,
169                    reader.read_u32()?,
170                ])
171            })(),
172            TRAILER_HEADER_STREAM_FAILED,
173        )?;
174        if magic != KEY_DATA_MAGIC {
175            return Err(invalid(KeyDataFailure::BadTrailerMagic));
176        }
177        let limit = if format > KEY_DATA_FORMAT_VERSION {
178            MAX_NEWER_FORMAT_WRAP_COUNT
179        } else {
180            MAX_WRAP_COUNT
181        };
182        if count > limit {
183            return Err(invalid(KeyDataFailure::BadWrapCount { count }));
184        }
185
186        let mut wraps: Vec<PasscodeWrap> = Vec::new();
187        for _ in 0..count {
188            let (kdf, salt, key_encrypted) = streamed(
189                (|| {
190                    let kdf = PasscodeKdf {
191                        kind: reader.read_u32()?,
192                        generation: reader.read_u32()?,
193                        memory: reader.read_u32()?,
194                        time: reader.read_u32()?,
195                        parallelism: reader.read_u32()?,
196                    };
197                    let salt = reader.read_bytearray()?.unwrap_or_default().to_vec();
198                    let key_encrypted = reader.read_bytearray()?.unwrap_or_default().to_vec();
199                    Ok((kdf, salt, key_encrypted))
200                })(),
201                WRAP_STREAM_FAILED,
202            )?;
203            // An over-cost wrap only makes the passcode unusable; a malformed
204            // one fails the whole file.
205            if kdf.cost_within_limits() && !kdf.valid() {
206                return Err(invalid(KeyDataFailure::BadKdfFamily { kind: kdf.kind }));
207            }
208            if salt.len() < kdf::PASSCODE_SALT_MIN_SIZE {
209                return Err(invalid(KeyDataFailure::BadWrapSaltSize {
210                    size: salt.len(),
211                }));
212            }
213            if wraps.iter().any(|w| w.kdf.generation == kdf.generation) {
214                return Err(invalid(KeyDataFailure::DuplicateWrapGeneration {
215                    generation: kdf.generation,
216                }));
217            }
218            wraps.push(PasscodeWrap {
219                kdf,
220                salt,
221                key_encrypted,
222            });
223        }
224        Ok(Self { committed, wraps })
225    }
226
227    /// Encodes the trailer blob in format [`KEY_DATA_FORMAT_VERSION`]
228    /// (`WriteKeyData`).
229    ///
230    /// # Errors
231    /// [`Error::QDataStreamFailed`] when a count or blob does not fit a `u32`.
232    pub fn encode(&self) -> Result<Vec<u8>> {
233        let count = u32::try_from(self.wraps.len())
234            .map_err(|_| Error::QDataStreamFailed { context: None })?;
235        let mut writer = QWriter::new();
236        writer.write_u32(KEY_DATA_MAGIC);
237        writer.write_u32(KEY_DATA_FORMAT_VERSION);
238        writer.write_u32(self.committed);
239        writer.write_u32(count);
240        for wrap in &self.wraps {
241            writer.write_u32(wrap.kdf.kind);
242            writer.write_u32(wrap.kdf.generation);
243            writer.write_u32(wrap.kdf.memory);
244            writer.write_u32(wrap.kdf.time);
245            writer.write_u32(wrap.kdf.parallelism);
246            writer.write_bytearray(Some(&wrap.salt))?;
247            writer.write_bytearray(Some(&wrap.key_encrypted))?;
248        }
249        Ok(writer.into_bytes())
250    }
251
252    /// Wraps of the committed generation.
253    pub fn live_wraps(&self) -> impl Iterator<Item = &PasscodeWrap> {
254        self.wraps
255            .iter()
256            .filter(move |w| w.kdf.generation == self.committed)
257    }
258}
259
260/// The decoded payload of a key file.
261#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct KeyData {
263    /// The open salt, [`LOCAL_ENCRYPT_SALT_SIZE`] bytes.
264    pub salt: Vec<u8>,
265    /// The open wrap of the local key; `None` when stored as a null array,
266    /// which 7.3 writes while the app lock is on.
267    pub key_encrypted: Option<Vec<u8>>,
268    /// The accounts info encrypted under the local key.
269    pub info_encrypted: Vec<u8>,
270    /// The 7.3 trailer; `None` for the legacy three-blob layout.
271    pub trailer: Option<KeyDataTrailer>,
272}
273
274impl KeyData {
275    /// Decodes a key file payload (the bytes inside its `TDF$` container).
276    ///
277    /// # Errors
278    /// - [`Error::QDataStreamFailed`] when a blob is cut short.
279    /// - [`Error::KeyDataInvalid`] when the salt is not
280    ///   [`LOCAL_ENCRYPT_SALT_SIZE`] bytes or the trailer is malformed (see
281    ///   [`KeyDataTrailer::parse`]).
282    pub fn parse(payload: &[u8]) -> Result<Self> {
283        let mut reader = QReader::new(payload);
284        let (salt, key_encrypted, info_encrypted) = streamed(
285            (|| {
286                let salt = reader.read_bytearray()?.unwrap_or_default().to_vec();
287                let key_encrypted = reader.read_bytearray()?.map(<[u8]>::to_vec);
288                let info_encrypted = reader.read_bytearray()?.unwrap_or_default().to_vec();
289                Ok((salt, key_encrypted, info_encrypted))
290            })(),
291            KEY_DATA_STREAM_FAILED,
292        )?;
293        if salt.len() != LOCAL_ENCRYPT_SALT_SIZE {
294            return Err(invalid(KeyDataFailure::BadSalt { size: salt.len() }));
295        }
296        let trailer = if reader.is_empty() {
297            None
298        } else {
299            let blob =
300                streamed(reader.read_bytearray(), TRAILER_STREAM_FAILED)?.unwrap_or_default();
301            Some(KeyDataTrailer::parse(blob)?)
302        };
303        Ok(Self {
304            salt,
305            key_encrypted,
306            info_encrypted,
307            trailer,
308        })
309    }
310
311    /// Encodes the key file payload; a trailer is written in format
312    /// [`KEY_DATA_FORMAT_VERSION`].
313    ///
314    /// # Errors
315    /// [`Error::QDataStreamFailed`] when a blob does not fit a `u32` length.
316    pub fn encode(&self) -> Result<Vec<u8>> {
317        let mut writer = QWriter::new();
318        writer.write_bytearray(Some(&self.salt))?;
319        writer.write_bytearray(self.key_encrypted.as_deref())?;
320        writer.write_bytearray(Some(&self.info_encrypted))?;
321        if let Some(trailer) = &self.trailer {
322            writer.write_bytearray(Some(&trailer.encode()?))?;
323        }
324        Ok(writer.into_bytes())
325    }
326
327    /// Number of wraps of the committed generation; 0 without a trailer.
328    #[must_use]
329    pub fn live_wrap_count(&self) -> usize {
330        self.trailer
331            .as_ref()
332            .map_or(0, |trailer| trailer.live_wraps().count())
333    }
334}
335
336/// The local key recovered from a key file and the key that unwrapped it.
337#[derive(Debug, Clone)]
338pub struct OpenedKey {
339    /// The local key every other tdata file is encrypted with.
340    pub local_key: AuthKey,
341    /// The open or passcode key the local key was wrapped under.
342    pub passcode_key: AuthKey,
343}
344
345/// Derives the key of one passcode wrap (`CreatePasscodeKey`).
346///
347/// # Errors
348/// - [`Error::KeyDataInvalid`] for an empty passcode, a salt shorter than
349///   [`kdf::PASSCODE_SALT_MIN_SIZE`], or parameters that are above the caps
350///   or not [`PasscodeKdf::valid`].
351/// - [`Error::BadDecryptKey`] when the derivation itself fails, which
352///   Telegram Desktop reports as a wrong passcode.
353pub fn create_passcode_key(passcode: &[u8], salt: &[u8], params: &PasscodeKdf) -> Result<AuthKey> {
354    if passcode.is_empty()
355        || salt.len() < kdf::PASSCODE_SALT_MIN_SIZE
356        || !params.cost_within_limits()
357        || !params.valid()
358    {
359        return Err(invalid(KeyDataFailure::UnusableWrapParameters {
360            kind: params.kind,
361            memory: params.memory,
362            time: params.time,
363            parallelism: params.parallelism,
364        }));
365    }
366    let derived = if params.kind == KDF_ARGON2ID {
367        kdf::argon2id(
368            passcode,
369            salt,
370            params.memory,
371            params.time,
372            params.parallelism,
373        )
374    } else {
375        kdf::scrypt(
376            passcode,
377            salt,
378            params.memory,
379            params.time,
380            params.parallelism,
381        )
382    };
383    derived.map(AuthKey::new).map_err(|_| Error::BadDecryptKey)
384}
385
386/// Wraps the local key under `wrap_key` (`PrepareEncrypted` of the raw
387/// 256 key bytes), with `padding` as the 12 bytes of local padding.
388///
389/// # Errors
390/// [`Error::BadLength`] when `padding` is not
391/// [`crypto::local_padding_len`]`(256)` bytes.
392pub fn wrap_local_key(local_key: &AuthKey, wrap_key: &AuthKey, padding: &[u8]) -> Result<Vec<u8>> {
393    crypto::encrypt_local(local_key.as_bytes(), wrap_key, |_| padding.to_vec())
394}
395
396/// Unwraps a local key written by [`wrap_local_key`].
397///
398/// # Errors
399/// [`Error::BadDecryptKey`] on any decryption failure or when the plaintext
400/// is not exactly [`AuthKey::SIZE`] bytes; both mean a wrong passcode.
401pub fn unwrap_local_key(key_encrypted: &[u8], wrap_key: &AuthKey) -> Result<AuthKey> {
402    let inner = crypto::decrypt_local(key_encrypted, wrap_key).map_err(|_| Error::BadDecryptKey)?;
403    AuthKey::from_slice(&inner).map_err(|_| Error::BadDecryptKey)
404}
405
406/// Recovers the local key the way `Domain::startModern` does.
407///
408/// - Legacy layout: the open wrap under `create_local_key(passcode, salt)`.
409/// - Empty passcode: the open wrap under `create_local_key("", salt)`.
410/// - Otherwise: the one live passcode wrap under [`create_passcode_key`].
411///
412/// # Errors
413/// - [`Error::BadDecryptKey`] for a wrong passcode, a wrap above the KDF read
414///   caps, an empty passcode while the open wrap is empty (app lock on), or a
415///   passcode without exactly one live wrap.
416/// - [`Error::KeyDataInvalid`] when the live wrap's parameters cannot make a
417///   passcode key.
418pub fn open_local_key(data: &KeyData, passcode: &[u8]) -> Result<OpenedKey> {
419    let open_wrap = data.key_encrypted.as_deref().unwrap_or_default();
420    let Some(trailer) = &data.trailer else {
421        let wrap_key = kdf::create_local_key(passcode, &data.salt);
422        return opened(open_wrap, wrap_key);
423    };
424    if trailer.wraps.iter().any(|w| !w.kdf.cost_within_limits()) {
425        return Err(Error::BadDecryptKey);
426    }
427    if passcode.is_empty() {
428        if open_wrap.is_empty() {
429            return Err(Error::BadDecryptKey);
430        }
431        return opened(open_wrap, kdf::create_local_key(&[], &data.salt));
432    }
433    // A typed passcode must open the one live passcode wrap, never the open
434    // wrap, or a later save would keep a passcode the file never had.
435    let mut live = trailer.live_wraps();
436    let (Some(wrap), None) = (live.next(), live.next()) else {
437        return Err(Error::BadDecryptKey);
438    };
439    let wrap_key = create_passcode_key(passcode, &wrap.salt, &wrap.kdf)?;
440    opened(&wrap.key_encrypted, wrap_key)
441}
442
443/// The sizes [`install`] draws, in order: the open salt, the wrap salt when
444/// there is a passcode, and the wrap padding.
445pub(crate) fn install_draws(has_passcode: bool) -> Vec<usize> {
446    let padding = crypto::local_padding_len(AuthKey::SIZE);
447    if has_passcode {
448        vec![LOCAL_ENCRYPT_SALT_SIZE, LOCAL_ENCRYPT_SALT_SIZE, padding]
449    } else {
450        vec![LOCAL_ENCRYPT_SALT_SIZE, padding]
451    }
452}
453
454/// Wraps `local_key` in a fresh 7.3 key file (`TDesktop.__installKeyData`,
455/// `Domain::generateLocalKey`) and returns it with the wrap key.
456///
457/// Draws a [`LOCAL_ENCRYPT_SALT_SIZE`] open salt. Without a passcode the
458/// local key is wrapped under `create_local_key("", salt)` with no passcode
459/// wrap; otherwise the open wrap is null, a second salt is drawn, and the one
460/// wrap uses [`default_passcode_kdf`] and is committed. The info is left
461/// empty for the writer to fill.
462pub(crate) fn install(
463    local_key: &AuthKey,
464    passcode: &[u8],
465    rng: &mut FillRandom<'_>,
466) -> Result<(KeyData, AuthKey)> {
467    let salt = random_bytes(rng, LOCAL_ENCRYPT_SALT_SIZE)?;
468    let padding_len = crypto::local_padding_len(AuthKey::SIZE);
469    if passcode.is_empty() {
470        let wrap_key = kdf::create_local_key(&[], &salt);
471        let padding = random_bytes(rng, padding_len)?;
472        let data = KeyData {
473            key_encrypted: Some(wrap_local_key(local_key, &wrap_key, &padding)?),
474            salt,
475            info_encrypted: Vec::new(),
476            trailer: Some(KeyDataTrailer {
477                committed: 0,
478                wraps: Vec::new(),
479            }),
480        };
481        return Ok((data, wrap_key));
482    }
483    let params = default_passcode_kdf();
484    let wrap_salt = random_bytes(rng, LOCAL_ENCRYPT_SALT_SIZE)?;
485    let wrap_key = create_passcode_key(passcode, &wrap_salt, &params)?;
486    let padding = random_bytes(rng, padding_len)?;
487    let wrap = PasscodeWrap {
488        kdf: params,
489        salt: wrap_salt,
490        key_encrypted: wrap_local_key(local_key, &wrap_key, &padding)?,
491    };
492    let data = KeyData {
493        salt,
494        key_encrypted: None,
495        info_encrypted: Vec::new(),
496        trailer: Some(KeyDataTrailer {
497            committed: params.generation,
498            wraps: vec![wrap],
499        }),
500    };
501    Ok((data, wrap_key))
502}
503
504fn opened(key_encrypted: &[u8], wrap_key: AuthKey) -> Result<OpenedKey> {
505    Ok(OpenedKey {
506        local_key: unwrap_local_key(key_encrypted, &wrap_key)?,
507        passcode_key: wrap_key,
508    })
509}
510
511/// The decrypted accounts info: account indices and the active index.
512#[derive(Debug, Clone, PartialEq, Eq)]
513pub struct AccountsInfo {
514    /// Account indices in file order, duplicates included.
515    pub indices: Vec<i32>,
516    /// Index of the active account; 0 when the stream ends before it.
517    pub active_index: i32,
518}
519
520impl AccountsInfo {
521    /// Decodes `count: i32`, `count` indices as `i32`, and an optional
522    /// `active: i32`.
523    ///
524    /// # Errors
525    /// - [`Error::QDataStreamFailed`] when the stream is cut short.
526    /// - [`Error::KeyDataInvalid`] when the count is not positive.
527    pub fn parse(data: &[u8]) -> Result<Self> {
528        let mut reader = QReader::new(data);
529        let count = reader.read_i32()?;
530        if count <= 0 {
531            return Err(invalid(KeyDataFailure::BadAccountsCount { count }));
532        }
533        let mut indices = Vec::new();
534        for _ in 0..count {
535            indices.push(reader.read_i32()?);
536        }
537        let active_index = if reader.is_empty() {
538            0
539        } else {
540            reader.read_i32()?
541        };
542        Ok(Self {
543            indices,
544            active_index,
545        })
546    }
547
548    /// Encodes `count: i32`, the indices as `i32`, and `active: i32`, the
549    /// plaintext [`TData::save`](super::TData::save) encrypts.
550    ///
551    /// # Errors
552    /// [`Error::QDataStreamFailed`] when the count does not fit an `i32`.
553    pub fn encode(&self) -> Result<Vec<u8>> {
554        let count = i32::try_from(self.indices.len())
555            .map_err(|_| Error::QDataStreamFailed { context: None })?;
556        let mut writer = QWriter::new();
557        writer.write_i32(count);
558        for &index in &self.indices {
559            writer.write_i32(index);
560        }
561        writer.write_i32(self.active_index);
562        Ok(writer.into_bytes())
563    }
564}
565
566/// Decrypts and decodes the accounts info of `data` with `local_key`.
567///
568/// `passcode` is the one [`open_local_key`] was given; it decides how a
569/// decryption failure is reported, as in `Domain::startModern`.
570///
571/// # Errors
572/// - [`Error::BadDecryptKey`] when decryption fails, no passcode was given,
573///   and exactly one live passcode wrap exists.
574/// - [`Error::KeyDataInvalid`] for any other decryption failure, or as
575///   [`AccountsInfo::parse`].
576/// - [`Error::QDataStreamFailed`] as [`AccountsInfo::parse`].
577pub fn decrypt_accounts_info(
578    data: &KeyData,
579    local_key: &AuthKey,
580    passcode: &[u8],
581) -> Result<AccountsInfo> {
582    let info = crypto::decrypt_local(&data.info_encrypted, local_key).map_err(|_| {
583        if passcode.is_empty() && data.live_wrap_count() == 1 {
584            Error::BadDecryptKey
585        } else {
586            invalid(KeyDataFailure::AccountsInfoDecrypt)
587        }
588    })?;
589    AccountsInfo::parse(&info)
590}
591
592/// The `ExpectStreamStatus` message after the three key file blobs.
593const KEY_DATA_STREAM_FAILED: &str = "Could not stream the key data";
594/// The message when the trailer blob is cut short.
595const TRAILER_STREAM_FAILED: &str = "Could not stream the key data trailer";
596/// The message when the trailer header is cut short.
597const TRAILER_HEADER_STREAM_FAILED: &str = "Could not stream the key data trailer header";
598/// The message when a passcode wrap is cut short.
599const WRAP_STREAM_FAILED: &str = "Could not stream a key data wrap";
600
601/// `result` with a stream failure named by the `context` of the
602/// reads it covers; other errors pass through.
603fn streamed<T>(result: Result<T>, context: &'static str) -> Result<T> {
604    result.map_err(|error| match error {
605        Error::QDataStreamFailed { context: None } => Error::QDataStreamFailed {
606            context: Some(context),
607        },
608        other => other,
609    })
610}
611
612const fn invalid(context: KeyDataFailure) -> Error {
613    Error::KeyDataInvalid { context }
614}