feat(@desktop/general): (macos) Keychain manager added

LocalAuthentication class - used to authenticate OS' logged user (using Touch Id)
Keychain class - able to store/read/remove item from the Keychain
KeychainManager class - manages the flow of storing/reading/removing an item from
the Keychain using own sync/async methods

This change is required as part of the feature issue-2675
This commit is contained in:
Sale Djenic 2021-09-07 10:35:41 +02:00 committed by Michał
parent 4d10692572
commit 5dc926f665
12 changed files with 688 additions and 3 deletions

View File

@ -1027,6 +1027,19 @@ DOS_API void dos_qsettings_delete(DosQSettings* vptr);
#pragma endregion
#pragma region KeychainManager exposed methods
DOS_API DosKeychainManager* dos_keychainmanager_create(const char* service,
const char* authenticationReason);
DOS_API char* dos_keychainmanager_read_data_sync(DosKeychainManager* vptr,
const char* key);
DOS_API void dos_keychainmanager_read_data_async(DosKeychainManager* vptr,
const char* key);
DOS_API void dos_keychainmanager_store_data_async(DosKeychainManager* vptr,
const char* key, const char* data);
DOS_API void dos_keychainmanager_delete_data_async(DosKeychainManager* vptr,
const char* key);
DOS_API void dos_keychainmanager_delete(DosKeychainManager* vptr);
#pragma endregion

View File

@ -107,6 +107,8 @@ typedef void DosEvent;
/// A pointer to a os notification object which is actualy a QObject
typedef DosQObject DosOSNotification;
/// A pointer to a keychain manager object which is actualy a QObject
typedef DosQObject DosKeychainManager;
/// A pixmap callback to be supplied to an image provider

View File

@ -0,0 +1,40 @@
#ifndef KEYCHAIN_H
#define KEYCHAIN_H
#include <QObject>
namespace Status
{
class Keychain : public QObject
{
Q_OBJECT
public:
enum Error {
NoError=0,
EntryNotFound,
CouldNotDeleteEntry,
AccessDeniedByUser,
AccessDenied,
NoBackendAvailable,
NotImplemented,
OtherError
};
Keychain(const QString& service, QObject *parent = nullptr);
void readItem(const QString& key);
void writeItem(const QString& key, const QString& data);
void deleteItem(const QString& key);
signals:
void success(QString data);
void error(int error, const QString& errorString);
private:
QString m_service;
};
}
#endif

View File

@ -0,0 +1,96 @@
#ifndef KEYCHAIN_MANAGER_H
#define KEYCHAIN_MANAGER_H
#include "Keychain.h"
#include "LocalAuthentication.h"
namespace Status
{
class KeychainManager : public QObject
{
Q_OBJECT
public:
/*!
* Constructor defining name of the service for storing in the Keychain
* and the reason for requesting authorisation via touch id.
*
* @param service Service name used in Keychain.
* @param authenticationReason Reason for requestion touch id authorization.
*/
KeychainManager(const QString& service,
const QString& authenticationReason, QObject* parent = nullptr);
/*!
* Synchronously reads @data stored in the Keychain under the @key and
* returns stored @data. In case of any error an empty string will be
* returned and error signal will be emitted.
*
* @param key Key which is stored in the Keychain.
*/
QString readDataSync(const QString& key) const;
/*!
* Asynchronously reads @data stored in the Keychain under the @key.
* Onces it's read success signal will be emitted containing read data,
* otherwise error signal will be emitted.
*
* @param key Key which is stored in the Keychain.
*/
void readDataAsync(const QString& key);
/*!
* Asynchronously stores @data under the @key in the Keychain.
* Onces @data is stored success signal will be emitted, otherwise error
* signal will be emitted.
*
* @param key Key which is stored in the Keychain.
* @param data Data which is stored in the Keychain.
*/
void storeDataAsync(const QString& key, const QString& data);
/*!
* Asynchronously deletes @data stored in the Keychain under the @key.
* Onces it's deleted success signal will be emitted, otherwise error
* signal will be emitted.
*
* @param key Key which is stored in the Keychain.
*/
void deleteDataAsync(const QString& key);
signals:
/*!
* Notifies that action was performed successfully and in case of asyc
* read contains read @data, in other cases @data param is empty.
*
* @param data Data read from the Keychain.
*/
void success(QString data);
/*!
* Notifies that an error with @error code and @errorString description
* occured.
*
* @param type Determins origin of the error ("authentication" or "keychain")
* @param code Error code.
* @param errorString Error description.
*/
void error(QString type, int code, const QString& errorString);
#ifdef Q_OS_MACOS
private:
void process(const std::function<void()> action);
QString readDataSyncMacOs(const QString& key) const;
void readDataAsyncMacOs(const QString& key);
void storeDataAsyncMacOs(const QString& key, const QString& data);
void deleteDataAsyncMacOs(const QString& key);
private:
QString m_authenticationReason;
std::unique_ptr<LocalAuthentication> m_localAuth;
std::unique_ptr<Keychain> m_keychain;
QMetaObject::Connection m_actionConnection;
#endif
};
}
#endif

