ZaStoGram_desktop/Telegram/SourceFiles/storage/storage_domain.cpp
loop-uh a27885b97a Поправить сборку после слияния с Telegram Desktop v7.3.1
storage_domain.cpp(103) C2248: WrapLocalKey (свободная функция в анонимном namespace) вызывала приватный MTP::AuthKey::write, friend только Storage::Domain. Сделал WrapLocalKey приватным static-членом Storage::Domain (объявление в storage_domain.h, определение в storage_domain.cpp перед конструктором). Поведение не менял. Проверить, что дальше нет других ошибок: сборка остановилась на 947/1147.
2026-10-16 01:00:29 +03:00

1140 lines
37 KiB
C++

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "storage/storage_domain.h"
#include "core/version.h"
#include "storage/details/storage_file_utilities.h"
#include "storage/serialize_common.h"
#include "mtproto/config/mtproto_config.h"
#include "main/main_domain.h"
#include "main/main_account.h"
#include "base/openssl_help.h"
#include "base/random.h"
namespace Storage {
struct PasscodeWrap final {
details::PasscodeKdf kdf;
quint32 generation = 0;
QByteArray salt;
QByteArray keyEncrypted;
};
// The two legacy flags travel together and are cleared together. legacy says
// the file carries only the three old blobs, so openKeyEncrypted is whatever
// the pre-change build wrote there; legacyPasscode says that blob is wrapped
// under a passcode rather than under the empty string. The parse cannot tell
// the two apart - both look like a non-empty openKeyEncrypted - so the second
// flag is set at the point the typed passcode proved which one it is, and it
// is what lets hasPasscode() and appLockEnabled() keep reporting the truth
// while a migration that failed to derive its wrap waits for the next start.
struct KeyData final {
QByteArray openSalt;
QByteArray openKeyEncrypted;
std::vector<PasscodeWrap> passcodeWraps;
quint32 committed = 0;
bool legacy = false;
bool legacyPasscode = false;
bool openKeyVerified = false;
};
namespace {
using namespace details;
enum class ReadKeyDataResult {
Success,
UnsupportedKdf,
Failed,
};
// A legacy key_data stream holds exactly three blobs, so it is at the end
// right after infoEncrypted and any bytes past it mean the new layout. The
// magic word turns "there are extra bytes" into a positive identification.
// AppVersion does not change with this format, so the version stamped in the
// file cannot discriminate the two shapes and the migration is driven by the
// parse-time legacy flag instead. The trailer travels as one appended blob
// because FileWriteDescriptor writes blobs and hashes the file over exactly
// those, so raw stream fields would not be covered by its md5.
constexpr auto kKeyDataMagic = quint32(0x4B443200);
constexpr auto kKeyDataFormatVersion = quint32(2);
constexpr auto kMaxWrapCount = quint32(2);
[[nodiscard]] QString BaseGlobalPath() {
return cWorkingDir() + u"tdata/"_q;
}
[[nodiscard]] QString ComputeKeyName(const QString &dataName) {
// We dropped old test authorizations when migrated to multi auth.
//return "key_" + dataName + (cTestMode() ? "[test]" : "");
return "key_" + dataName;
}
[[nodiscard]] QString ComputeWalletKeyringName(const QString &dataName) {
return u"wallet_keyring_"_q + dataName;
}
[[nodiscard]] QByteArray RandomSalt() {
auto result = QByteArray(LocalEncryptSaltSize, Qt::Uninitialized);
base::RandomFill(result.data(), result.size());
return result;
}
[[nodiscard]] bool SameWrapKey(const PasscodeWrap &a, const PasscodeWrap &b) {
return (a.kdf.kind == b.kdf.kind)
&& (a.kdf.memory == b.kdf.memory)
&& (a.kdf.time == b.kdf.time)
&& (a.kdf.parallel == b.kdf.parallel)
&& (a.salt == b.salt);
}
[[nodiscard]] MTP::AuthKeyPtr UnwrapLocalKey(
const QByteArray &keyEncrypted,
const MTP::AuthKeyPtr &wrapKey) {
if (!wrapKey) {
return nullptr;
}
EncryptedDescriptor data;
if (!DecryptLocal(data, keyEncrypted, wrapKey)) {
return nullptr;
}
auto key = Serialize::read<MTP::AuthKey::Data>(data.stream);
if (data.stream.status() != QDataStream::Ok || !data.stream.atEnd()) {
return nullptr;
}
return std::make_shared<MTP::AuthKey>(key);
}
[[nodiscard]] bool WrapOpensLocalKey(
const QByteArray &keyEncrypted,
const MTP::AuthKeyPtr &wrapKey,
const MTP::AuthKeyPtr &localKey) {
const auto recovered = UnwrapLocalKey(keyEncrypted, wrapKey);
return recovered && recovered->equals(localKey);
}
[[nodiscard]] ReadKeyDataResult ReadKeyData(
QDataStream &stream,
KeyData &data,
QByteArray &infoEncrypted) {
stream >> data.openSalt >> data.openKeyEncrypted >> infoEncrypted;
if (!CheckStreamStatus(stream)) {
return ReadKeyDataResult::Failed;
} else if (data.openSalt.size() != LocalEncryptSaltSize) {
LOG(("App Error: bad salt in info file, size: %1"
).arg(data.openSalt.size()));
return ReadKeyDataResult::Failed;
}
if (stream.atEnd()) {
data.legacy = true;
return ReadKeyDataResult::Success;
}
auto trailer = QByteArray();
stream >> trailer;
if (!CheckStreamStatus(stream)) {
return ReadKeyDataResult::Failed;
}
QBuffer buffer(&trailer);
if (!buffer.open(QIODevice::ReadOnly)) {
LOG(("App Error: could not open the key data trailer."));
return ReadKeyDataResult::Failed;
}
QDataStream inner(&buffer);
inner.setVersion(QDataStream::Qt_5_1);
auto magic = quint32();
auto formatVersion = quint32();
auto wrapCount = quint32();
inner >> magic >> formatVersion >> data.committed >> wrapCount;
if (!CheckStreamStatus(inner)) {
return ReadKeyDataResult::Failed;
} else if (magic != kKeyDataMagic) {
LOG(("App Error: bad key data trailer magic."));
return ReadKeyDataResult::Failed;
} else if (formatVersion > kKeyDataFormatVersion) {
LOG(("App Error: too new key data format: %1").arg(formatVersion));
return ReadKeyDataResult::Failed;
} else if (wrapCount > kMaxWrapCount) {
LOG(("App Error: bad key data wrap count: %1").arg(wrapCount));
return ReadKeyDataResult::Failed;
}
auto unsupportedCost = false;
data.passcodeWraps.reserve(wrapCount);
for (auto i = quint32(); i != wrapCount; ++i) {
auto wrap = PasscodeWrap();
inner >> wrap.kdf.kind
>> wrap.generation
>> wrap.kdf.memory
>> wrap.kdf.time
>> wrap.kdf.parallel
>> wrap.salt
>> wrap.keyEncrypted;
if (!CheckStreamStatus(inner)) {
return ReadKeyDataResult::Failed;
} else if (!wrap.kdf.costWithinLimits()) {
unsupportedCost = true;
} else if (!wrap.kdf.valid()) {
LOG(("App Error: bad key data KDF family: %1").arg(wrap.kdf.kind));
return ReadKeyDataResult::Failed;
}
if (wrap.salt.size() < kPasscodeSaltMinSize) {
LOG(("App Error: bad key data wrap salt size: %1"
).arg(wrap.salt.size()));
return ReadKeyDataResult::Failed;
}
for (const auto &already : data.passcodeWraps) {
if (already.generation == wrap.generation) {
LOG(("App Error: duplicate key data wrap generation: %1"
).arg(wrap.generation));
return ReadKeyDataResult::Failed;
}
}
data.passcodeWraps.push_back(std::move(wrap));
}
return unsupportedCost
? ReadKeyDataResult::UnsupportedKdf
: ReadKeyDataResult::Success;
}
void WriteKeyData(
FileWriteDescriptor &file,
const KeyData &data,
const QByteArray &infoEncrypted) {
file.writeData(data.openSalt);
file.writeData(data.openKeyEncrypted);
file.writeData(infoEncrypted);
if (data.legacy) {
return;
}
auto trailer = QByteArray();
QBuffer buffer(&trailer);
const auto opened = buffer.open(QIODevice::WriteOnly);
Assert(opened);
QDataStream stream(&buffer);
stream.setVersion(QDataStream::Qt_5_1);
stream
<< kKeyDataMagic
<< kKeyDataFormatVersion
<< data.committed
<< quint32(data.passcodeWraps.size());
for (const auto &wrap : data.passcodeWraps) {
stream
<< wrap.kdf.kind
<< wrap.generation
<< wrap.kdf.memory
<< wrap.kdf.time
<< wrap.kdf.parallel
<< wrap.salt
<< wrap.keyEncrypted;
}
buffer.close();
file.writeData(trailer);
}
} // namespace
PasscodeVerification::PasscodeVerification(quint64 nonce)
: _nonce(nonce) {
}
PasscodeDerivation::PasscodeDerivation(
std::unique_ptr<PasscodeWrap> wrap,
const QByteArray &passcode)
: _wrap(std::move(wrap))
, _passcode(passcode.constData(), passcode.size()) {
}
PasscodeDerivation::PasscodeDerivation(PasscodeDerivation &&other) noexcept
: _wrap(base::take(other._wrap))
, _passcode(base::take(other._passcode))
, _key(base::take(other._key))
, _cleansed(base::take(other._cleansed)) {
}
PasscodeDerivation &PasscodeDerivation::operator=(
PasscodeDerivation &&other) noexcept {
cleanse();
_wrap = base::take(other._wrap);
_passcode = base::take(other._passcode);
_key = base::take(other._key);
_cleansed = base::take(other._cleansed);
return *this;
}
PasscodeDerivation::~PasscodeDerivation() {
cleanse();
}
void PasscodeDerivation::run() {
if (!_wrap || _passcode.isEmpty()) {
return;
} else if (_wrap->kdf.kind == 0) {
_key = CreateLocalKey(_passcode, _wrap->salt);
return;
}
_key = CreatePasscodeKey(_passcode, _wrap->salt, _wrap->kdf);
cleanse();
}
bool PasscodeDerivation::empty() const {
return _passcode.isEmpty();
}
MTP::AuthKeyPtr PasscodeDerivation::keyFor(const PasscodeWrap &wrap) {
const auto matched = _wrap && SameWrapKey(*_wrap, wrap);
if (matched && _key) {
return _key;
} else if (_cleansed) {
if (!matched) {
LOG(("App Error: the passcode wrap changed under the derivation."));
}
return nullptr;
} else if (wrap.kdf.kind == 0) {
return CreateLocalKey(_passcode, wrap.salt);
} else if (_passcode.isEmpty()) {
return nullptr;
}
return CreatePasscodeKey(_passcode, wrap.salt, wrap.kdf);
}
void PasscodeDerivation::cleanse() {
if (!_passcode.isEmpty()) {
OPENSSL_cleanse(_passcode.data(), _passcode.size());
}
_cleansed = true;
}
QByteArray Domain::WrapLocalKey(
const MTP::AuthKeyPtr &localKey,
const MTP::AuthKeyPtr &wrapKey) {
Expects(localKey != nullptr);
Expects(wrapKey != nullptr);
EncryptedDescriptor data(MTP::AuthKey::kSize);
localKey->write(data.stream);
return PrepareEncrypted(data, wrapKey);
}
Domain::Domain(not_null<Main::Domain*> owner, const QString &dataName)
: _owner(owner)
, _dataName(dataName)
, _keyData(std::make_unique<KeyData>()) {
}
Domain::~Domain() = default;
StartResult Domain::start(PasscodeDerivation derived) {
const auto modern = startModern(derived);
if (modern == StartModernResult::Success) {
if (_keyData->legacy || _keyDataDirty || _oldVersion < AppVersion) {
writeAccounts();
}
return StartResult::Success;
} else if (modern == StartModernResult::IncorrectPasscode) {
return StartResult::IncorrectPasscode;
} else if (modern == StartModernResult::Failed) {
// startFromScratch() drops a local key startModern() already read.
startFromScratch();
return StartResult::Success;
}
auto legacy = std::make_unique<Main::Account>(_owner, _dataName, 0);
const auto result = legacy->legacyStart(derived._passcode);
if (result == StartResult::Success) {
_oldVersion = legacy->local().oldMapVersion();
startWithSingleAccount(derived._passcode, std::move(legacy));
}
return result;
}
void Domain::startAdded(
not_null<Main::Account*> account,
std::unique_ptr<MTP::Config> config) {
Expects(_localKey != nullptr);
account->prepareToStartAdded(_localKey);
account->start(std::move(config));
}
void Domain::startWithSingleAccount(
const QByteArray &passcode,
std::unique_ptr<Main::Account> account) {
Expects(account != nullptr);
if (auto localKey = account->local().peekLegacyLocalKey()) {
_localKey = std::move(localKey);
installLegacyWrap(*_keyData, passcode);
migrateFromLegacy(passcode);
account->start(nullptr);
} else {
generateLocalKey();
account->start(account->prepareToStart(_localKey));
}
_owner->accountAddedInStorage(Main::Domain::AccountWithIndex{
.account = std::move(account)
});
writeAccounts();
}
void Domain::generateLocalKey() {
Expects(_localKey == nullptr);
Expects(_keyData->openKeyEncrypted.isEmpty());
Expects(_keyData->passcodeWraps.empty());
auto pass = QByteArray(MTP::AuthKey::kSize, Qt::Uninitialized);
base::RandomFill(pass.data(), pass.size());
const auto salt = RandomSalt();
_localKey = CreateLocalKey(pass, salt);
installOpenWrap(*_keyData);
}
void Domain::installOpenWrap(KeyData &data) const {
Expects(_localKey != nullptr);
data.openSalt = RandomSalt();
const auto wrapKey = CreateLocalKey(QByteArray(), data.openSalt);
data.openKeyEncrypted = WrapLocalKey(_localKey, wrapKey);
data.openKeyVerified = WrapOpensLocalKey(
data.openKeyEncrypted,
wrapKey,
_localKey);
}
// Turning the app lock on drops the open wrap, recording its absence with
// an empty openKeyEncrypted - never with an empty openSalt. A build of the
// same AppVersion that predates this format reads only the three legacy
// blobs: a real-size salt beside an empty wrap makes it fail inside
// DecryptLocal and ask for a passcode, while an empty salt would make it fail
// the salt-size check, start from scratch and orphan every account store. So
// the salt stays 32 real random bytes in both states.
void Domain::dropOpenWrap(KeyData &data) const {
data.openSalt = RandomSalt();
data.openKeyEncrypted = QByteArray();
data.openKeyVerified = false;
}
void Domain::installLegacyWrap(
KeyData &data,
const QByteArray &passcode) const {
Expects(_localKey != nullptr);
data.openSalt = RandomSalt();
const auto wrapKey = CreateLocalKey(passcode, data.openSalt);
data.openKeyEncrypted = WrapLocalKey(_localKey, wrapKey);
data.legacy = true;
data.legacyPasscode = !passcode.isEmpty();
data.openKeyVerified = passcode.isEmpty()
&& WrapOpensLocalKey(data.openKeyEncrypted, wrapKey, _localKey);
}
MTP::AuthKeyPtr Domain::installPasscodeWrap(
KeyData &data,
PasscodeDerivation &derived,
quint32 generation) const {
Expects(_localKey != nullptr);
Expects(!derived.empty());
Expects(derived._wrap && derived._wrap->keyEncrypted.isEmpty());
auto wrap = *derived._wrap;
wrap.generation = generation;
const auto wrapKey = derived.keyFor(wrap);
if (!wrapKey) {
LOG(("App Error: could not derive the passcode key, family: %1"
).arg(wrap.kdf.kind));
return nullptr;
}
wrap.keyEncrypted = WrapLocalKey(_localKey, wrapKey);
if (!WrapOpensLocalKey(wrap.keyEncrypted, wrapKey, _localKey)) {
LOG(("App Error: a fresh passcode wrap does not open the local key."));
return nullptr;
}
data.passcodeWraps.push_back(std::move(wrap));
return wrapKey;
}
// Migration keeps "a passcode means the app lock is on", so hasLocalPasscode(),
// the auto-lock and the lock icon read exactly as they did before it ran. A
// derivation failure is not allowed to lose either the data or the app lock:
// the key data is then left in the legacy shape it was read in, which writes
// back byte for byte what the pre-change build wrote, and the next start
// simply tries the migration again. In that state openKeyEncrypted still holds
// the passcode-derived legacy blob, which legacyPasscode records, so the
// predicates keep reporting a passcode with the app lock armed and every
// mutator refuses instead of relabelling that blob as an open wrap.
void Domain::migrateFromLegacy(const QByteArray &passcode) {
Expects(_keyData->legacy);
Expects(_localKey != nullptr);
if (passcode.isEmpty()) {
installOpenWrap(*_keyData);
_keyData->passcodeWraps.clear();
_keyData->committed = 0;
} else {
auto fresh = prepareNewWrap(passcode);
if (!installPasscodeWrap(*_keyData, fresh, 1)) {
return;
}
dropOpenWrap(*_keyData);
_keyData->committed = 1;
}
_keyData->legacy = false;
_keyData->legacyPasscode = false;
_keyDataDirty = true;
_verificationNonce = 0;
}
Domain::StartModernResult Domain::startModern(
PasscodeDerivation &derived) {
const auto name = ComputeKeyName(_dataName);
FileReadDescriptor file;
if (!ReadFile(file, name, BaseGlobalPath())) {
return StartModernResult::Empty;
}
LOG(("App Info: reading accounts info..."));
auto parsed = KeyData();
auto infoEncrypted = QByteArray();
const auto read = ReadKeyData(file.stream, parsed, infoEncrypted);
if (read == ReadKeyDataResult::Failed) {
return StartModernResult::Failed;
} else if (read == ReadKeyDataResult::UnsupportedKdf) {
return StartModernResult::IncorrectPasscode;
}
*_keyData = std::move(parsed);
// Probe the stored open wrap independently of the typed passcode result.
// PrepareEncrypted adds a 16-byte prefix to the padded length and key.
// Only this ciphertext size can hold the fixed-size local key. Bound
// this extra decrypt before it allocates, keeping the selected unwrap
// and startup results intact. Compare once that unwrap gives a key.
constexpr auto kOpenWrapSize = 16
+ ((sizeof(uint32) + MTP::AuthKey::kSize + 15) / 16) * 16;
auto openKey = (_keyData->openKeyEncrypted.size() == kOpenWrapSize)
? UnwrapLocalKey(
_keyData->openKeyEncrypted,
CreateLocalKey(QByteArray(), _keyData->openSalt))
: nullptr;
auto wrapKey = MTP::AuthKeyPtr();
auto localKeyEncrypted = QByteArray();
if (_keyData->legacy) {
const auto blob = PasscodeWrap{
.salt = _keyData->openSalt,
.keyEncrypted = _keyData->openKeyEncrypted,
};
wrapKey = derived.keyFor(blob);
if (!wrapKey) {
return StartModernResult::IncorrectPasscode;
}
localKeyEncrypted = blob.keyEncrypted;
} else {
// A passcode wrap is live only while its generation equals the
// committed one. That drops a staged wrap left above committed by a
// crash in the middle of a passcode change and a stale wrap left
// below it, and it is also what turns an interrupted creation of a
// first passcode back into the "no passcode" file it was before.
auto live = std::vector<PasscodeWrap>();
for (auto &wrap : _keyData->passcodeWraps) {
if (wrap.generation == _keyData->committed) {
live.push_back(std::move(wrap));
}
}
if (live.size() != _keyData->passcodeWraps.size()) {
_keyDataDirty = true;
}
_keyData->passcodeWraps = std::move(live);
if (derived.empty()) {
if (_keyData->openKeyEncrypted.isEmpty()) {
LOG(("App Info: the app lock is on, a passcode is needed."));
return StartModernResult::IncorrectPasscode;
}
wrapKey = CreateLocalKey(QByteArray(), _keyData->openSalt);
localKeyEncrypted = _keyData->openKeyEncrypted;
} else if (_keyData->passcodeWraps.size() != 1) {
LOG(("App Info: no passcode wrap to open the local key with."));
return StartModernResult::IncorrectPasscode;
} else {
const auto &wrap = _keyData->passcodeWraps.front();
wrapKey = derived.keyFor(wrap);
if (!wrapKey) {
LOG(("App Error: passcode KDF family %1 is unavailable."
).arg(wrap.kdf.kind));
return StartModernResult::IncorrectPasscode;
}
localKeyEncrypted = wrap.keyEncrypted;
}
}
EncryptedDescriptor keyInnerData, info;
if (!DecryptLocal(keyInnerData, localKeyEncrypted, wrapKey)) {
LOG(("App Info: could not decrypt pass-protected key from info file, "
"maybe bad password..."));
return StartModernResult::IncorrectPasscode;
}
// An authentic envelope that does not hold a local key cannot be told
// from an overwritten open field by the empty start alone, so while a
// live passcode wrap survives, asking for it beats resetting what that
// passcode still opens, and the file stays untouched for the typed
// retry. Nothing is published yet here - the candidate key and its
// verified-open claim are settled below - so returning is all this
// refusal has to do.
auto key = Serialize::read<MTP::AuthKey::Data>(keyInnerData.stream);
if (keyInnerData.stream.status() != QDataStream::Ok
|| !keyInnerData.stream.atEnd()) {
if (derived.empty() && _keyData->passcodeWraps.size() == 1) {
LOG(("App Info: the open wrap does not hold a local key, "
"a passcode is needed."));
return StartModernResult::IncorrectPasscode;
}
LOG(("App Error: could not read pass-protected key from info file"));
return StartModernResult::Failed;
}
_localKey = std::make_shared<MTP::AuthKey>(key);
_keyData->openKeyVerified = openKey && openKey->equals(_localKey);
openKey = nullptr;
if (_keyData->legacy) {
_keyData->legacyPasscode = !derived.empty();
migrateFromLegacy(derived._passcode);
}
// The empty start cannot tell an open wrap that holds another key from a
// broken accounts info: only the passcode can, and while a live passcode
// wrap survives asking for it is the honest answer. The candidate key and
// its verified-open claim were already published above, so a refusal here
// withdraws both, exactly as the earlier refusals leave them, and the file
// stays untouched for the typed retry. With no passcode wrap there is no
// secret left to ask for, so the scratch recovery keeps that case.
if (!DecryptLocal(info, infoEncrypted, _localKey)) {
if (derived.empty() && _keyData->passcodeWraps.size() == 1) {
LOG(("App Info: the open wrap does not open the accounts info, "
"a passcode is needed."));
_localKey = nullptr;
_keyData->openKeyVerified = false;
return StartModernResult::IncorrectPasscode;
}
LOG(("App Error: could not decrypt info."));
return StartModernResult::Failed;
}
LOG(("App Info: reading encrypted info..."));
auto count = qint32();
info.stream >> count;
if (count <= 0 || count > Main::Domain::kPremiumMaxAccounts) {
LOG(("App Error: bad accounts count: %1").arg(count));
return StartModernResult::Failed;
}
_oldVersion = file.version;
auto tried = base::flat_set<int>();
auto sessions = base::flat_set<uint64>();
auto active = 0;
for (auto i = 0; i != count; ++i) {
auto index = qint32();
info.stream >> index;
if (index >= 0
&& index < Main::Domain::kPremiumMaxAccounts
&& tried.emplace(index).second) {
auto account = std::make_unique<Main::Account>(
_owner,
_dataName,
index);
auto config = account->prepareToStart(_localKey);
const auto sessionId = account->willHaveSessionUniqueId(
config.get());
if (!sessions.contains(sessionId)
&& (sessionId != 0 || (sessions.empty() && i + 1 == count))) {
if (sessions.empty()) {
active = index;
}
account->start(std::move(config));
_owner->accountAddedInStorage({
.index = index,
.account = std::move(account)
});
sessions.emplace(sessionId);
}
}
}
if (sessions.empty()) {
LOG(("App Error: no accounts read."));
return StartModernResult::Failed;
}
if (!info.stream.atEnd()) {
info.stream >> active;
}
_owner->activateFromStorage(active);
Ensures(!sessions.empty());
return StartModernResult::Success;
}
void Domain::writeAccounts() {
Expects(!_owner->accounts().empty());
Expects(!_keyData->openKeyEncrypted.isEmpty()
|| !_keyData->passcodeWraps.empty());
Expects(_keyData->passcodeWraps.size() <= 1);
writeKeyData(*_keyData, false);
_keyDataDirty = false;
}
QByteArray Domain::prepareAccountsInfo() const {
Expects(_localKey != nullptr);
const auto &list = _owner->accounts();
auto keySize = sizeof(qint32) + sizeof(qint32) * list.size();
EncryptedDescriptor info(keySize);
info.stream << qint32(list.size());
for (const auto &[index, account] : list) {
info.stream << qint32(index);
}
info.stream << qint32(_owner->activeForStorage());
return PrepareEncrypted(info, _localKey);
}
bool Domain::writeKeyData(const KeyData &data, bool sync) const {
const auto path = BaseGlobalPath();
if (!QDir().exists(path)) {
QDir().mkpath(path);
}
FileWriteDescriptor key(ComputeKeyName(_dataName), path, sync);
WriteKeyData(key, data, prepareAccountsInfo());
return key.finish();
}
// Unlike writeAccounts(), which queues the bytes and always reports success,
// this one commits them on the storage thread and answers whether they really
// reached the disk. Every path that changes which secret opens the local key
// goes through it, because a removal or a rewrap that only looked like it was
// written would leave the user with a file none of the secrets they know can
// open. details::Sync() is deliberately not called afterwards: the sync
// descriptor already drops any queued entry for the same base and performs the
// write before finish() returns.
bool Domain::writeKeyDataChecked(const KeyData &data) const {
Expects(!_owner->accounts().empty());
Expects(!data.legacy);
Expects(!data.openKeyEncrypted.isEmpty() || !data.passcodeWraps.empty());
Expects(data.passcodeWraps.size() <= kMaxWrapCount);
const auto written = writeKeyData(data, true);
if (!written) {
LOG(("App Error: could not write the accounts key file."));
}
return written;
}
// The staged wrap was already proved to open the local key in memory, so what
// is left to prove is that the bytes the write path put on disk are those same
// bytes. Comparing the parsed wrap field by field against the staged one and
// then unwrapping with the key the derivation already produced keeps the whole
// read-back round trip - a serialization that dropped, reordered or truncated
// a field still fails here - while paying for the memory-hard derivation once
// instead of twice: byte-equal salt and parameters can only re-derive the same
// key, so re-deriving it would prove nothing the comparison has not.
bool Domain::wrapOnDiskOpensLocalKey(
const PasscodeWrap &staged,
const MTP::AuthKeyPtr &wrapKey) const {
Expects(_localKey != nullptr);
Expects(wrapKey != nullptr);
FileReadDescriptor file;
if (!ReadFile(file, ComputeKeyName(_dataName), BaseGlobalPath())) {
return false;
}
auto parsed = KeyData();
auto infoEncrypted = QByteArray();
const auto read = ReadKeyData(file.stream, parsed, infoEncrypted);
if (read != ReadKeyDataResult::Success || parsed.legacy) {
return false;
}
for (const auto &wrap : parsed.passcodeWraps) {
if (wrap.generation != staged.generation) {
continue;
} else if (!SameWrapKey(wrap, staged)
|| wrap.keyEncrypted != staged.keyEncrypted) {
LOG(("App Error: the staged passcode wrap changed on disk."));
return false;
}
return WrapOpensLocalKey(wrap.keyEncrypted, wrapKey, _localKey);
}
return false;
}
WalletEngineValue Domain::readWalletKeyring() const {
using State = WalletEngineValue::State;
const auto name = ComputeWalletKeyringName(_dataName);
const auto path = BaseGlobalPath();
const auto base = path + name;
if (!QFileInfo::exists(base + 's')
&& !QFileInfo::exists(base + '0')
&& !QFileInfo::exists(base + '1')) {
return { .state = State::Absent };
} else if (!_localKey) {
return { .state = State::Broken };
}
FileReadDescriptor file;
if (!ReadEncryptedFile(file, name, path, _localKey)) {
return { .state = State::Broken };
}
auto bytes = QByteArray();
file.stream >> bytes;
if (!CheckStreamStatus(file.stream) || !file.stream.atEnd()) {
return { .state = State::Broken };
}
return { .state = State::Read, .bytes = std::move(bytes) };
}
bool Domain::writeWalletKeyring(const QByteArray &bytes) const {
Expects(_localKey != nullptr);
const auto path = BaseGlobalPath();
if (!QDir().exists(path) && !QDir().mkpath(path)) {
return false;
}
EncryptedDescriptor data(Serialize::bytearraySize(bytes));
data.stream << bytes;
FileWriteDescriptor file(ComputeWalletKeyringName(_dataName), path, true);
file.writeEncrypted(data, _localKey);
const auto written = file.finish();
if (!written) {
LOG(("Wallet Error: could not write the device keyring."));
}
return written;
}
bool Domain::removeWalletKeyring() {
const auto base = BaseGlobalPath() + ComputeWalletKeyringName(_dataName);
auto result = true;
for (const auto suffix : { 's', '0', '1' }) {
const auto path = base + suffix;
if (QFileInfo::exists(path) && !QFile::remove(path)) {
result = false;
}
}
return result;
}
void Domain::startFromScratch() {
if (!removeWalletKeyring()) {
LOG(("Wallet Error: could not remove the discarded device keyring."));
}
*_keyData = KeyData();
_keyDataDirty = false;
_verificationNonce = 0;
_localKey = nullptr;
startWithSingleAccount(
QByteArray(),
std::make_unique<Main::Account>(_owner, _dataName, 0));
}
std::unique_ptr<PasscodeWrap> Domain::wrapToOpen() const {
if (_keyData->legacy || _keyData->legacyPasscode) {
return std::make_unique<PasscodeWrap>(PasscodeWrap{
.salt = _keyData->openSalt,
.keyEncrypted = _keyData->openKeyEncrypted,
});
} else if (_keyData->passcodeWraps.size() != 1) {
return nullptr;
}
return std::make_unique<PasscodeWrap>(_keyData->passcodeWraps.front());
}
PasscodeDerivation Domain::prepareOpen(const QByteArray &passcode) const {
return PasscodeDerivation(wrapToOpen(), passcode);
}
PasscodeDerivation Domain::prepareNewWrap(const QByteArray &passcode) const {
return PasscodeDerivation(
std::make_unique<PasscodeWrap>(PasscodeWrap{
.kdf = DefaultPasscodeKdf(),
.salt = RandomSalt(),
}),
passcode);
}
// The only decisions this makes before the derivation runs are which wrap the
// passcode has to open and whether such a wrap exists at all; neither depends
// on what was typed and so neither tells an attacker anything about it. From
// there a wrong passcode and the right one walk the same derivation and the
// same unwrap attempt, and the verdict is the comparison of the recovered key
// with the one already in memory - the typed passcode is never needed to open
// the file at start, only to prove it is the one the wrap was made from.
//
// The retained legacy state carries no passcode wrap: the only secret that
// opens the local key there is the passcode-derived blob the pre-change build
// left in openKeyEncrypted, so it is checked exactly the way startModern()
// opens it. That state reports a passcode with the app lock armed, and this
// leg is what lets the lock it arms be opened by the passcode that armed it.
bool Domain::checkPasscode(PasscodeDerivation derived) const {
Expects(_localKey != nullptr);
const auto wrap = wrapToOpen();
if (!wrap) {
return false;
}
const auto key = derived.keyFor(*wrap);
return key && WrapOpensLocalKey(wrap->keyEncrypted, key, _localKey);
}
std::optional<PasscodeVerification> Domain::verifyPasscode(
const QByteArray &passcode) {
return verifyPasscode(prepareOpen(passcode));
}
std::optional<PasscodeVerification> Domain::verifyPasscode(
PasscodeDerivation derived) {
if (!checkPasscode(std::move(derived))) {
return std::nullopt;
}
auto nonce = quint64();
while (!nonce) {
nonce = base::RandomValue<quint64>();
}
_verificationNonce = nonce;
return PasscodeVerification(nonce);
}
bool Domain::accepts(PasscodeVerification verification) const {
return _verificationNonce && (verification._nonce == _verificationNonce);
}
SetPasscodeResult Domain::setPasscode(
const QByteArray &passcode,
PasscodeVerification verification) {
if (passcode.isEmpty()) {
return changePasscode(nullptr, verification);
}
auto derived = prepareNewWrap(passcode);
return changePasscode(&derived, verification);
}
SetPasscodeResult Domain::setPasscode(
PasscodeDerivation derived,
PasscodeVerification verification) {
Expects(!derived.empty());
return changePasscode(&derived, verification);
}
// A passcode change is staged so that no crash window can leave a file only a
// passcode nobody has typed can open. The new wrap is written beside the old
// one first, with the committed generation left where it was, so a start that
// happens in that window drops the staged wrap by generation and finds exactly
// the previous truth; the bytes are then read back from disk and proved to
// open the local key, and only after that does a second write strip the old
// wrap and move committed forward. No write on this path ever adds a fresh
// open wrap, because a file carrying both the old passcode wrap and a fresh
// open wrap would keep the old passcode working while silently turning the app
// lock off - which no reconciliation rule at the next start can tell from an
// intended state. A removal is the mirror of the same rule and needs no
// staging: nothing new has to be proved, so it is a single atomic checked
// write of the open wrap with no passcode wrap left.
//
// This is the passcode-role mutator and it does not move the app-lock role.
// The open wrap is dropped only where setPasscode() creates a first passcode,
// the one case that has to keep the Settings behaviour of "setting a passcode
// locks the app"; createPasscodeWithoutAppLock() runs the same staged
// sequence for a first passcode in the wallet-only role and keeps the
// verified open wrap it already has, and a passcode change carries whatever
// open-wrap state the file already had straight through. A file still in the
// legacy shape is refused outright, because its openKeyEncrypted may be the
// passcode-derived blob and nothing here could honestly relabel that as an
// open wrap.
SetPasscodeResult Domain::changePasscode(
PasscodeDerivation *derived,
PasscodeVerification verification) {
Expects(_localKey != nullptr);
const auto singleUse = gsl::finally([&] { _verificationNonce = 0; });
if (_keyData->legacy) {
LOG(("App Error: refusing a passcode change before the migration."));
return SetPasscodeResult::Failed;
} else if (!_keyData->passcodeWraps.empty() && !accepts(verification)) {
return SetPasscodeResult::NeedsVerification;
} else if (derived) {
return installPasscode(*derived, !_keyData->passcodeWraps.empty());
}
auto updated = *_keyData;
updated.passcodeWraps.clear();
updated.committed = _keyData->committed + 1;
installOpenWrap(updated);
Assert(!updated.openKeyEncrypted.isEmpty());
if (!writeKeyDataChecked(updated)) {
return SetPasscodeResult::Failed;
}
*_keyData = std::move(updated);
_keyDataDirty = false;
_verificationNonce = 0;
_passcodeKeyChanged.fire({});
return SetPasscodeResult::Success;
}
SetPasscodeResult Domain::installPasscode(
PasscodeDerivation &derived,
bool keepOpenWrap) {
const auto generation = _keyData->committed + 1;
auto staged = *_keyData;
const auto wrapKey = installPasscodeWrap(staged, derived, generation);
if (!wrapKey) {
return SetPasscodeResult::Failed;
} else if (!writeKeyDataChecked(staged)) {
return SetPasscodeResult::Failed;
} else if (!wrapOnDiskOpensLocalKey(
staged.passcodeWraps.back(),
wrapKey)) {
if (!writeKeyDataChecked(*_keyData)) {
LOG(("App Error: could not drop the staged passcode wrap."));
}
return SetPasscodeResult::Failed;
}
auto updated = *_keyData;
updated.passcodeWraps.clear();
updated.passcodeWraps.push_back(staged.passcodeWraps.back());
updated.committed = generation;
if (!keepOpenWrap) {
dropOpenWrap(updated);
}
Assert(!updated.passcodeWraps.empty());
if (!writeKeyDataChecked(updated)) {
return SetPasscodeResult::Failed;
}
*_keyData = std::move(updated);
_keyDataDirty = false;
_verificationNonce = 0;
_passcodeKeyChanged.fire({});
return SetPasscodeResult::Success;
}
SetPasscodeResult Domain::createPasscodeWithoutAppLock(
PasscodeDerivation derived) {
Expects(_localKey != nullptr);
const auto singleUse = gsl::finally([&] { _verificationNonce = 0; });
if (_keyData->legacy || _keyData->legacyPasscode) {
LOG(("App Error: refusing a passcode without the app lock "
"before the migration."));
return SetPasscodeResult::Failed;
} else if (!_keyData->passcodeWraps.empty()) {
return SetPasscodeResult::NeedsVerification;
} else if (_keyData->openKeyEncrypted.isEmpty()
|| !_keyData->openKeyVerified) {
LOG(("App Error: refusing a passcode without the app lock "
"over an unverified open wrap."));
return SetPasscodeResult::Failed;
}
return installPasscode(derived, true);
}
// Turning the lock off installs an open wrap over a file a passcode still
// protects - the same weakening of the data at rest a removal is - so it
// carries the same proof a removal does. Turning it on proves nothing new
// and only needs a passcode wrap to exist. The gsl::finally still spends the
// nonce on every exit, whichever way the lock moves, so a token that
// authorized one change cannot authorize another.
SetPasscodeResult Domain::setAppLockEnabled(
bool enabled,
PasscodeVerification verification) {
Expects(_localKey != nullptr);
const auto singleUse = gsl::finally([&] { _verificationNonce = 0; });
const auto hasWrap = !_keyData->passcodeWraps.empty();
if (_keyData->legacy) {
LOG(("App Error: refusing an app lock change before the migration."));
return SetPasscodeResult::Failed;
} else if ((enabled && !hasWrap)
|| (!enabled && hasWrap && !accepts(verification))) {
return SetPasscodeResult::NeedsVerification;
}
auto updated = *_keyData;
if (enabled) {
dropOpenWrap(updated);
} else {
installOpenWrap(updated);
}
if (!writeKeyDataChecked(updated)) {
return SetPasscodeResult::Failed;
}
*_keyData = std::move(updated);
_keyDataDirty = false;
_verificationNonce = 0;
_passcodeKeyChanged.fire({});
return SetPasscodeResult::Success;
}
// Main::Domain::removePasscodeIfEmpty() calls this after Local::reset() has
// destroyed every store the local key protected. Nothing remains for the old
// passcode to guard or to ask for it - the account it belonged to is gone.
// Wallet's confirmed forgotten-passcode reset calls it after the shared
// keyring and all signed-in custody records are gone. DropUnusedPasscode()
// calls it when the verified app lock is off and no live keyring entry uses
// the Passcode policy, at startup, logout and terminal wallet operations.
// Each caller establishes that the passcode guards nothing left to ask
// for, which is why this removal carries no verification and has its own
// name instead of being reachable through setPasscode(). The write stays
// checked so failure does not claim the passcode was removed.
void Domain::clearPasscodeAfterReset() {
Expects(_localKey != nullptr);
auto updated = *_keyData;
updated.legacy = false;
updated.legacyPasscode = false;
updated.passcodeWraps.clear();
updated.committed = _keyData->committed + 1;
installOpenWrap(updated);
Assert(!updated.openKeyEncrypted.isEmpty());
if (!writeKeyDataChecked(updated)) {
return;
}
*_keyData = std::move(updated);
_keyDataDirty = false;
_verificationNonce = 0;
_passcodeKeyChanged.fire({});
}
int Domain::oldVersion() const {
return _oldVersion;
}
void Domain::clearOldVersion() {
_oldVersion = 0;
}
rpl::producer<> Domain::localPasscodeChanged() const {
return _passcodeKeyChanged.events();
}
bool Domain::hasPasscode() const {
return !_keyData->passcodeWraps.empty() || _keyData->legacyPasscode;
}
bool Domain::appLockEnabled() const {
return _keyData->legacyPasscode || !_keyData->openKeyVerified;
}
bool Domain::hasLocalPasscode() const {
// This historical API means verified app locking, not a wallet passcode.
return appLockEnabled();
}
} // namespace Storage