Files
keepassxc/src/touchid/TouchID.mm
Jonathan White 80809ace67 Replace all crypto libraries with Botan
Selected the [Botan crypto library](https://github.com/randombit/botan) due to its feature list, maintainer support, availability across all deployment platforms, and ease of use. Also evaluated Crypto++ as a viable candidate, but the additional features of Botan (PKCS#11, TPM, etc) won out.

The random number generator received a backend upgrade. Botan prefers hardware-based RNG's and will provide one if available. This is transparent to KeePassXC and a significant improvement over gcrypt.

Replaced Argon2 library with built-in Botan implementation that supports i, d, and id. This requires Botan 2.11.0 or higher. Also simplified the parameter test across KDF's.

Aligned SymmetricCipher parameters with available modes. All encrypt and decrypt operations are done in-place instead of returning new objects. This allows use of secure vectors in the future with no additional overhead.

Took this opportunity to decouple KeeShare from SSH Agent. Removed leftover code from OpenSSHKey and consolidated the SSH Agent code into the same directory. Removed bcrypt and blowfish inserts since they are provided by Botan.

Additionally simplified KeeShare settings interface by removing raw certificate byte data from the user interface. KeeShare will be further refactored in a future PR.

NOTE: This PR breaks backwards compatibility with KeeShare certificates due to different RSA key storage with Botan. As a result, new "own" certificates will need to be generated and trust re-established.

Removed YKChallengeResponseKeyCLI in favor of just using the original implementation with signal/slots.

Removed TestRandom stub since it was just faking random numbers and not actually using the backend. TestRandomGenerator now uses the actual RNG.

Greatly simplified Secret Service plugin's use of crypto functions with Botan.
2021-04-05 22:56:03 -04:00

286 lines
10 KiB
Plaintext

#define SECURITY_ACCOUNT_PREFIX QString("KeepassXC_TouchID_Keys_")
#include "touchid/TouchID.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "crypto/CryptoHash.h"
#include <Foundation/Foundation.h>
#include <CoreFoundation/CoreFoundation.h>
#include <LocalAuthentication/LocalAuthentication.h>
#include <Security/Security.h>
#include <QCoreApplication>
inline void debug(const char* message, ...)
{
Q_UNUSED(message);
// qWarning(...);
}
inline QString hash(const QString& value)
{
QByteArray result = CryptoHash::hash(value.toUtf8(), CryptoHash::Sha256).toHex();
return QString(result);
}
/**
* Singleton
*/
TouchID& TouchID::getInstance()
{
static TouchID instance; // Guaranteed to be destroyed.
// Instantiated on first use.
return instance;
}
/**
* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that
* protects the database. The encrypted PasswordKey is kept in memory while the
* AES key is stored in the macOS KeyChain protected by TouchID.
*/
bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey)
{
if (databasePath.isEmpty() || passwordKey.isEmpty()) {
// illegal arguments
debug("TouchID::storeKey - Illegal arguments: databasePath = %s, len(passwordKey) = %d",
databasePath.toUtf8().constData(),
passwordKey.length());
return false;
}
if (this->m_encryptedMasterKeys.contains(databasePath)) {
// already stored key for this database
debug("TouchID::storeKey - Already stored key for this database");
return true;
}
// generate random AES 256bit key and IV
QByteArray randomKey = randomGen()->randomArray(32);
QByteArray randomIV = randomGen()->randomArray(16);
SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_CBC, SymmetricCipher::Encrypt, randomKey, randomIV)) {
debug("TouchID::storeKey - Error initializing encryption: %s",
aes256Encrypt.errorString().toUtf8().constData());
return false;
}
// encrypt and keep result in memory
QByteArray encryptedMasterKey = passwordKey;
if (!aes256Encrypt.process(encryptedMasterKey)) {
debug("TouchID::storeKey - Error encrypting: %s", aes256Encrypt.errorString().toUtf8().constData());
return false;
}
// memorize which database the stored key is for
m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey);
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased
// try to delete an existing entry
CFMutableDictionaryRef
query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
// get data from the KeyChain
OSStatus status = SecItemDelete(query);
debug("TouchID::storeKey - Status deleting existing entry: %d", status);
// prepare adding secure entry to the macOS KeyChain
CFErrorRef error = NULL;
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlTouchIDCurrentSet, // depr: kSecAccessControlBiometryCurrentSet,
&error);
if (sacObject == NULL || error != NULL) {
NSError* e = (__bridge NSError*) error;
debug("TouchID::storeKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
return false;
}
CFMutableDictionaryRef attributes =
CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
// prepare data (key) to be stored
QByteArray dataBytes = (randomKey + randomIV).toHex();
CFDataRef valueData =
CFDataCreateWithBytesNoCopy(NULL, reinterpret_cast<UInt8*>(dataBytes.data()), dataBytes.length(), NULL);
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(attributes, kSecValueData, valueData);
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
// add to KeyChain
status = SecItemAdd(attributes, NULL);
debug("TouchID::storeKey - Status adding new entry: %d", status); // read w/ e.g. "security error -50" in shell
CFRelease(sacObject);
CFRelease(attributes);
if (status != errSecSuccess) {
debug("TouchID::storeKey - Not successful, resetting TouchID");
this->m_encryptedMasterKeys.remove(databasePath);
return false;
}
return true;
}
/**
* Checks if an encrypted PasswordKey is available for the given database, tries to
* decrypt it using the KeyChain and if successful, returns it.
*/
bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
{
passwordKey.clear();
if (databasePath.isEmpty()) {
// illegal arguments
debug("TouchID::storeKey - Illegal argument: databasePath = %s", databasePath.toUtf8().constData());
return false;
}
// checks if encrypted PasswordKey is available and is stored for the given database
if (!this->m_encryptedMasterKeys.contains(databasePath)) {
debug("TouchID::getKey - No stored key found");
return false;
}
// query the KeyChain for the AES key
CFMutableDictionaryRef
query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased
NSString* touchPromptMessage =
QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
.toNSString(); // autoreleased
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef) accountName);
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef) touchPromptMessage);
// get data from the KeyChain
CFTypeRef dataTypeRef = NULL;
OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
CFRelease(query);
if (status == errSecUserCanceled) {
// user canceled the authentication, return true with empty key
debug("TouchID::getKey - User canceled authentication");
return true;
} else if (status != errSecSuccess || dataTypeRef == NULL) {
debug("TouchID::getKey - Error retrieving result: %d", status);
return false;
}
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)),
CFDataGetLength(valueData)));
CFRelease(valueData);
// extract AES key and IV from data bytes
QByteArray key = dataBytes.left(32);
QByteArray iv = dataBytes.right(16);
SymmetricCipher aes256Decrypt;
if (!aes256Decrypt.init(SymmetricCipher::Aes256_CBC, SymmetricCipher::Decrypt, key, iv)) {
debug("TouchID::getKey - Error initializing decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
return false;
}
// decrypt PasswordKey from memory using AES
passwordKey = m_encryptedMasterKeys[databasePath];
if (!aes256Decrypt.process(passwordKey)) {
debug("TouchID::getKey - Error decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
return false;
}
return true;
}
/**
* Dynamic check if TouchID is available on the current machine.
*/
bool TouchID::isAvailable()
{
// cache result
if (this->m_available != TOUCHID_UNDEFINED)
return (this->m_available == TOUCHID_AVAILABLE);
@try {
LAContext* context = [[LAContext alloc] init];
bool canAuthenticate = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil];
[context release];
this->m_available = canAuthenticate ? TOUCHID_AVAILABLE : TOUCHID_NOT_AVAILABLE;
return canAuthenticate;
}
@catch (NSException*) {
this->m_available = TOUCHID_NOT_AVAILABLE;
return false;
}
}
typedef enum
{
kTouchIDResultNone,
kTouchIDResultAllowed,
kTouchIDResultFailed
} TouchIDResult;
/**
* Performs a simple authentication using TouchID.
*/
bool TouchID::authenticate(const QString& message) const
{
// message must not be an empty string
QString msg = message;
if (message.length() == 0)
msg = QCoreApplication::translate("DatabaseOpenWidget", "authenticate a privileged operation");
@try {
LAContext* context = [[LAContext alloc] init];
__block TouchIDResult result = kTouchIDResultNone;
NSString* authMessage = msg.toNSString(); // autoreleased
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:authMessage reply:^(BOOL success, NSError* error) {
Q_UNUSED(error);
result = success ? kTouchIDResultAllowed : kTouchIDResultFailed;
CFRunLoopWakeUp(CFRunLoopGetCurrent());
}];
while (result == kTouchIDResultNone)
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
[context release];
return result == kTouchIDResultAllowed;
}
@catch (NSException*) {
return false;
}
}
/**
* Resets the inner state either for all or for the given database
*/
void TouchID::reset(const QString& databasePath)
{
if (databasePath.isEmpty()) {
this->m_encryptedMasterKeys.clear();
return;
}
this->m_encryptedMasterKeys.remove(databasePath);
}