View File

@ -0,0 +1,33 @@
#ifndef LOCAL_AUTHENTICATION_H
#define LOCAL_AUTHENTICATION_H
#include <QObject>
namespace Status
{
class LocalAuthentication : public QObject
{
Q_OBJECT
public:
enum Error {
Domain=0,
AppCanceled,
SystemCanceled,
UserCanceled,
TouchIdNotAvailable,
TouchIdNotConfigured,
WrongCredentials,
OtherError
};
void runAuthentication(const QString& authenticationReason);
signals:
void success();
void error(int error, const QString& errorString);
};
}
#endif

View File

@ -66,6 +66,7 @@
#include "DOtherSide/Status/DockShowAppEvent.h"
#include "DOtherSide/Status/OSThemeEvent.h"
#include "DOtherSide/Status/OSNotification.h"
#include "DOtherSide/Status/KeychainManager.h"
#include "DOtherSide/DosSpellchecker.h"
namespace {
@ -1459,6 +1460,60 @@ void dos_qsettings_delete(DosQSettings* vptr)
}
#pragma endregion
#pragma region KeychainManager
DosKeychainManager* dos_keychainmanager_create(const char* service,
const char* authenticationReason)
{
return new Status::KeychainManager(QString(service), QString(authenticationReason));
}
char* dos_keychainmanager_read_data_sync(DosKeychainManager* vptr,
const char* key)
{
auto obj = static_cast<Status::KeychainManager*>(vptr);
if(obj)
{
return convert_to_cstring(obj->readDataSync(QString(key)));
}
return convert_to_cstring(QString());
}
void dos_keychainmanager_read_data_async(DosKeychainManager* vptr,
const char* key)
{
auto obj = static_cast<Status::KeychainManager*>(vptr);
if(obj)
obj->readDataAsync(QString(key));
}
void dos_keychainmanager_store_data_async(DosKeychainManager* vptr,
const char* key, const char* data)
{
auto obj = static_cast<Status::KeychainManager*>(vptr);
if(obj)
{
obj->storeDataAsync(QString(key), QString(data));
}
}
void dos_keychainmanager_delete_data_async(DosKeychainManager* vptr,
const char* key)
{
auto obj = static_cast<Status::KeychainManager*>(vptr);
if(obj)
obj->deleteDataAsync(QString(key));
}
void dos_keychainmanager_delete(DosKeychainManager* vptr)
{
auto qobject = static_cast<QObject*>(vptr);
if(qobject)
qobject->deleteLater();
}
#pragma endregion
char* dos_to_local_file(const char* fileUrl)
{
return convert_to_cstring(QUrl(QString::fromUtf8(fileUrl)).toLocalFile());
@ -1467,4 +1522,4 @@ char* dos_to_local_file(const char* fileUrl)
char* dos_from_local_file(const char* filePath)
{
return convert_to_cstring(QUrl::fromLocalFile(QString::fromUtf8(filePath)).toString());
}
}

View File

