ZaStoGram_desktop/Telegram/SourceFiles/platform/win/wallet_protection_win.cpp

712 lines
23 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 "platform/win/wallet_protection_win.h"
#include "base/call_delayed.h"
#include "base/platform/win/base_windows_safe_library.h"
#include "base/platform/win/base_windows_winrt.h"
#include "base/random.h"
#include "core/update_channel.h"
#include "lang/lang_keys.h"
#include "storage/serialize_common.h"
#include "wallet/wallet_key_protection.h"
#include "wallet/wallet_vault.h"
#include "settings.h"
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtWidgets/QApplication>
#include <QtWidgets/QWidget>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Security.Credentials.h>
#include <winrt/Windows.Security.Cryptography.h>
#include <winrt/Windows.Storage.Streams.h>
#include <winrt/Windows.UI.h>
#include <tbs.h>
// The wrap key is stored nowhere: it is HKDF-SHA256 over the signature a
// Windows Hello credential produces for a device-wrap random challenge kept
// in the wrap's payload, salted with the wrap's own random salt. That rests
// on the Hello key storage provider answering RequestSignAsync with
// RSASSA-PKCS1-v1_5 over SHA-256 - a deterministic function of the private
// key and the challenge, measured on a discrete-TPM host rather than
// promised by the platform's documentation. The keyring thus holds every
// public input and no private one, and DeriveVaultWrapKey refuses this
// kind, so nothing derivable from the keyring opens D.
// Should Windows ever move the provider to a probabilistic scheme, every
// existing wrap would read Corrupt - read-only for every account, nothing
// deleted, restore from the backup or the phrase - and never leak a key.
//
// Where the credential's private key lives is Windows' choice: the TPM the
// availability check found when Hello can use it, a software key store
// otherwise. Its attestation is not asked for. That proof depended on the
// TPM's endorsement certificate and on an attestation certificate Windows
// obtains from Microsoft's online service, which many machines never get,
// so demanding it turned the offered row into a failure right after the
// user's PIN. The row is offered on a qualifying build with a TPM and
// Windows Hello set up, and whatever Hello then creates is accepted.
namespace Platform {
namespace {
using Wallet::ProtectionError;
using namespace winrt::Windows::Security::Credentials;
using winrt::Windows::Foundation::IAsyncAction;
using winrt::Windows::Foundation::IAsyncInfo;
using winrt::Windows::Foundation::IAsyncOperation;
using AsyncStatus = winrt::Windows::Foundation::AsyncStatus;
using winrt::Windows::Storage::Streams::IBuffer;
using winrt::Windows::Security::Cryptography::CryptographicBuffer;
using winrt::Windows::UI::WindowId;
constexpr auto kPayloadVersion = quint32(1);
constexpr auto kCredentialIdSize = 16;
constexpr auto kChallengeSize = 32;
constexpr char kWrapLabel[] = "tdesktop-wallet-vault/windows-hello-wrap/v1";
constexpr auto kCredentialPrefix
= std::wstring_view(L"TelegramDesktop.WalletVault.");
constexpr auto kPromptFocusInterval = crl::time(100);
constexpr auto kPromptFocusAttempts = 50;
TBS_RESULT(WINAPI *GetTpmDeviceInfo)(UINT32, PVOID) = nullptr;
// ABI::Windows::UI::WindowId, as windows.ui.interop.h declares the export.
struct WindowIdAbi {
uint64_t value = 0;
};
HRESULT(WINAPI *GetWindowIdFromWindow)(HWND, WindowIdAbi*) = nullptr;
struct Payload {
QByteArray credentialId;
QByteArray challenge;
};
struct Retrieval {
ProtectionError error = ProtectionError::Unavailable;
KeyCredential credential = nullptr;
};
struct Signing {
ProtectionError error = ProtectionError::Unavailable;
Wallet::SecureBytes signature;
};
struct EnrollOperation {
winrt::hstring name;
QByteArray challenge;
std::optional<WindowId> owner;
KeyCredential credential = nullptr;
Wallet::VaultWrap wrap;
Fn<void(Wallet::ProtectionEnrollResult)> done;
};
struct UnwrapOperation {
winrt::hstring name;
QByteArray challenge;
std::optional<WindowId> owner;
KeyCredential credential = nullptr;
Wallet::VaultWrap wrap;
Fn<void(Wallet::ProtectionUnwrapResult)> done;
};
[[nodiscard]] QByteArray SerializePayload(const Payload &payload) {
auto stream = Serialize::ByteArrayWriter(int(sizeof(quint32))
+ Serialize::bytearraySize(payload.credentialId)
+ Serialize::bytearraySize(payload.challenge));
stream << kPayloadVersion << payload.credentialId << payload.challenge;
return std::move(stream).result();
}
[[nodiscard]] std::optional<Payload> ParsePayload(const QByteArray &data) {
auto stream = Serialize::ByteArrayReader(data);
auto version = quint32();
auto result = Payload();
stream >> version >> result.credentialId >> result.challenge;
if (!stream.ok()
|| !stream.atEnd()
|| version != kPayloadVersion
|| result.credentialId.size() != kCredentialIdSize
|| result.challenge.size() != kChallengeSize) {
return std::nullopt;
}
return result;
}
[[nodiscard]] winrt::hstring CredentialName(const QByteArray &credentialId) {
const auto hex = QString::fromLatin1(credentialId.toHex());
return winrt::hstring(
std::wstring(kCredentialPrefix) + hex.toStdWString());
}
// A Windows Hello prompt asked for without an owner window opens behind the
// app and only flashes in the taskbar. Windows 11 24H2 accepts the owner as a
// WindowId, so every ask resolves one from the window the user is acting in
// when the operation starts - later prompts of the same operation keep it,
// because by then the active window can be the previous prompt. Without the
// export, or with no active window, the prompts go unowned as before.
[[nodiscard]] std::optional<WindowId> PromptOwner() {
static const auto loaded = [] {
const auto library = base::Platform::SafeLoadLibrary(
L"Windows.UI.dll");
return base::Platform::LoadMethod(
library,
"GetWindowIdFromWindow",
GetWindowIdFromWindow);
}();
const auto window = QApplication::activeWindow();
if (!loaded || !window) {
return std::nullopt;
}
auto id = WindowIdAbi();
const auto handle = reinterpret_cast<HWND>(window->winId());
if (FAILED(GetWindowIdFromWindow(handle, &id)) || !id.value) {
return std::nullopt;
}
return WindowId{ id.value };
}
// An unowned prompt is shown by the credential broker, which may not take the
// foreground from the app the user is in, so it opens behind and flashes in
// the taskbar. Right after the user acted the app still is the foreground
// process, which lets it bring the prompt forward itself once the broker has
// shown it. The window is found by its class name, which is undocumented: a
// Windows that renames it leaves the prompt where the broker put it.
void FocusUnownedPrompt(int attemptsLeft = kPromptFocusAttempts) {
const auto window = FindWindow(L"Credential Dialog Xaml Host", nullptr);
if (window && IsWindowVisible(window)) {
if (GetForegroundWindow() == window || SetForegroundWindow(window)) {
LOG(("Wallet Info: the Windows Hello prompt is in the "
"foreground, attempts left: %1.").arg(attemptsLeft));
return;
}
}
if (attemptsLeft > 0) {
base::call_delayed(kPromptFocusInterval, [=] {
FocusUnownedPrompt(attemptsLeft - 1);
});
} else {
LOG(("Wallet Warning: the Windows Hello prompt was not brought to "
"the foreground, found: %1.").arg(window ? 1 : 0));
}
}
// The owned overloads exist from Windows 11 24H2: on an older system the
// factory or the credential lacks the interface, and the unowned ask runs
// with the prompt brought forward by its class name instead.
[[nodiscard]] IAsyncOperation<KeyCredentialRetrievalResult> RequestCreate(
const winrt::hstring &name,
const std::optional<WindowId> &owner) {
const auto option = KeyCredentialCreationOption::ReplaceExisting;
if (owner) {
auto owned = base::WinRT::Try([&] {
return KeyCredentialManager::RequestCreateForWindowAsync(
*owner,
name,
option);
});
if (owned) {
return std::move(*owned);
}
}
auto result = KeyCredentialManager::RequestCreateAsync(name, option);
FocusUnownedPrompt();
return result;
}
[[nodiscard]] IAsyncOperation<KeyCredentialOperationResult> RequestSign(
const KeyCredential &credential,
const IBuffer &data,
const std::optional<WindowId> &owner) {
if (owner) {
if (const auto owned = credential.try_as<IKeyCredentialWithWindow>()) {
auto request = base::WinRT::Try([&] {
return owned.RequestSignForWindowAsync(*owner, data);
});
if (request) {
return std::move(*request);
}
}
}
auto result = credential.RequestSignAsync(data);
FocusUnownedPrompt();
return result;
}
[[nodiscard]] QByteArray RandomBytes(int size) {
auto result = QByteArray(size, Qt::Uninitialized);
base::RandomFill(result.data(), result.size());
return result;
}
[[nodiscard]] IBuffer ToBuffer(const QByteArray &bytes) {
return CryptographicBuffer::CreateFromByteArray(
winrt::array_view<const uint8_t>(
reinterpret_cast<const uint8_t*>(bytes.constData()),
uint32_t(bytes.size())));
}
// The signature is copied into a SecureBytes and the runtime buffer we hold
// is zeroed in place; the runtime may keep other copies of the bytes it
// handed out, which is the same caveat the Touch ID provider records for
// its CFData - this zeroes the one copy under our control.
[[nodiscard]] Wallet::SecureBytes TakeSecure(const IBuffer &buffer) {
if (!buffer) {
return Wallet::SecureBytes();
}
const auto data = buffer.data();
const auto length = buffer.Length();
if (!data || !length) {
return Wallet::SecureBytes();
}
auto result = Wallet::SecureBytes(bytes::make_span(data, length));
SecureZeroMemory(data, length);
return result;
}
// The one environment not offered Windows Hello is an unpacked copy of an
// official release: no uninstaller beside the executable and not a canary
// build. An installed copy and either canary are offered it whenever the
// TPM and Hello itself allow, and so is a Debug build working from its
// TelegramForcePortable folder, to debug the flow from the build output.
[[nodiscard]] bool UninstallerPresent() {
static const auto Result = QFile::exists(cExeDir() + u"unins000.exe"_q);
return Result;
}
[[nodiscard]] bool DebugPortable() {
#ifdef _DEBUG
static const auto Result = (QDir(cWorkingDir())
== QDir(cExeDir() + u"TelegramForcePortable"_q));
return Result;
#else // _DEBUG
return false;
#endif // _DEBUG
}
[[nodiscard]] bool ReadTpmPresence() {
const auto library = base::Platform::SafeLoadLibrary(L"tbs.dll");
if (!base::Platform::LoadMethod(
library,
"Tbsi_GetDeviceInfo",
GetTpmDeviceInfo)) {
return false;
}
auto info = TPM_DEVICE_INFO();
const auto result = GetTpmDeviceInfo(sizeof(info), &info);
LOG(("Wallet Info: TBS device info result %1, TPM version %2."
).arg(result
).arg(info.tpmVersion));
return (result == TBS_SUCCESS) && (info.tpmVersion != 0);
}
[[nodiscard]] bool TpmPresent() {
static const auto Result = ReadTpmPresence();
return Result;
}
// NotFound alone is Absent: it is what OpenAsync answers once the credential
// is gone - after a DeleteAsync or a Windows Hello reset - the confirmed
// typed absence that lands the keyless modes, with nothing deleted here.
// UserCanceled is the dismissed prompt, and a Hello lockout
// (SecurityDeviceLocked) is answered the same way: Windows refuses the
// gesture for a while after too many wrong attempts, so the prompt is over
// for now and the user simply asks again once Hello accepts input, with no
// mode change and nothing written. Everything else is a service or access
// failure and stays Unavailable, which preserves the keyring; a non-Completed
// async status or a thrown HRESULT lands there too, the fail-safe side.
// AuthenticationFailed is never produced: Hello retries a wrong gesture
// inside its own UI and reports only the lockout.
[[nodiscard]] ProtectionError Classify(KeyCredentialStatus status) {
switch (status) {
case KeyCredentialStatus::Success:
return ProtectionError::None;
case KeyCredentialStatus::NotFound:
LOG(("Wallet Error: Windows Hello credential not found."));
return ProtectionError::Absent;
case KeyCredentialStatus::UserCanceled:
return ProtectionError::Cancelled;
case KeyCredentialStatus::SecurityDeviceLocked:
LOG(("Wallet Info: Windows Hello is locked out, prompt dismissed."));
return ProtectionError::Cancelled;
}
LOG(("Wallet Error: Windows Hello operation failed, status %1."
).arg(int(status)));
return ProtectionError::Unavailable;
}
[[nodiscard]] ProtectionError ClassifyFailure(
const IAsyncInfo &info,
AsyncStatus status) {
const auto code = base::WinRT::Try([&] {
return uint32(info.ErrorCode().value);
}).value_or(0);
LOG(("Wallet Error: Windows Hello operation ended with async status %1, "
"code 0x%2."
).arg(int(status)
).arg(code, 8, 16, QChar('0')));
return ProtectionError::Unavailable;
}
[[nodiscard]] Wallet::SecureBytes DeriveWrapKey(
const Wallet::SecureBytes &signature,
const QByteArray &salt) {
return Wallet::HkdfSha256(
signature.span(),
bytes::make_span(salt),
bytes::make_span(kWrapLabel, sizeof(kWrapLabel) - 1),
Wallet::kVaultKeySize);
}
[[nodiscard]] Retrieval ReadRetrieval(
const IAsyncOperation<KeyCredentialRetrievalResult> &that,
AsyncStatus status) {
if (status != AsyncStatus::Completed) {
return { .error = ClassifyFailure(that, status) };
}
auto read = base::WinRT::Try([&] {
const auto result = that.GetResults();
auto retrieval = Retrieval{ .error = Classify(result.Status()) };
if (retrieval.error == ProtectionError::None) {
retrieval.credential = result.Credential();
if (!retrieval.credential) {
retrieval.error = ProtectionError::Unavailable;
}
}
return retrieval;
});
return read ? std::move(*read) : Retrieval();
}
[[nodiscard]] Signing ReadSigning(
const IAsyncOperation<KeyCredentialOperationResult> &that,
AsyncStatus status) {
if (status != AsyncStatus::Completed) {
return { .error = ClassifyFailure(that, status) };
}
auto read = base::WinRT::Try([&] {
const auto result = that.GetResults();
auto signing = Signing{ .error = Classify(result.Status()) };
if (signing.error == ProtectionError::None) {
signing.signature = TakeSecure(result.Result());
if (signing.signature.empty()) {
signing.error = ProtectionError::Unavailable;
}
}
return signing;
});
return read ? std::move(*read) : Signing();
}
void AnswerEnroll(
Fn<void(Wallet::ProtectionEnrollResult)> done,
ProtectionError error) {
crl::on_main([done = std::move(done), error] {
done({ .error = error });
});
}
void AnswerUnwrap(
Fn<void(Wallet::ProtectionUnwrapResult)> done,
ProtectionError error) {
crl::on_main([done = std::move(done), error] {
done({ .error = error });
});
}
class WindowsHelloProtection final : public Wallet::ProtectionProvider {
public:
[[nodiscard]] Wallet::VaultKind kind() const override;
[[nodiscard]] rpl::producer<bool> available() const override;
[[nodiscard]] rpl::producer<QString> title() const override;
[[nodiscard]] rpl::producer<QString> description() const override;
[[nodiscard]] rpl::producer<QString> label() const override;
void enroll(
not_null<Storage::Account*> local,
Fn<void(Wallet::ProtectionEnrollResult)> done) override;
void unwrap(
not_null<Storage::Account*> local,
Wallet::VaultWrap wrap,
Fn<void(Wallet::ProtectionUnwrapResult)> done) override;
void remove(
not_null<Storage::Account*> local,
Wallet::VaultWrap wrap,
Fn<void(ProtectionError)> done) override;
void prime();
private:
void signForEnroll(std::shared_ptr<EnrollOperation> op);
void signForUnwrap(std::shared_ptr<UnwrapOperation> op);
void discardCredential(const winrt::hstring &name);
rpl::variable<bool> _available = false;
bool _uninstaller = false;
bool _debugPortable = false;
bool _tpm = false;
};
Wallet::VaultKind WindowsHelloProtection::kind() const {
return Wallet::VaultKind::WindowsHello;
}
rpl::producer<bool> WindowsHelloProtection::available() const {
return _available.value();
}
rpl::producer<QString> WindowsHelloProtection::title() const {
return tr::lng_settings_use_winhello();
}
rpl::producer<QString> WindowsHelloProtection::description() const {
return tr::lng_wallet_protection_hello_about();
}
rpl::producer<QString> WindowsHelloProtection::label() const {
return tr::lng_wallet_protection_hello_label();
}
void WindowsHelloProtection::prime() {
_uninstaller = UninstallerPresent();
_debugPortable = DebugPortable();
_tpm = TpmPresent();
const auto started = base::WinRT::Try([&] {
KeyCredentialManager::IsSupportedAsync().Completed([this](
IAsyncOperation<bool> that,
AsyncStatus status) {
const auto supported = (status == AsyncStatus::Completed)
&& base::WinRT::Try([&] {
return that.GetResults();
}).value_or(false);
crl::on_main([this, supported] {
_available = (_uninstaller
|| Core::BuildIsCanary
|| _debugPortable)
&& _tpm
&& supported;
LOG(("Wallet Info: Windows Hello availability: "
"uninstaller %1, canary %2, debug portable %3, "
"TPM %4, supported %5."
).arg(_uninstaller ? 1 : 0
).arg(Core::BuildIsCanary ? 1 : 0
).arg(_debugPortable ? 1 : 0
).arg(_tpm ? 1 : 0
).arg(supported ? 1 : 0));
});
});
});
if (!started) {
LOG(("Wallet Error: Windows Hello support check could not start."));
}
}
void WindowsHelloProtection::enroll(
not_null<Storage::Account*> local,
Fn<void(Wallet::ProtectionEnrollResult)> done) {
if (!_available.current()) {
AnswerEnroll(std::move(done), ProtectionError::Unavailable);
return;
}
const auto credentialId = RandomBytes(kCredentialIdSize);
auto op = std::make_shared<EnrollOperation>();
op->name = CredentialName(credentialId);
op->challenge = RandomBytes(kChallengeSize);
op->owner = PromptOwner();
op->wrap = Wallet::VaultWrap{
.kind = Wallet::VaultKind::WindowsHello,
.salt = RandomBytes(Wallet::kVaultSaltSize),
.openSecret = SerializePayload({
.credentialId = credentialId,
.challenge = op->challenge,
}),
};
op->done = std::move(done);
const auto started = base::WinRT::Try([&] {
RequestCreate(op->name, op->owner).Completed([op, this](
IAsyncOperation<KeyCredentialRetrievalResult> that,
AsyncStatus status) {
auto retrieval = ReadRetrieval(that, status);
crl::on_main([op, result = std::move(retrieval), this]() mutable {
if (result.error != ProtectionError::None) {
if (result.error == ProtectionError::Unavailable) {
discardCredential(op->name);
}
op->done({ .error = result.error });
return;
}
op->credential = std::move(result.credential);
signForEnroll(op);
});
});
});
if (!started) {
AnswerEnroll(std::move(op->done), ProtectionError::Unavailable);
}
}
void WindowsHelloProtection::signForEnroll(
std::shared_ptr<EnrollOperation> op) {
const auto started = base::WinRT::Try([&] {
RequestSign(
op->credential,
ToBuffer(op->challenge),
op->owner
).Completed([op, this](
IAsyncOperation<KeyCredentialOperationResult> that,
AsyncStatus status) {
auto signing = ReadSigning(that, status);
crl::on_main([op, signing = std::move(signing), this]() mutable {
if (signing.error != ProtectionError::None) {
discardCredential(op->name);
op->done({ .error = signing.error });
return;
}
auto wrapKey = DeriveWrapKey(signing.signature, op->wrap.salt);
signing.signature.clear();
if (wrapKey.empty()) {
discardCredential(op->name);
op->done({ .error = ProtectionError::Unavailable });
return;
}
op->done({
.wrap = Wallet::VaultPreparedWrap{
.wrap = std::move(op->wrap),
.wrapKey = std::move(wrapKey),
},
.error = ProtectionError::None,
});
});
});
});
if (!started) {
discardCredential(op->name);
AnswerEnroll(std::move(op->done), ProtectionError::Unavailable);
}
}
void WindowsHelloProtection::unwrap(
not_null<Storage::Account*> local,
Wallet::VaultWrap wrap,
Fn<void(Wallet::ProtectionUnwrapResult)> done) {
auto payload = ParsePayload(wrap.openSecret);
if (!payload) {
AnswerUnwrap(std::move(done), ProtectionError::Corrupt);
return;
}
auto op = std::make_shared<UnwrapOperation>();
op->name = CredentialName(payload->credentialId);
op->challenge = std::move(payload->challenge);
op->owner = PromptOwner();
op->wrap = std::move(wrap);
op->done = std::move(done);
const auto started = base::WinRT::Try([&] {
KeyCredentialManager::OpenAsync(op->name).Completed([op, this](
IAsyncOperation<KeyCredentialRetrievalResult> that,
AsyncStatus status) {
auto retrieval = ReadRetrieval(that, status);
crl::on_main([op, result = std::move(retrieval), this]() mutable {
if (result.error != ProtectionError::None) {
op->done({ .error = result.error });
return;
}
op->credential = std::move(result.credential);
signForUnwrap(op);
});
});
});
if (!started) {
AnswerUnwrap(std::move(op->done), ProtectionError::Unavailable);
}
}
void WindowsHelloProtection::signForUnwrap(
std::shared_ptr<UnwrapOperation> op) {
const auto started = base::WinRT::Try([&] {
RequestSign(
op->credential,
ToBuffer(op->challenge),
op->owner
).Completed([op](
IAsyncOperation<KeyCredentialOperationResult> that,
AsyncStatus status) {
auto signing = ReadSigning(that, status);
crl::on_main([op, signing = std::move(signing)]() mutable {
if (signing.error != ProtectionError::None) {
op->done({ .error = signing.error });
return;
}
const auto wrapKey = DeriveWrapKey(
signing.signature,
op->wrap.salt);
signing.signature.clear();
if (wrapKey.empty()) {
op->done({ .error = ProtectionError::Unavailable });
return;
}
auto key = Wallet::UnwrapVaultKey(op->wrap, wrapKey);
if (!key) {
op->done({ .error = ProtectionError::Corrupt });
return;
}
op->done({
.key = std::move(key),
.error = ProtectionError::None,
});
});
});
});
if (!started) {
AnswerUnwrap(std::move(op->done), ProtectionError::Unavailable);
}
}
void WindowsHelloProtection::remove(
not_null<Storage::Account*> local,
Wallet::VaultWrap wrap,
Fn<void(ProtectionError)> done) {
if (const auto payload = ParsePayload(wrap.openSecret)) {
discardCredential(CredentialName(payload->credentialId));
}
crl::on_main([done = std::move(done)] {
done(ProtectionError::None);
});
}
// Retirement follows the successful replacement or removal of the one
// device wrap. An abandoned enrollment is also safe to retire because no
// committed ring names it. Native deletion stays asynchronous: an already
// missing credential is harmless, and other failures are logged by the
// existing provider path without changing the stored protection.
void WindowsHelloProtection::discardCredential(const winrt::hstring &name) {
base::WinRT::Try([&] {
KeyCredentialManager::DeleteAsync(name).Completed([](
IAsyncAction,
AsyncStatus status) {
if (status != AsyncStatus::Completed) {
LOG(("Wallet Warning: Windows Hello credential deletion "
"ended with async status %1.").arg(int(status)));
}
});
});
}
} // namespace
void RegisterWalletProtectionProvider() {
auto provider = std::make_unique<WindowsHelloProtection>();
const auto raw = provider.get();
Wallet::RegisterProtectionProvider(std::move(provider));
if (Wallet::ProtectionProviderFor(Wallet::VaultKind::WindowsHello) == raw) {
raw->prime();
}
}
} // namespace Platform