feat: keychain available property (#17367)

* feat: Keychain availability check

* fix: ifdef older systems

* fix: typoe
This commit is contained in:
Igor Sirotin 2025-02-21 19:30:52 +03:00 committed by GitHub
parent 65cc5a7c55
commit 9bebae3119
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 84 additions and 16 deletions

View File

@ -12,6 +12,8 @@ Keychain {
service: "StatusStorybookMocked"
required property bool available
// shadowing Keychain's "loading" property
readonly property alias loading: d.loading
@ -43,6 +45,10 @@ Keychain {
}
function requestGetCredential(reason, account) {
if (!root.available) {
root.getCredentialRequestCompleted(Keychain.StatusUnavailable, "")
return
}
d.loading = true
d.key = account
biometricsPopup.open()

View File

@ -1,9 +1,12 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import QtQuick.Window 2.15
import Qt.labs.settings 1.0
import StatusQ 0.1
import Storybook 1.0
@ -15,11 +18,22 @@ Page {
Logs { id: logs }
Settings {
property alias mockedKeychainAvailable: keychainAvailableCheckBox.checked
}
QtObject {
id: d
readonly property bool keychainAvailable: loader.item.available
readonly property bool useMockedKeychain: root.isMac && !forceMockedKeychainCheckBox.checked
}
Loader {
id: loader
sourceComponent: root.isMac && !forceMockedKeychainCheckBox.checked
? nativeKeychainComponent : mockedKeychainComponent
sourceComponent: d.useMockedKeychain ? nativeKeychainComponent
: mockedKeychainComponent
}
Component {
@ -35,6 +49,7 @@ Page {
KeychainMock {
parent: root
available: keychainAvailableCheckBox.checked
}
}
@ -61,7 +76,7 @@ Page {
anchors.centerIn: parent
Text {
text: `Is MacOS: ${root.isMac}`
text: `Is MacOS: ${root.isMac ? "🍏" : "🙅‍"}, Keychain available: ${d.keychainAvailable ? "✅" : "❌"}`
}
CheckBox {
@ -71,6 +86,12 @@ Page {
text: `Force using mocked Keychain`
}
CheckBox {
id: keychainAvailableCheckBox
text: `Mocked keychain available`
enabled: !d.useMockedKeychain
}
RowLayout {
Text {
text: "Account"
@ -94,6 +115,7 @@ Page {
RowLayout {
Button {
text: "Save"
enabled: d.keychainAvailable
onClicked: {
const status = loader.item.saveCredential(accountInput.text, passwordInput.text)
logs.logEvent("SaveCredentials", ["status"], [status])
@ -101,6 +123,7 @@ Page {
}
Button {
text: "Delete"
enabled: d.keychainAvailable
onClicked: {
const status = loader.item.deleteCredential(accountInput.text)
logs.logEvent("DeleteCredential", ["status"], [status])
@ -108,6 +131,7 @@ Page {
}
Button {
text: "Get"
enabled: d.keychainAvailable
onClicked: {
loader.item.requestGetCredential("Get reason",
accountInput.text)
@ -115,6 +139,7 @@ Page {
}
Button {
text: "Has"
enabled: d.keychainAvailable
onClicked: {
const status = loader.item.hasCredential(accountInput.text)
logs.logEvent("HasCredential", ["status"], [status])
@ -123,6 +148,7 @@ Page {
}
Button {
text: "Cancel"
enabled: d.keychainAvailable
onClicked: {
loader.item.cancelActiveRequest()
}

View File

@ -14,6 +14,7 @@ class Keychain : public QObject {
Q_PROPERTY(QString service READ service WRITE setService NOTIFY serviceChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool available READ available CONSTANT)
public:
explicit Keychain(QObject *parent = nullptr);
@ -33,6 +34,8 @@ public:
QString service() const;
void setService(const QString &service);
bool available() const;
bool loading() const;
Q_INVOKABLE Status saveCredential(const QString &account, const QString &password);
@ -51,6 +54,7 @@ signals:
private:
QString m_service;
bool m_loading = false;
bool m_available = false;
void setLoading(bool loading);
QFuture<void> m_future;
@ -58,5 +62,6 @@ private:
#ifdef Q_OS_MACOS
Status getCredential(const QString &reason, const QString &account, QString *out);
void reevaluateAvailability();
#endif
};

View File

@ -2,9 +2,6 @@
#include <QDebug>
Keychain::Keychain(QObject *parent) : QObject(parent)
{}
QString Keychain::service() const
{
return m_service;

View File

@ -9,7 +9,14 @@
#include <LocalAuthentication/LocalAuthentication.h>
#include <Security/Security.h>
const static auto authPolicy = LAPolicyDeviceOwnerAuthentication;
const static auto authPolicy =
#if defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 150000
LAPolicyDeviceOwnerAuthenticationWithBiometricsOrCompanion;
#elif defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 101202
LAPolicyDeviceOwnerAuthenticationWithBiometrics;
#else
LAPolicyDeviceOwnerAuthentication;
#endif
static Keychain::Status convertStatus(OSStatus status)
{
@ -39,12 +46,23 @@ Keychain::Status convertError(NSError *error)
}
}
Keychain::Keychain(QObject *parent)
: QObject(parent)
{
reevaluateAvailability();
}
Keychain::~Keychain()
{
cancelActiveRequest();
m_future.waitForFinished();
}
bool Keychain::available() const
{
return m_available;
}
Keychain::Status authenticate(const QString &reason, LAContext **context)
{
if (context == nullptr)
@ -56,14 +74,6 @@ Keychain::Status authenticate(const QString &reason, LAContext **context)
}
*context = [[LAContext alloc] init];
NSError *authError = nil;
// Check if Biometrics Authentication is available
if (![*context canEvaluatePolicy:authPolicy error:&authError]) {
qWarning() << "biometric authentication not available:"
<< QString::fromNSString(authError.localizedDescription);
return convertError(authError);
}
QEventLoop loop;
auto loopPtr = &loop;
@ -165,6 +175,10 @@ Keychain::Status Keychain::deleteCredential(const QString &account)
Keychain::Status Keychain::getCredential(const QString &reason, const QString &account, QString *out)
{
if (!m_available) {
return StatusUnavailable;
}
QScopedValueRollback<LAContext *> roolback(m_activeAuthContext, nullptr);
const auto authStatus = authenticate(reason, &m_activeAuthContext);
@ -194,6 +208,19 @@ Keychain::Status Keychain::getCredential(const QString &reason, const QString &a
return convertStatus(status);
}
void Keychain::reevaluateAvailability()
{
auto context = [[LAContext alloc] init];
NSError *authError = nil;
m_available = [context canEvaluatePolicy:authPolicy error:&authError];
if (!m_available) {
const auto description = QString::fromNSString(authError.localizedDescription);
qDebug() << "Keychain is not available" << description;
}
}
Keychain::Status Keychain::hasCredential(const QString &account) const
{
NSDictionary *query = @{

View File

@ -1,7 +1,14 @@
#include "StatusQ/keychain.h"
Keychain::Keychain(QObject *parent) : QObject(parent) {}
Keychain::~Keychain() = default;
bool Keychain::available() const
{
return false;
}
Keychain::Status Keychain::saveCredential(const QString &account, const QString &password)
{
Q_UNUSED(account);