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

View File

@ -1,9 +1,12 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import QtQml 2.15
import QtQuick.Window 2.15 import QtQuick.Window 2.15
import Qt.labs.settings 1.0
import StatusQ 0.1 import StatusQ 0.1
import Storybook 1.0 import Storybook 1.0
@ -15,11 +18,22 @@ Page {
Logs { id: logs } 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 { Loader {
id: loader id: loader
sourceComponent: root.isMac && !forceMockedKeychainCheckBox.checked sourceComponent: d.useMockedKeychain ? nativeKeychainComponent
? nativeKeychainComponent : mockedKeychainComponent : mockedKeychainComponent
} }
Component { Component {
@ -35,6 +49,7 @@ Page {
KeychainMock { KeychainMock {
parent: root parent: root
available: keychainAvailableCheckBox.checked
} }
} }
@ -61,7 +76,7 @@ Page {
anchors.centerIn: parent anchors.centerIn: parent
Text { Text {
text: `Is MacOS: ${root.isMac}` text: `Is MacOS: ${root.isMac ? "🍏" : "🙅‍"}, Keychain available: ${d.keychainAvailable ? "✅" : "❌"}`
} }
CheckBox { CheckBox {
@ -71,6 +86,12 @@ Page {
text: `Force using mocked Keychain` text: `Force using mocked Keychain`
} }
CheckBox {
id: keychainAvailableCheckBox
text: `Mocked keychain available`
enabled: !d.useMockedKeychain
}
RowLayout { RowLayout {
Text { Text {
text: "Account" text: "Account"
@ -94,6 +115,7 @@ Page {
RowLayout { RowLayout {
Button { Button {
text: "Save" text: "Save"
enabled: d.keychainAvailable
onClicked: { onClicked: {
const status = loader.item.saveCredential(accountInput.text, passwordInput.text) const status = loader.item.saveCredential(accountInput.text, passwordInput.text)
logs.logEvent("SaveCredentials", ["status"], [status]) logs.logEvent("SaveCredentials", ["status"], [status])
@ -101,6 +123,7 @@ Page {
} }
Button { Button {
text: "Delete" text: "Delete"
enabled: d.keychainAvailable
onClicked: { onClicked: {
const status = loader.item.deleteCredential(accountInput.text) const status = loader.item.deleteCredential(accountInput.text)
logs.logEvent("DeleteCredential", ["status"], [status]) logs.logEvent("DeleteCredential", ["status"], [status])
@ -108,6 +131,7 @@ Page {
} }
Button { Button {
text: "Get" text: "Get"
enabled: d.keychainAvailable
onClicked: { onClicked: {
loader.item.requestGetCredential("Get reason", loader.item.requestGetCredential("Get reason",
accountInput.text) accountInput.text)
@ -115,6 +139,7 @@ Page {
} }
Button { Button {
text: "Has" text: "Has"
enabled: d.keychainAvailable
onClicked: { onClicked: {
const status = loader.item.hasCredential(accountInput.text) const status = loader.item.hasCredential(accountInput.text)
logs.logEvent("HasCredential", ["status"], [status]) logs.logEvent("HasCredential", ["status"], [status])
@ -123,6 +148,7 @@ Page {
} }
Button { Button {
text: "Cancel" text: "Cancel"
enabled: d.keychainAvailable
onClicked: { onClicked: {
loader.item.cancelActiveRequest() 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(QString service READ service WRITE setService NOTIFY serviceChanged)
Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)
Q_PROPERTY(bool available READ available CONSTANT)
public: public:
explicit Keychain(QObject *parent = nullptr); explicit Keychain(QObject *parent = nullptr);
@ -33,6 +34,8 @@ public:
QString service() const; QString service() const;
void setService(const QString &service); void setService(const QString &service);
bool available() const;
bool loading() const; bool loading() const;
Q_INVOKABLE Status saveCredential(const QString &account, const QString &password); Q_INVOKABLE Status saveCredential(const QString &account, const QString &password);
@ -51,6 +54,7 @@ signals:
private: private:
QString m_service; QString m_service;
bool m_loading = false; bool m_loading = false;
bool m_available = false;
void setLoading(bool loading); void setLoading(bool loading);
QFuture<void> m_future; QFuture<void> m_future;
@ -58,5 +62,6 @@ private:
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
Status getCredential(const QString &reason, const QString &account, QString *out); Status getCredential(const QString &reason, const QString &account, QString *out);
void reevaluateAvailability();
#endif #endif
}; };

View File

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

View File

@ -9,7 +9,14 @@
#include <LocalAuthentication/LocalAuthentication.h> #include <LocalAuthentication/LocalAuthentication.h>
#include <Security/Security.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) static Keychain::Status convertStatus(OSStatus status)
{ {
@ -39,12 +46,23 @@ Keychain::Status convertError(NSError *error)
} }
} }
Keychain::Keychain(QObject *parent)
: QObject(parent)
{
reevaluateAvailability();
}
Keychain::~Keychain() Keychain::~Keychain()
{ {
cancelActiveRequest(); cancelActiveRequest();
m_future.waitForFinished(); m_future.waitForFinished();
} }
bool Keychain::available() const
{
return m_available;
}
Keychain::Status authenticate(const QString &reason, LAContext **context) Keychain::Status authenticate(const QString &reason, LAContext **context)
{ {
if (context == nullptr) if (context == nullptr)
@ -56,14 +74,6 @@ Keychain::Status authenticate(const QString &reason, LAContext **context)
} }
*context = [[LAContext alloc] init]; *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; QEventLoop loop;
auto loopPtr = &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) Keychain::Status Keychain::getCredential(const QString &reason, const QString &account, QString *out)
{ {
if (!m_available) {
return StatusUnavailable;
}
QScopedValueRollback<LAContext *> roolback(m_activeAuthContext, nullptr); QScopedValueRollback<LAContext *> roolback(m_activeAuthContext, nullptr);
const auto authStatus = authenticate(reason, &m_activeAuthContext); const auto authStatus = authenticate(reason, &m_activeAuthContext);
@ -194,6 +208,19 @@ Keychain::Status Keychain::getCredential(const QString &reason, const QString &a
return convertStatus(status); 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 Keychain::Status Keychain::hasCredential(const QString &account) const
{ {
NSDictionary *query = @{ NSDictionary *query = @{

View File

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