diff --git a/storybook/pages/KeychainMock.qml b/storybook/pages/KeychainMock.qml index a9d6dd2cba..9837719406 100644 --- a/storybook/pages/KeychainMock.qml +++ b/storybook/pages/KeychainMock.qml @@ -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() diff --git a/storybook/pages/KeychainPage.qml b/storybook/pages/KeychainPage.qml index 19b3d24c10..1ac4f90539 100644 --- a/storybook/pages/KeychainPage.qml +++ b/storybook/pages/KeychainPage.qml @@ -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,13 +148,14 @@ Page { } Button { text: "Cancel" + enabled: d.keychainAvailable onClicked: { loader.item.cancelActiveRequest() } } BusyIndicator { Layout.preferredHeight: 40 - running: loader.item.loading + running: loader.item.loading } } } diff --git a/ui/StatusQ/include/StatusQ/keychain.h b/ui/StatusQ/include/StatusQ/keychain.h index 6edf4420ba..6607dbe51c 100644 --- a/ui/StatusQ/include/StatusQ/keychain.h +++ b/ui/StatusQ/include/StatusQ/keychain.h @@ -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 m_future; @@ -58,5 +62,6 @@ private: #ifdef Q_OS_MACOS Status getCredential(const QString &reason, const QString &account, QString *out); + void reevaluateAvailability(); #endif }; diff --git a/ui/StatusQ/src/keychain.cpp b/ui/StatusQ/src/keychain.cpp index 239f341844..7cfed3e473 100644 --- a/ui/StatusQ/src/keychain.cpp +++ b/ui/StatusQ/src/keychain.cpp @@ -2,9 +2,6 @@ #include -Keychain::Keychain(QObject *parent) : QObject(parent) -{} - QString Keychain::service() const { return m_service; diff --git a/ui/StatusQ/src/keychain_osx.mm b/ui/StatusQ/src/keychain_osx.mm index ecd80c8f7e..bc3f20a8a6 100644 --- a/ui/StatusQ/src/keychain_osx.mm +++ b/ui/StatusQ/src/keychain_osx.mm @@ -9,7 +9,14 @@ #include #include -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 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 = @{ diff --git a/ui/StatusQ/src/keychain_other.cpp b/ui/StatusQ/src/keychain_other.cpp index a1abce92e7..e53a5a64c3 100644 --- a/ui/StatusQ/src/keychain_other.cpp +++ b/ui/StatusQ/src/keychain_other.cpp @@ -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);