@ -0,0 +1,162 @@
#include "DOtherSide/Status/Keychain.h"
#import <Foundation/Foundation.h>
#import <Security/Security.h>
using namespace Status;
struct ErrorDescription
{
Keychain::Error code;
QString message;
ErrorDescription(Keychain::Error code, const QString &message)
: code(code)
, message(message)
{}
static ErrorDescription fromStatus(OSStatus status)
{
switch(status) {
case errSecSuccess:
return ErrorDescription(Keychain::NoError,
"No error");
case errSecItemNotFound:
return ErrorDescription(Keychain::EntryNotFound,
"The specified item could not be found in the keychain");
case errSecUserCanceled:
return ErrorDescription(Keychain::AccessDeniedByUser,
"User canceled the operation");
case errSecInteractionNotAllowed:
return ErrorDescription(Keychain::AccessDenied,
"User interaction is not allowed");
case errSecNotAvailable:
return ErrorDescription(Keychain::AccessDenied,
"No keychain is available. You may need to restart your computer");
case errSecAuthFailed:
return ErrorDescription(Keychain::AccessDenied,
"The user name or passphrase you entered is not correct");
case errSecVerifyFailed:
return ErrorDescription(Keychain::AccessDenied,
"A cryptographic verification failure has occurred");
case errSecUnimplemented:
return ErrorDescription(Keychain::NotImplemented,
"Function or operation not implemented");
case errSecIO:
return ErrorDescription(Keychain::OtherError,
"I/O error");
case errSecOpWr:
return ErrorDescription(Keychain::OtherError,
"Already open with with write permission");
case errSecParam:
return ErrorDescription(Keychain::OtherError,
"Invalid parameters passed to a function");
case errSecAllocate:
return ErrorDescription(Keychain::OtherError,
"Failed to allocate memory");
case errSecBadReq:
return ErrorDescription(Keychain::OtherError,
"Bad parameter or invalid state for operation");
case errSecInternalComponent:
return ErrorDescription(Keychain::OtherError,
"An internal component failed");
case errSecDuplicateItem:
return ErrorDescription(Keychain::OtherError,
"The specified item already exists in the keychain");
case errSecDecode:
return ErrorDescription(Keychain::OtherError,
"Unable to decode the provided data");
}
return ErrorDescription(Keychain::OtherError, "Unknown error");
}
};
Keychain::Keychain(const QString& service, QObject *parent)
: QObject(parent)
, m_service(service)
{}
void Keychain::readItem(const QString& key)
{
NSDictionary *const query = @{
(__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword,
(__bridge id) kSecAttrService: (__bridge NSString *) m_service.toCFString(),
(__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(),
(__bridge id) kSecReturnData: @YES,
};
CFTypeRef dataRef = nil;
const OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, &dataRef);
if (status == errSecSuccess) {
QByteArray data;
if (dataRef)
data = QByteArray::fromCFData((CFDataRef) dataRef);
emit success(QString::fromUtf8(data));
} else {
const ErrorDescription ed = ErrorDescription::fromStatus(status);
emit error(ed.code,
QString("Could not retrieve private key from keystore: %1").arg(ed.message));
}
if (dataRef)
[dataRef release];
}
void Keychain::writeItem(const QString& key, const QString& data)
{
NSDictionary *const query = @{
(__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword,
(__bridge id) kSecAttrService: (__bridge NSString *) m_service.toCFString(),
(__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(),
};
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef) query, nil);
QByteArray baData = data.toUtf8();
if (status == errSecSuccess) {
NSDictionary *const update = @{
(__bridge id) kSecValueData: (__bridge NSData *) baData.toCFData(),
};
status = SecItemUpdate((__bridge CFDictionaryRef) query, (__bridge CFDictionaryRef) update);
} else {
NSDictionary *const insert = @{
(__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword,
(__bridge id) kSecAttrService: (__bridge NSString *) m_service.toCFString(),
(__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(),
(__bridge id) kSecValueData: (__bridge NSData *) baData.toCFData(),
};
status = SecItemAdd((__bridge CFDictionaryRef) insert, nil);
}
if (status == errSecSuccess) {
emit success(QString());
} else {
const ErrorDescription ed = ErrorDescription::fromStatus(status);
emit error(ed.code,
QString("Could not store data in settings: %1").arg(ed.message));
}
}
void Keychain::deleteItem(const QString& key)
{
const NSDictionary *const query = @{
(__bridge id) kSecClass: (__bridge id) kSecClassGenericPassword,
(__bridge id) kSecAttrService: (__bridge NSString *) m_service.toCFString(),
(__bridge id) kSecAttrAccount: (__bridge NSString *) key.toCFString(),
};
const OSStatus status = SecItemDelete((__bridge CFDictionaryRef) query);
if (status == errSecSuccess) {
emit success(QString());
} else {
const ErrorDescription ed = ErrorDescription::fromStatus(status);
emit error(ed.code,
QString("Could not remove private key from keystore: %1").arg(ed.message));
}
}

View File

@ -0,0 +1,57 @@
#include "DOtherSide/Status/KeychainManager.h"
using namespace Status;
KeychainManager::KeychainManager(const QString& service,
const QString& authenticationReason, QObject* parent)
: QObject(parent)
{
#ifdef Q_OS_MACOS
m_authenticationReason = authenticationReason;
m_localAuth = std::unique_ptr<LocalAuthentication>(new LocalAuthentication());
m_keychain = std::unique_ptr<Keychain>(new Keychain(service));
connect(m_localAuth.get(), &LocalAuthentication::error, [this](int code, const QString& errorString){
emit error("authentication", code, errorString);
});
connect(m_keychain.get(), &Keychain::success, this, &KeychainManager::success);
connect(m_keychain.get(), &Keychain::error, [this](int code, const QString& errorString){
emit error("keychain", code, errorString);
});
#else
// Marked params unused until we need them for Win/Linux.
Q_UNUSED(authenticationReason);
Q_UNUSED(service);
#endif
}
QString KeychainManager::readDataSync(const QString& key) const
{
#ifdef Q_OS_MACOS
return readDataSyncMacOs(key);
#endif
return QString();
}
void KeychainManager::readDataAsync(const QString& key)
{
#ifdef Q_OS_MACOS
readDataAsyncMacOs(key);
#endif
}
void KeychainManager::storeDataAsync(const QString& key, const QString& data)
{
#ifdef Q_OS_MACOS
storeDataAsyncMacOs(key, data);
#endif
}
void KeychainManager::deleteDataAsync(const QString& key)
{
#ifdef Q_OS_MACOS
deleteDataAsyncMacOs(key);
#endif
}

View File

@ -0,0 +1,75 @@
#include "DOtherSide/Status/KeychainManager.h"
#include <QEventLoop>
using namespace Status;
QString KeychainManager::readDataSyncMacOs(const QString& key) const
{
QString storedData;
QEventLoop loop;
auto onAuthenticationSuccess = [this, &key](){
m_keychain->readItem(key);
};
auto onReadItemSuccess = [&loop, &storedData](QString data){
storedData = data;
loop.quit();
};
connect(m_localAuth.get(), &LocalAuthentication::success, onAuthenticationSuccess);
connect(m_keychain.get(), &Keychain::success, onReadItemSuccess);
connect(m_localAuth.get(), &LocalAuthentication::error, [this, &loop](int code, const QString& errorString){
Q_UNUSED(code)
Q_UNUSED(errorString)
loop.quit();
});
connect(m_keychain.get(), &Keychain::error, [this, &loop](int code, const QString& errorString){
Q_UNUSED(code)
Q_UNUSED(errorString)
loop.quit();
});
m_localAuth->runAuthentication(m_authenticationReason);
loop.exec();
return storedData;
}
void KeychainManager::readDataAsyncMacOs(const QString& key)
{
auto readAction = [this, key](){
m_keychain->readItem(key);
};
process(readAction);
}
void KeychainManager::storeDataAsyncMacOs(const QString& key, const QString& data)
{
auto writeAction = [this, key, data](){
m_keychain->writeItem(key, data);
};
process(writeAction);
}
void KeychainManager::deleteDataAsyncMacOs(const QString& key)
{
auto deleteAction = [this, key](){
m_keychain->deleteItem(key);
};
process(deleteAction);
}
void KeychainManager::process(const std::function<void()> action)
{
disconnect(m_actionConnection);
m_actionConnection = connect(m_localAuth.get(), &LocalAuthentication::success, action);
m_localAuth->runAuthentication(m_authenticationReason);
}

View File

@ -0,0 +1,152 @@
#include "DOtherSide/Status/LocalAuthentication.h"
#include <AvailabilityMacros.h>
#import <LocalAuthentication/LocalAuthentication.h>
using namespace Status;
struct ErrorDescription
{
LocalAuthentication::Error code;
QString message;
ErrorDescription(LocalAuthentication::Error code, const QString &message)
: code(code)
, message(message)
{}
static ErrorDescription fromLAError(NSInteger err)
{
NSProcessInfo *pInfo = [NSProcessInfo processInfo];
NSOperatingSystemVersion info = [pInfo operatingSystemVersion];
#ifdef __MAC_OS_X_VERSION_MAX_ALLOWED
#if defined MAC_OS_VERSION_11_2 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_VERSION_11_2
switch(err) {
case LAErrorBiometryDisconnected: /* MacOs 11.2+ */
return ErrorDescription(LocalAuthentication::OtherError,
"The device supports biometry only using a removable accessory, but the paired accessory isnt connected");
case LAErrorBiometryNotPaired: /* MacOs 11.2+ */
return ErrorDescription(LocalAuthentication::OtherError,
"The device supports biometry only using a removable accessory, but no accessory is paired");
}
#endif
#if defined MAC_OS_X_VERSION_10_15 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_15
switch(err) {
case LAErrorWatchNotAvailable: /* MacOs 10.15+ */
return ErrorDescription(LocalAuthentication::OtherError,
"An attempt to authenticate with Apple Watch failed");
}
#endif
#if defined MAC_OS_X_VERSION_10_13 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_13
switch(err) {
case LAErrorBiometryLockout: /* MacOs 10.13+ */
return ErrorDescription(LocalAuthentication::OtherError,
"Biometry is locked because there were too many failed attempts");
case LAErrorBiometryNotAvailable: /* MacOs 10.13+ */
return ErrorDescription(LocalAuthentication::TouchIdNotAvailable,
"Biometry is not available on the device");
case LAErrorBiometryNotEnrolled: /* MacOs 10.13+ */
return ErrorDescription(LocalAuthentication::TouchIdNotConfigured,
"The user has no enrolled biometric identities");
}
#endif
#if defined MAC_OS_X_VERSION_10_11 \
&& defined MAC_OS_X_VERSION_10_13 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11 \
&& MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_13
switch(err) {
case LAErrorTouchIDLockout: /* MacOs 10.11 - 10.13 */
return ErrorDescription(LocalAuthentication::OtherError,
"Touch ID is locked because there were too many failed attempts");
}
#endif
#if defined MAC_OS_X_VERSION_10_10 \
&& defined MAC_OS_X_VERSION_10_13 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10 \
&& MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_13
switch(err) {
case LAErrorTouchIDNotAvailable: /* MacOs 10.10 - 10.13 */
return ErrorDescription(LocalAuthentication::TouchIdNotAvailable,
"Touch ID is not available on the device");
case LAErrorTouchIDNotEnrolled: /* MacOs 10.10 - 10.13 */
return ErrorDescription(LocalAuthentication::TouchIdNotConfigured,
"The user has no enrolled Touch ID fingers");
}
#endif
#if defined MAC_OS_X_VERSION_10_11 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11
switch(err) {
case LAErrorInvalidContext: /* MacOs 10.11+ */
return ErrorDescription(LocalAuthentication::OtherError,
"The context was previously invalidated");
}
#endif
#if defined MAC_OS_X_VERSION_10_10 \
&& MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_10
switch(err) {
case LAErrorSystemCancel: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::SystemCanceled,
"The system canceled authentication");
case LAErrorUserCancel: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::UserCanceled,
"The user tapped the cancel button in the authentication dialog");
case LAErrorAuthenticationFailed: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::WrongCredentials,
"The user failed to provide valid credentials");
case LAErrorNotInteractive: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::OtherError,
"Displaying the required authentication user interface is forbidden");
case LAErrorPasscodeNotSet: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::OtherError,
"A passcode isnt set on the device");
case LAErrorUserFallback: /* MacOs 10.10+ */
return ErrorDescription(LocalAuthentication::OtherError,
"The user tapped the fallback button in the authentication dialog, but no fallback is available for the authentication policy");
}
#endif
#endif
return ErrorDescription(LocalAuthentication::OtherError, "Unknown error");
}
};
void LocalAuthentication::runAuthentication(const QString& authenticationReason)
{
LAContext *laContext = [[LAContext alloc] init];
NSError *authError = nil;
NSString *localizedReasonString = authenticationReason.toNSString();
if ([laContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError])
{
[laContext evaluatePolicy:LAPolicyDeviceOwnerAuthentication
localizedReason:localizedReasonString
reply:^(BOOL authenticated, NSError *err)
{
if (authenticated)
{
emit success();
}
else
{
const ErrorDescription ed = ErrorDescription::fromLAError([err code]);
emit error(ed.code, QString("User did not authenticate successfully: %1").arg(ed.message));
}
}];
}
else
{
const ErrorDescription ed = ErrorDescription::fromLAError([authError code]);
emit error(ed.code, QString("Could not evaluate policy: %1").arg(ed.message));
}
}

View File

@ -1,4 +1,4 @@
#include "DOtherSide/StatusNotification/OSNotification.h"
#include "DOtherSide/Status/OSNotification.h"
#ifdef Q_OS_WIN
#include <shellapi.h>

View File

@ -1,4 +1,4 @@
#include "DOtherSide/StatusNotification/OSNotification.h"
#include "DOtherSide/Status/OSNotification.h"
#ifdef Q_OS_MACOS