keepassxc
196 строк · 6.6 Кб
1/*
2* Copyright (C) 2022 KeePassXC Team <team@keepassxc.org>
3*
4* This program is free software: you can redistribute it and/or modify
5* it under the terms of the GNU General Public License as published by
6* the Free Software Foundation, either version 2 or (at your option)
7* version 3 of the License.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12* GNU General Public License for more details.
13*
14* You should have received a copy of the GNU General Public License
15* along with this program. If not, see <http://www.gnu.org/licenses/>.
16*/
17
18#include "WindowsHello.h"
19
20#include <Userconsentverifierinterop.h>
21#include <winrt/base.h>
22#include <winrt/windows.foundation.h>
23#include <winrt/windows.security.credentials.h>
24#include <winrt/windows.security.cryptography.h>
25#include <winrt/windows.storage.streams.h>
26
27#include "core/AsyncTask.h"
28#include "crypto/CryptoHash.h"
29#include "crypto/Random.h"
30#include "crypto/SymmetricCipher.h"
31
32#include <QTimer>
33#include <QWindow>
34
35using namespace winrt;
36using namespace Windows::Foundation;
37using namespace Windows::Security::Credentials;
38using namespace Windows::Security::Cryptography;
39using namespace Windows::Storage::Streams;
40
41namespace
42{
43const std::wstring s_winHelloKeyName{L"keepassxc_winhello"};
44int g_promptFocusCount = 0;
45
46void queueSecurityPromptFocus(int delay = 500)
47{
48QTimer::singleShot(delay, [] {
49auto hWnd = ::FindWindowA("Credential Dialog Xaml Host", nullptr);
50if (hWnd) {
51::SetForegroundWindow(hWnd);
52} else if (++g_promptFocusCount <= 3) {
53queueSecurityPromptFocus();
54return;
55}
56g_promptFocusCount = 0;
57});
58}
59
60bool deriveEncryptionKey(QByteArray& challenge, QByteArray& key, QString& error)
61{
62error.clear();
63auto challengeBuffer = CryptographicBuffer::CreateFromByteArray(
64array_view<uint8_t>(reinterpret_cast<uint8_t*>(challenge.data()), challenge.size()));
65
66return AsyncTask::runAndWaitForFuture([&] {
67try {
68// The first time this is used a key-pair will be generated using the common name
69auto result = KeyCredentialManager::RequestCreateAsync(s_winHelloKeyName,
70KeyCredentialCreationOption::FailIfExists)
71.get();
72
73if (result.Status() == KeyCredentialStatus::CredentialAlreadyExists) {
74result = KeyCredentialManager::OpenAsync(s_winHelloKeyName).get();
75} else if (result.Status() != KeyCredentialStatus::Success) {
76error = QObject::tr("Failed to create Windows Hello credential.");
77return false;
78}
79
80const auto signature = result.Credential().RequestSignAsync(challengeBuffer).get();
81if (signature.Status() != KeyCredentialStatus::Success) {
82if (signature.Status() != KeyCredentialStatus::UserCanceled) {
83error = QObject::tr("Failed to sign challenge using Windows Hello.");
84}
85return false;
86}
87
88// Use the SHA-256 hash of the challenge signature as the encryption key
89const auto response = signature.Result();
90CryptoHash hasher(CryptoHash::Sha256);
91hasher.addData({reinterpret_cast<const char*>(response.data()), static_cast<int>(response.Length())});
92key = hasher.result();
93return true;
94} catch (winrt::hresult_error const& ex) {
95error = QString::fromStdString(winrt::to_string(ex.message()));
96return false;
97}
98});
99}
100} // namespace
101
102bool WindowsHello::isAvailable() const
103{
104auto task = concurrency::create_task([] { return KeyCredentialManager::IsSupportedAsync().get(); });
105return task.get();
106}
107
108QString WindowsHello::errorString() const
109{
110return m_error;
111}
112
113bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
114{
115queueSecurityPromptFocus();
116
117// Generate a random challenge that will be signed by Windows Hello
118// to create the key. The challenge is also used as the IV.
119auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
120auto challenge = Random::instance()->randomArray(ivSize);
121QByteArray key;
122if (!deriveEncryptionKey(challenge, key, m_error)) {
123return false;
124}
125
126// Encrypt the data using AES-256-CBC
127SymmetricCipher cipher;
128if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, challenge)) {
129m_error = QObject::tr("Failed to init KeePassXC crypto.");
130return false;
131}
132QByteArray encrypted = data;
133if (!cipher.finish(encrypted)) {
134m_error = QObject::tr("Failed to encrypt key data.");
135return false;
136}
137
138// Prepend the challenge/IV to the encrypted data
139encrypted.prepend(challenge);
140m_encryptedKeys.insert(dbUuid, encrypted);
141return true;
142}
143
144bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
145{
146data.clear();
147if (!hasKey(dbUuid)) {
148m_error = QObject::tr("Failed to get Windows Hello credential.");
149return false;
150}
151
152queueSecurityPromptFocus();
153
154// Read the previously used challenge and encrypted data
155auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
156const auto& keydata = m_encryptedKeys.value(dbUuid);
157auto challenge = keydata.left(ivSize);
158auto encrypted = keydata.mid(ivSize);
159QByteArray key;
160
161if (!deriveEncryptionKey(challenge, key, m_error)) {
162return false;
163}
164
165// Decrypt the data using the generated key and IV from above
166SymmetricCipher cipher;
167if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) {
168m_error = QObject::tr("Failed to init KeePassXC crypto.");
169return false;
170}
171
172// Store the decrypted data into the passed parameter
173data = encrypted;
174if (!cipher.finish(data)) {
175data.clear();
176m_error = QObject::tr("Failed to decrypt key data.");
177return false;
178}
179
180return true;
181}
182
183void WindowsHello::reset(const QUuid& dbUuid)
184{
185m_encryptedKeys.remove(dbUuid);
186}
187
188bool WindowsHello::hasKey(const QUuid& dbUuid) const
189{
190return m_encryptedKeys.contains(dbUuid);
191}
192
193void WindowsHello::reset()
194{
195m_encryptedKeys.clear();
196}
197