keepassxc

Форк
0
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
22
inline void debug(const char *message, ...)
23
{
24
   Q_UNUSED(message);
25
}
26
#endif
27

28
inline std::string StatusToErrorMessage(OSStatus status)
29
{
30
   CFStringRef text = SecCopyErrorMessageString(status, NULL);
31
   if (!text) {
32
      return std::to_string(status);
33
   }
34

35
   auto msg = CFStringGetCStringPtr(text, kCFStringEncodingUTF8);
36
   std::string result;
37
   if (msg) {
38
       result = msg;
39
   }
40
   CFRelease(text);
41
   return result;
42
}
43

44
inline void LogStatusError(const char *message, OSStatus status)
45
{
46
   if (!status) {
47
      return;
48
   }
49

50
   std::string msg = StatusToErrorMessage(status);
51
   debug("%s: %s", message, msg.c_str());
52
}
53

54
inline CFMutableDictionaryRef makeDictionary() {
55
   return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
56
}
57

58
//! Try to delete an existing keychain entry
59
void TouchID::deleteKeyEntry(const QString& accountName)
60
{
61
   NSString* nsAccountName = accountName.toNSString(); // The NSString is released by Qt
62

63
   // try to delete an existing entry
64
   CFMutableDictionaryRef query = makeDictionary();
65
   CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
66
   CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) nsAccountName);
67
   CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
68

69
   // get data from the KeyChain
70
   OSStatus status = SecItemDelete(query);
71
   LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
72
}
73

74
QString TouchID::databaseKeyName(const QUuid& dbUuid)
75
{
76
   static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
77
   return keyPrefix + dbUuid.toString();
78
}
79

80
QString TouchID::errorString() const
81
{
82
    // TODO
83
    return "";
84
}
85

86
void TouchID::reset()
87
{
88
    m_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
 */
96
bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
97
{
98
    if (passwordKey.isEmpty()) {
99
        debug("TouchID::setKey - illegal arguments");
100
        return false;
101
    }
102

103
    if (m_encryptedMasterKeys.contains(dbUuid)) {
104
        debug("TouchID::setKey - Already stored key for this database");
105
        return true;
106
    }
107

108
    // generate random AES 256bit key and IV
109
    QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
110
    QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
111

112
    SymmetricCipher aes256Encrypt;
113
    if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
114
        debug("TouchID::setKey - AES initialisation failed");
115
        return false;
116
    }
117

118
    // encrypt and keep result in memory
119
    QByteArray encryptedMasterKey = passwordKey;
120
    if (!aes256Encrypt.finish(encryptedMasterKey)) {
121
        debug("TouchID::getKey - AES encrypt failed: %s", aes256Encrypt.errorString().toUtf8().constData());
122
        return false;
123
    }
124

125
    const QString keyName = databaseKeyName(dbUuid);
126

127
    deleteKeyEntry(keyName); // Try to delete the existing key entry
128

129
    // prepare adding secure entry to the macOS KeyChain
130
    CFErrorRef 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
135
    SecAccessControlCreateFlags accessControlFlags = 0;
136
    if (isTouchIdAvailable()) {
137
#if XC_COMPILER_SUPPORT(APPLE_BIOMETRY)
138
       // Prefer the non-deprecated flag when available
139
       accessControlFlags = kSecAccessControlBiometryCurrentSet;
140
#elif XC_COMPILER_SUPPORT(TOUCH_ID)
141
       accessControlFlags = kSecAccessControlTouchIDCurrentSet;
142
#endif
143
    }
144

145
   if (isWatchAvailable()) {
146
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
147
      accessControlFlags = accessControlFlags | kSecAccessControlOr | kSecAccessControlWatch;
148
#endif
149
   }
150

151
   SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(
152
       kCFAllocatorDefault, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, accessControlFlags, &error);
153

154
    if (sacObject == NULL || error != NULL) {
155
        NSError* e = (__bridge NSError*) error;
156
        debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
157
        return false;
158
    }
159

160
    NSString *accountName = keyName.toNSString(); // The NSString is released by Qt
161

162
    // prepare data (key) to be stored
163
    QByteArray keychainKeyValue = (randomKey + randomIV).toHex();
164
    CFDataRef keychainValueData =
165
        CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, reinterpret_cast<UInt8 *>(keychainKeyValue.data()),
166
                                    keychainKeyValue.length(), kCFAllocatorDefault);
167

168
    CFMutableDictionaryRef attributes = makeDictionary();
169
    CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
170
    CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
171
    CFDictionarySetValue(attributes, kSecValueData, (__bridge CFDataRef) keychainValueData);
172
    CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
173
    CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
174
    CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
175

176
    // add to KeyChain
177
    OSStatus status = SecItemAdd(attributes, NULL);
