pub mod account;
pub mod key_data;
pub mod map;
pub mod mtp_config;
use std::collections::HashSet;
use std::fmt;
use std::path::Path;
use crate::crypto::{self, AuthKey};
use crate::kdf;
use crate::tdata::account::Account;
use crate::tdata::key_data::{
AccountsInfo, DEFAULT_KEY_FILE, KeyData, KeyDataTrailer, LOCAL_ENCRYPT_SALT_SIZE,
};
use crate::tdf::TdfFile;
use crate::{Error, Result, tdf};
pub const APP_VERSION: u32 = 7_003_001;
pub use crate::tdf::MAGIC as TDF_MAGIC;
pub const NEW_FOLDER_ACTIVE_INDEX: i32 = -1;
pub(crate) type FillRandom<'a> = dyn FnMut(&mut [u8]) -> Result<()> + 'a;
pub(crate) const fn hex_bytes<const N: usize>(hex: &str) -> [u8; N] {
const fn nibble(digit: u8) -> u8 {
match digit {
b'0'..=b'9' => digit - b'0',
b'a'..=b'f' => digit - b'a' + 10,
_ => panic!("hex constant holds a non-hex digit"),
}
}
let hex = hex.as_bytes();
assert!(hex.len() == 2 * N, "hex constant has the wrong length");
let mut out = [0u8; N];
let mut i = 0;
while i < N {
out[i] = (nibble(hex[2 * i]) << 4) | nibble(hex[2 * i + 1]);
i += 1;
}
out
}
const PERF_LOCAL_KEY: [u8; AuthKey::SIZE] = hex_bytes(concat!(
"d8745944519e0d2d71309d6c8d272dc64948f5e3eba7685324d5c691ad810c20",
"3b31d19d29aed6ac33c014be6e09843293f6fa32dbe42b6a04e00481fae99511",
"4caf6342bd98e96d293dd062c458689b3abd23a5cf230c75527c05bf5f90f38c",
"d93952cf61aaac1cfeaae4608592e363ded35f8d8c45234def53231decb35592",
"afc40d0601bbed11090969f74d9ab0cc97827546f441242d2cfb8e05a0610e97",
"669c0da1adccb56e39e10c69e2942387ff4922f8c55dcb8890e345ef318266f4",
"b3831430ea210c863c17624c0494cdead81f523430b5f74c15da323d766bd01c",
"b5b88b9d2a731f6d853380ad306a8647fa614cc4017f08902c1e1f997ee12e3c",
));
const PERF_PASSCODE_KEY_SALT: [u8; LOCAL_ENCRYPT_SALT_SIZE] =
hex_bytes("aed1e082994281d975760e729560d2c8d008f2a9dd3ff4d83245e22eedb66716");
const PERF_PASSCODE_KEY: [u8; AuthKey::SIZE] = hex_bytes(concat!(
"273e6483b313c9dbc4acd9173864c0422f172881bfe1c6649ca5538654d0bd6e",
"bcfbcc8dabbb669717ce53b41ca8afbf9d15c23fa2b06a6e166cc61f62e99805",
"58cd42cf1022e05b466811ea29e9359fc1f94668e5c85155bce638fe7aa42aa8",
"80ca83e0d2e534198a0a890139c8f679a57d3ba06d75fe5faa6e250101a58bc6",
"e42b9696604de1e34ae40f6bba144a284f3ad8843253ec9b3971863a2c409208",
"c2563967b3587e509b42a42a6040d23ff696ad552a240084fa3f950240f499b2",
"3cd6d27e7010cbde07daae0667a7df8a5115a60b265c58f5d9291f7a98fc3c60",
"1e2a4a32f1881b8218c855239d7b532959609e6ab52e48ad691c2583b566c8f9",
));
const PERF_PASSCODE_KEY_ENCRYPTED: [u8; 288] = hex_bytes(concat!(
"97df0cd2e3109149b77b5287994d9c1ca240c51e87488e79dd029bea65fb9d27",
"89bb5abcfe65e871d752bd938d83313c794c8993a734ce1216f2e660473f3143",
"af9a333610a17995876e1721ce1f611d1c69d8c1a2f59f9493119704274e2cb5",
"f36c20df439d156deff7a34371dc44bc86f8730cebf9b028eb7a1ed6621d99ad",
"b62b3b2cf2295dbbb24bf132d37fffc17a0bdccc84bbea6ea34737a236b58248",
"a7ab4c14363c20541cb45338677f339782b205e355189658dd45ea3e8005f851",
"148e7e15f431904fa79c6827ee426d3ab9cba936eb33d485db88a6f0ff9722a6",
"d62ff788347e27c82e9e139eb03ae521539bf3d363b4baea76e5e884cf66fe6b",
"cd8a9e089d36405db99d01db20464fb6cabbdce4f67e4ec3742f913a1dd2dac5",
));
pub(crate) fn random_bytes(rng: &mut FillRandom<'_>, len: usize) -> Result<Vec<u8>> {
let mut bytes = vec![0u8; len];
rng(&mut bytes)?;
Ok(bytes)
}
pub(crate) fn encrypt(
data: &[u8],
local_key: &AuthKey,
rng: &mut FillRandom<'_>,
) -> Result<Vec<u8>> {
let len = crypto::local_padding_len(data.len());
let padding = if len == 0 {
Vec::new()
} else {
random_bytes(rng, len)?
};
crypto::encrypt_local(data, local_key, move |_| padding)
}
pub(crate) fn write_tdf(dir: &Path, name: &str, payload: Vec<u8>) -> Result<()> {
tdf::write(
dir,
name,
&TdfFile {
version: APP_VERSION,
payload,
},
)
}
#[derive(Clone)]
struct Passcode(Vec<u8>);
impl Drop for Passcode {
fn drop(&mut self) {
self.0.fill(0);
std::hint::black_box(&mut self.0);
}
}
#[derive(Clone)]
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,
passcode: Passcode,
loaded_accounts: usize,
}
impl fmt::Debug for TData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TData")
.field("app_version", &self.app_version)
.field("key_file", &self.key_file)
.field("key_data", &self.key_data)
.field("local_key", &self.local_key)
.field("passcode_key", &self.passcode_key)
.field("accounts", &self.accounts)
.field("active_index", &self.active_index)
.field("performance_mode", &self.performance_mode)
.finish_non_exhaustive()
}
}
impl TData {
pub const MAX_ACCOUNTS: usize = 6;
pub fn new(
accounts: Vec<Account>,
passcode: &[u8],
performance_mode: bool,
rng: &mut dyn FnMut(&mut [u8]),
) -> Result<Self> {
Self::generate(accounts, passcode, performance_mode, &mut |buf| {
rng(buf);
Ok(())
})
}
fn generate(
accounts: Vec<Account>,
passcode: &[u8],
performance_mode: bool,
rng: &mut FillRandom<'_>,
) -> Result<Self> {
let (key_data, local_key, passcode_key) = if performance_mode && passcode.is_empty() {
let data = KeyData {
salt: PERF_PASSCODE_KEY_SALT.to_vec(),
key_encrypted: Some(PERF_PASSCODE_KEY_ENCRYPTED.to_vec()),
info_encrypted: Vec::new(),
trailer: Some(KeyDataTrailer {
committed: 0,
wraps: Vec::new(),
}),
};
(
data,
AuthKey::new(PERF_LOCAL_KEY),
AuthKey::new(PERF_PASSCODE_KEY),
)
} else {
let pass = random_bytes(rng, AuthKey::SIZE)?;
let salt = random_bytes(rng, LOCAL_ENCRYPT_SALT_SIZE)?;
let local_key = kdf::create_local_key(&pass, &salt);
let (data, passcode_key) = key_data::install(&local_key, passcode, rng)?;
(data, local_key, passcode_key)
};
Ok(Self {
app_version: APP_VERSION,
key_file: DEFAULT_KEY_FILE.to_owned(),
key_data,
local_key,
passcode_key,
accounts,
active_index: NEW_FOLDER_ACTIVE_INDEX,
performance_mode,
passcode: Passcode(passcode.to_vec()),
loaded_accounts: 0,
})
}
pub fn save(&self, dir: impl AsRef<Path>, rng: &mut dyn FnMut(&mut [u8])) -> Result<()> {
self.save_with(dir.as_ref(), self.performance_mode, &mut |buf| {
rng(buf);
Ok(())
})
}
pub fn save_random(&self, dir: impl AsRef<Path>) -> Result<()> {
self.save_with(
dir.as_ref(),
self.performance_mode,
&mut crypto::random_fill,
)
}
pub fn new_with_draws(
accounts: Vec<Account>,
passcode: &[u8],
performance_mode: bool,
active_index: i32,
mut draws: DrawSource,
) -> Result<Self> {
let created = Self::new(accounts, passcode, performance_mode, &mut |buf| {
draws.fill(buf);
});
draws.finish()?;
let mut created = created?;
created.active_index = active_index;
Ok(created)
}
pub fn save_with_draws(
&self,
dir: impl AsRef<Path>,
performance_mode: Option<bool>,
mut draws: DrawSource,
) -> Result<()> {
let dir = dir.as_ref();
let performance_mode = performance_mode.unwrap_or(self.performance_mode);
if !draws.is_replay() {
return self.save_with(dir, performance_mode, &mut crypto::random_fill);
}
let saved = self.save_with(dir, performance_mode, &mut |buf| {
draws.fill(buf);
Ok(())
});
draws.finish()?;
saved
}
pub fn migrate_with_draws(&mut self, mut draws: DrawSource) -> Result<bool> {
let migration = self.migration(&mut |buf| {
draws.fill(buf);
Ok(())
});
draws.finish()?;
let Some(migration) = migration? else {
return Ok(false);
};
self.commit_migration(migration);
Ok(true)
}
pub fn migrate(&mut self, rng: &mut dyn FnMut(&mut [u8])) -> Result<bool> {
let Some(migration) = self.migration(&mut |buf| {
rng(buf);
Ok(())
})?
else {
return Ok(false);
};
self.commit_migration(migration);
Ok(true)
}
fn migration(&self, rng: &mut FillRandom<'_>) -> Result<Option<(KeyData, AuthKey)>> {
if self.key_data.trailer.is_some() {
return Ok(None);
}
key_data::install(&self.local_key, &self.passcode.0, rng).map(Some)
}
fn commit_migration(&mut self, (data, passcode_key): (KeyData, AuthKey)) {
self.key_data = KeyData {
info_encrypted: std::mem::take(&mut self.key_data.info_encrypted),
..data
};
self.passcode_key = passcode_key;
}
fn save_with(
&self,
dir: &Path,
performance_mode: bool,
rng: &mut FillRandom<'_>,
) -> Result<()> {
if self.accounts.is_empty() {
return Err(Error::SaveFailed);
}
let migrated = if self.key_data.trailer.is_none() {
Some(key_data::install(&self.local_key, &self.passcode.0, rng)?.0)
} else {
None
};
let base = migrated.as_ref().unwrap_or(&self.key_data);
for account in &self.accounts {
account.write(dir, &self.key_file, &self.local_key, performance_mode, rng)?;
}
let info = self.accounts_info();
let key_data = KeyData {
info_encrypted: encrypt(&info.encode()?, &self.local_key, rng)?,
..base.clone()
};
write_tdf(
dir,
&key_data::file_name(&self.key_file),
key_data.encode()?,
)
}
pub fn load(dir: impl AsRef<Path>, passcode: &[u8], key_file: Option<&str>) -> Result<Self> {
let dir = dir.as_ref();
let key_file = key_file.unwrap_or(DEFAULT_KEY_FILE);
let file = tdf::read(dir, &key_data::file_name(key_file))?;
let mut data = KeyData::parse(&file.payload)?;
let opened = key_data::open_local_key(&data, passcode)?;
let info = key_data::decrypt_accounts_info(&data, &opened.local_key, passcode)?;
data.trailer = data.trailer.map(|trailer| KeyDataTrailer {
committed: trailer.committed,
wraps: trailer.live_wraps().cloned().collect(),
});
let mut seen = HashSet::new();
let accounts: Vec<Account> = info
.indices
.iter()
.filter(|&&index| seen.insert(index) && index >= 0)
.filter_map(|&index| Account::read(dir, key_file, index, &opened.local_key).ok())
.collect();
if accounts.is_empty() {
return Err(Error::AuthKeyNotFound);
}
Ok(Self {
app_version: file.version,
key_file: key_file.to_owned(),
key_data: data,
local_key: opened.local_key,
passcode_key: opened.passcode_key,
loaded_accounts: accounts.len(),
accounts,
active_index: info.active_index,
performance_mode: true,
passcode: Passcode(passcode.to_vec()),
})
}
pub fn set_accounts(&mut self, accounts: Vec<Account>) -> Result<()> {
if accounts.len() > Self::MAX_ACCOUNTS.max(self.loaded_accounts) {
return Err(Error::MaxAccountLimit);
}
let mut seen = HashSet::new();
if !accounts.iter().all(|account| seen.insert(account.index)) {
return Err(Error::SaveFailed);
}
self.accounts = accounts;
Ok(())
}
pub fn add_account(&mut self, user_id: u64, main_dc: i32, auth_key: AuthKey) -> Result<i32> {
if self.accounts.len() >= Self::MAX_ACCOUNTS {
return Err(Error::MaxAccountLimit);
}
let index = (0..)
.take(Self::MAX_ACCOUNTS)
.find(|index| self.accounts.iter().all(|account| account.index != *index))
.ok_or(Error::MaxAccountLimit)?;
let account = Account::new(index, user_id, main_dc, vec![(main_dc, auth_key)])?;
self.accounts.push(account);
Ok(index)
}
pub fn set_key_file(&mut self, name: &str) -> Result<()> {
if name.is_empty() {
return Err(Error::SaveFailed);
}
name.clone_into(&mut self.key_file);
Ok(())
}
#[must_use]
pub fn main_account(&self) -> Option<&Account> {
self.accounts
.iter()
.find(|account| account.index == self.active_index)
.or_else(|| self.accounts.first())
}
}
struct Replay {
draws: Vec<Vec<u8>>,
next: usize,
}
pub struct DrawSource {
replay: Option<Replay>,
failure: Option<Error>,
}
impl DrawSource {
#[must_use]
pub fn os() -> Self {
Self {
replay: None,
failure: None,
}
}
#[must_use]
pub fn replay(draws: Vec<Vec<u8>>) -> Self {
Self {
replay: Some(Replay { draws, next: 0 }),
failure: None,
}
}
#[must_use]
pub fn new(draws: Option<Vec<Vec<u8>>>) -> Self {
draws.map_or_else(Self::os, Self::replay)
}
#[must_use]
pub fn is_replay(&self) -> bool {
self.replay.is_some()
}
pub fn fill(&mut self, buf: &mut [u8]) {
if self.failure.is_some() {
buf.fill(0);
return;
}
let Some(replay) = self.replay.as_mut() else {
if let Err(error) = crypto::random_fill(buf) {
self.failure = Some(error);
buf.fill(0);
}
return;
};
match replay.draws.get(replay.next) {
Some(draw) if draw.len() == buf.len() => {
buf.copy_from_slice(draw);
replay.next += 1;
}
Some(draw) => {
self.failure = Some(replay_mismatch(format!(
"draw {} has {} bytes, but {} were drawn",
replay.next,
draw.len(),
buf.len()
)));
buf.fill(0);
}
None => {
self.failure = Some(replay_mismatch(format!(
"draw {} of {} bytes was not given ({} draws given)",
replay.next,
buf.len(),
replay.draws.len()
)));
buf.fill(0);
}
}
}
pub fn finish(mut self) -> Result<()> {
if let Some(failure) = self.failure.take() {
return Err(failure);
}
match &self.replay {
Some(replay) if replay.next != replay.draws.len() => Err(replay_mismatch(format!(
"{} of {} draws were left unused",
replay.draws.len() - replay.next,
replay.draws.len()
))),
_ => Ok(()),
}
}
}
impl fmt::Debug for DrawSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut out = f.debug_struct("DrawSource");
match &self.replay {
Some(replay) => out
.field("replay_draws", &replay.draws.len())
.field("next", &replay.next),
None => out.field("source", &"os"),
};
out.field("failed", &self.failure.is_some()).finish()
}
}
impl Drop for DrawSource {
fn drop(&mut self) {
if let Some(replay) = self.replay.as_mut() {
for draw in &mut replay.draws {
draw.fill(0);
}
std::hint::black_box(&mut replay.draws);
}
}
}
fn replay_mismatch(reason: String) -> Error {
Error::InvalidInput {
what: "draws",
reason,
}
}
#[derive(Clone, Copy, Debug)]
pub enum DrawOperation<'a> {
New {
has_passcode: bool,
performance_mode: bool,
},
Migrate(&'a TData),
Save(&'a TData),
}
impl<'a> DrawOperation<'a> {
pub const NAMES: [&'static str; 3] = ["new", "migrate", "save"];
pub fn from_name(
name: &str,
has_passcode: Option<bool>,
performance_mode: Option<bool>,
folder: Option<&'a TData>,
) -> Result<Self> {
match (name, folder) {
("new", None) => Ok(Self::New {
has_passcode: has_passcode.unwrap_or(false),
performance_mode: performance_mode.unwrap_or(true),
}),
("new", Some(_)) => Err(Error::InvalidInput {
what: "target",
reason: "the \"new\" plan takes has_passcode and performance_mode, not a TData"
.to_owned(),
}),
("migrate", Some(folder)) => Ok(Self::Migrate(folder)),
("save", Some(folder)) => Ok(Self::Save(folder)),
("migrate" | "save", None) => Err(Error::InvalidInput {
what: "target",
reason: format!("the {name:?} plan takes a TData"),
}),
_ => Err(Error::InvalidInput {
what: "operation",
reason: format!(
"unknown operation {name:?}; expected one of {}",
Self::NAMES.join(", ")
),
}),
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::New { .. } => "new",
Self::Migrate(_) => "migrate",
Self::Save(_) => "save",
}
}
}
pub fn draw_plan(operation: DrawOperation<'_>) -> Result<Vec<usize>> {
match operation {
DrawOperation::New {
has_passcode,
performance_mode,
} => {
if performance_mode && !has_passcode {
return Ok(Vec::new());
}
let mut draws = vec![AuthKey::SIZE, LOCAL_ENCRYPT_SALT_SIZE];
draws.extend(key_data::install_draws(has_passcode));
Ok(draws)
}
DrawOperation::Migrate(tdata) => Ok(tdata.migration_draws()),
DrawOperation::Save(tdata) => {
if tdata.accounts.is_empty() {
return Err(Error::SaveFailed);
}
let mut draws = tdata.migration_draws();
let mut padding = |len: usize| {
let padding = crypto::local_padding_len(len);
if padding > 0 {
draws.push(padding);
}
};
for account in &tdata.accounts {
for plaintext in account.write_plaintexts(tdata.performance_mode)? {
padding(plaintext.len());
}
}
padding(tdata.accounts_info().encode()?.len());
Ok(draws)
}
}
}
impl TData {
fn migration_draws(&self) -> Vec<usize> {
if self.key_data.trailer.is_some() {
Vec::new()
} else {
key_data::install_draws(!self.passcode.0.is_empty())
}
}
fn accounts_info(&self) -> AccountsInfo {
AccountsInfo {
indices: self.accounts.iter().map(|account| account.index).collect(),
active_index: self.active_index,
}
}
}