keepassxc
350 строк · 11.9 Кб
1#include "quickunlock/TouchID.h"
2
3#include "crypto/Random.h"
4#include "crypto/SymmetricCipher.h"
5#include "crypto/CryptoHash.h"
6#include "config-keepassx.h"
7
8#include <botan/mem_ops.h>
9
10#include <Foundation/Foundation.h>
11#include <CoreFoundation/CoreFoundation.h>
12#include <LocalAuthentication/LocalAuthentication.h>
13#include <Security/Security.h>
14
15#include <QCoreApplication>
16#include <QString>
17
18#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0
19#if TOUCH_ID_ENABLE_DEBUG_LOGS()
20#define debug(...) qWarning(__VA_ARGS__)
21#else
22inline void debug(const char *message, ...)
23{
24Q_UNUSED(message);
25}
26#endif
27
28inline std::string StatusToErrorMessage(OSStatus status)
29{
30CFStringRef text = SecCopyErrorMessageString(status, NULL);
31if (!text) {
32return std::to_string(status);
33}
34
35auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
36std::string result;
37if (msg) {
38result = msg;
39}
40CFRelease(text);
41return result;
42}
43
44inline void LogStatusError(const char *message, OSStatus status)
45{
46if (!status) {
47return;
48}
49
50std::string msg = StatusToErrorMessage(status);
51debug("%s: %s", message, msg.c_str());
52}
53
54inline CFMutableDictionaryRef makeDictionary() {
55return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
56}
57
58//! Try to delete an existing keychain entry
59void TouchID::deleteKeyEntry(const QString& accountName)
60{
61NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt
62
63// try to delete an existing entry
64CFMutableDictionaryRef query = makeDictionary();
65CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
66CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName);
67CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
68
69// get data from the KeyChain
70OSStatus status = SecItemDelete(query);
71LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
72}
73
74QString TouchID::databaseKeyName(const QUuid& dbUuid)
75{
76static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
77return keyPrefix + dbUuid.toString();
78}
79
80QString TouchID::errorString() const
81{
82// TODO
83return "";
84}
85
86void TouchID::reset()
87{
88m_encryptedMasterKeys.clear();
89}
90
91/**
92* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that
93* protects the database. The encrypted PasswordKey is kept in memory while the
94* AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
95*/
96bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
97{
98if (passwordKey.isEmpty()) {
99debug("TouchID::setKey - illegal arguments");
100return false;
101}
102
103if (m_encryptedMasterKeys.contains(dbUuid)) {
104debug("TouchID::setKey - Already stored key for this database");
105return true;
106}
107
108// generate random AES 256bit key and IV
109QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
110QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
111
112SymmetricCipher aes256Encrypt;
113if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
114debug("TouchID::setKey - AES initialisation failed");
115return false;
116}
117
118// encrypt and keep result in memory
119QByteArray encryptedMasterKey = passwordKey;
120if (!aes256Encrypt.finish(encryptedMasterKey)) {
121debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
122return false;
123}
124
125const QString keyName = databaseKeyName(dbUuid);
126
127deleteKeyEntry(keyName); // Try to delete the existing key entry
128
129// prepare adding secure entry to the macOS KeyChain
130CFErrorRef error = NULL;
131
132// We need both runtime and compile time checks here to solve the following problems:
133// - Not all flags are available in all OS versions, so we have to check it at compile time
134// - Requesting Biometry/TouchID when to fingerprint sensor is available will result in runtime error
135SecAccessControlCreateFlags accessControlFlags = 0;
136if (isTouchIdAvailable()) {
137#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
138// Prefer the non-deprecated flag when available
139accessControlFlags = kSecAccessControlBiometryCurrentSet;
140#elif XC_COMPILER_SUPPORT(TOUCH_ID)
141accessControlFlags = kSecAccessControlTouchIDCurrentSet;
142#endif
143}
144
145if (isWatchAvailable()) {
146#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
147accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
148#endif
149}
150
151SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
152kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
153
154if (sacObject == NULL || error != NULL) {
155NSError* e = (__bridge NSError*) error;
156debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
157return false;
158}
159
160NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
161
162// prepare data (key) to be stored
163QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
164CFDataRef keychainValueData =
165CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
166keychainKeyValue.length(), kCFAllocatorDefault);
167
168CFMutableDictionaryRef attributes = makeDictionary();
169CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
170CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
171CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
172CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
173CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
174CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
175
176// add to KeyChain
177OSStatus status = SecItemAdd(attributes, NULL);
178LogStatusError("TouchID::setKey - Status adding new entry", status);
179
180CFRelease(sacObject);
181CFRelease(attributes);
182
183if (status != errSecSuccess) {
184return false;
185}
186
187// Cleanse the key information from the memory
188Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
189Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
190
191// memorize which database the stored key is for
192m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
193debug("TouchID::setKey - Success!");
194return true;
195}
196
197/**
198* Checks if an encrypted PasswordKey is available for the given database, tries to
199* decrypt it using the KeyChain and if successful, returns it.
200*/
201bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
202{
203passwordKey.clear();
204
205if (!hasKey(dbUuid)) {
206debug("TouchID::getKey - No stored key found");
207return false;
208}
209
210// query the KeyChain for the AES key
211CFMutableDictionaryRef query = makeDictionary();
212
213const QString keyName = databaseKeyName(dbUuid);
214NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
215NSString* touchPromptMessage =
216QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
217.toNSString(); // The NSString is released by Qt
218
219CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
220CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
221CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
222CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
223
224// get data from the KeyChain
225CFTypeRef dataTypeRef = NULL;
226OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
227CFRelease(query);
228
229if (status == errSecUserCanceled) {
230// user canceled the authentication, return true with empty key
231debug("TouchID::getKey - User canceled authentication");
232return true;
233} else if (status != errSecSuccess || dataTypeRef == NULL) {
234LogStatusError("TouchID::getKey - key query error", status);
235return false;
236}
237
238CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
239QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
240CFDataGetLength(valueData)));
241CFRelease(dataTypeRef);
242
243// extract AES key and IV from data bytes
244QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
245QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
246
247SymmetricCipher aes256Decrypt;
248if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
249debug("TouchID::getKey - AES initialization failed");
250return false;
251}
252
253// decrypt PasswordKey from memory using AES
254passwordKey = m_encryptedMasterKeys[dbUuid];
255if (!aes256Decrypt.finish(passwordKey)) {
256passwordKey.clear();
257debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
258return false;
259}
260
261// Cleanse the key information from the memory
262Botan::secure_scrub_memory(key.data(), key.size());
263Botan::secure_scrub_memory(iv.data(), iv.size());
264
265return true;
266}
267
268bool TouchID::hasKey(const QUuid& dbUuid) const
269{
270return m_encryptedMasterKeys.contains(dbUuid);
271}
272
273// TODO: Both functions below should probably handle the returned errors to
274// provide more information on availability. E.g.: the closed laptop lid results
275// in an error (because touch id is not unavailable). That error could be
276// displayed to the user when we first check for availability instead of just
277// hiding the checkbox.
278
279//! @return true if Apple Watch is available for authentication.
280bool TouchID::isWatchAvailable()
281{
282#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
283@try {
284LAContext *context = [[LAContext alloc] init];
285
286LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
287NSError *error;
288
289bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
290[context release];
291if (error) {
292debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
293(long)error.code, error.description.UTF8String,
294error.localizedDescription.UTF8String);
295} else {
296debug("Apple Wach available: %d", canAuthenticate);
297}
298return canAuthenticate;
299} @catch (NSException *) {
300return false;
301}
302#else
303return false;
304#endif
305}
306
307//! @return true if Touch ID is available for authentication.
308bool TouchID::isTouchIdAvailable()
309{
310#if XC_COMPILER_SUPPORT(TOUCH_ID)
311@try {
312LAContext *context = [[LAContext alloc] init];
313
314LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
315NSError *error;
316
317bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
318[context release];
319if (error) {
320debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
321(long)error.code, error.description.UTF8String,
322error.localizedDescription.UTF8String);
323} else {
324debug("Touch ID available: %d", canAuthenticate);
325}
326return canAuthenticate;
327} @catch (NSException *) {
328return false;
329}
330#else
331return false;
332#endif
333}
334
335//! @return true if either TouchID or Apple Watch is available at the moment.
336bool TouchID::isAvailable() const
337{
338// note: we cannot cache the check results because the configuration
339// is dynamic in its nature. User can close the laptop lid or take off
340// the watch, thus making one (or both) of the authentication types unavailable.
341return isWatchAvailable() || isTouchIdAvailable();
342}
343
344/**
345* Resets the inner state either for all or for the given database
346*/
347void TouchID::reset(const QUuid& dbUuid)
348{
349m_encryptedMasterKeys.remove(dbUuid);
350}
351