178
    LogStatusError("TouchID::setKey - Status adding new entry", status);
179

180
    CFRelease(sacObject);
181
    CFRelease(attributes);
182

183
    if (status != errSecSuccess) {
184
        return false;
185
    }
186

187
    // Cleanse the key information from the memory
188
    Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
189
    Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
190

191
    // memorize which database the stored key is for
192
    m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
193
    debug("TouchID::setKey - Success!");
194
    return 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
 */
201
bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
202
{
203
    passwordKey.clear();
204

205
    if (!hasKey(dbUuid)) {
206
        debug("TouchID::getKey - No stored key found");
207
        return false;
208
    }
209

210
    // query the KeyChain for the AES key
211
    CFMutableDictionaryRef query = makeDictionary();
212

213
    const QString keyName = databaseKeyName(dbUuid);
214
    NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
215
    NSString* touchPromptMessage =
216
        QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
217
            .toNSString();  // The NSString is released by Qt
218

219
    CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
220
    CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
221
    CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
222
    CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
223

224
    // get data from the KeyChain
225
    CFTypeRef dataTypeRef = NULL;
226
    OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
227
    CFRelease(query);
228

229
    if (status == errSecUserCanceled) {
230
        // user canceled the authentication, return true with empty key
231
        debug("TouchID::getKey - User canceled authentication");
232
        return true;
233
    } else if (status != errSecSuccess || dataTypeRef == NULL) {
234
        LogStatusError("TouchID::getKey - key query error", status);
235
        return false;
236
    }
237

238
    CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
239
    QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
240
                                                          CFDataGetLength(valueData)));
241
    CFRelease(dataTypeRef);
242

243
    // extract AES key and IV from data bytes
244
    QByteArray key = dataBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
245
    QByteArray iv = dataBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
246

247
    SymmetricCipher aes256Decrypt;
248
    if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, iv)) {
249
        debug("TouchID::getKey - AES initialization failed");
250
        return false;
251
    }
252

253
    // decrypt PasswordKey from memory using AES
254
    passwordKey = m_encryptedMasterKeys[dbUuid];
255
    if (!aes256Decrypt.finish(passwordKey)) {
256
        passwordKey.clear();
257
        debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
258
        return false;
259
    }
260

261
    // Cleanse the key information from the memory
262
    Botan::secure_scrub_memory(key.data(), key.size());
263
    Botan::secure_scrub_memory(iv.data(), iv.size());
264

265
    return true;
266
}
267

268
bool TouchID::hasKey(const QUuid& dbUuid) const
269
{
270
    return 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.
280
bool TouchID::isWatchAvailable()
281
{
282
#if XC_COMPILER_SUPPORT(WATCH_UNLOCK)
283
   @try {
284
      LAContext *context = [[LAContext alloc] init];
285

286
      LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithWatch;
287
      NSError *error;
288

289
      bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
290
      [context release];
291
      if (error) {
292
         debug("Apple Wach available: %d (%ld / %s / %s)", canAuthenticate,
293
               (long)error.code, error.description.UTF8String,
294
               error.localizedDescription.UTF8String);
295
      } else {
296
          debug("Apple Wach available: %d", canAuthenticate);
297
      }
298
      return canAuthenticate;
299
   } @catch (NSException *) {
300
      return false;
301
   }
302
#else
303
   return false;
304
#endif
305
}
306

307
//! @return true if Touch ID is available for authentication.
308
bool TouchID::isTouchIdAvailable()
309
{
310
#if XC_COMPILER_SUPPORT(TOUCH_ID)
311
   @try {
312
      LAContext *context = [[LAContext alloc] init];
313

314
      LAPolicy policyCode = LAPolicyDeviceOwnerAuthenticationWithBiometrics;
315
      NSError *error;
316

317
      bool canAuthenticate = [context canEvaluatePolicy:policyCode error:&error];
318
      [context release];
319
      if (error) {
320
         debug("Touch ID available: %d (%ld / %s / %s)", canAuthenticate,
321
               (long)error.code, error.description.UTF8String,
322
               error.localizedDescription.UTF8String);
323
      } else {
324
          debug("Touch ID available: %d", canAuthenticate);
325
      }
326
      return canAuthenticate;
327
   } @catch (NSException *) {
328
      return false;
329
   }
330
#else
331
   return false;
332
#endif
333
}
334

335
//! @return true if either TouchID or Apple Watch is available at the moment.
336
bool 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.
341
   return  isWatchAvailable() || isTouchIdAvailable();
342
}
343

344
/**
345
 * Resets the inner state either for all or for the given database
346
 */
347
void TouchID::reset(const QUuid& dbUuid)
348
{
349
    m_encryptedMasterKeys.remove(dbUuid);
350
}
351

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.