diff --git a/CMakeLists.txt b/CMakeLists.txt
index 074c709330..8655a523e5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -53,6 +53,7 @@ set(WITH_XC_ALL OFF CACHE BOOL "Build in all available plugins")
option(WITH_XC_AUTOTYPE "Include Auto-Type." ON)
option(WITH_XC_NETWORKING "Include networking code (e.g. for downloading website icons)." OFF)
option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF)
+option(WITH_XC_BROWSER_WEBAUTHN "WebAuthn support for browser integration." OFF)
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
@@ -98,6 +99,7 @@ if(WITH_XC_ALL)
set(WITH_XC_AUTOTYPE ON)
set(WITH_XC_NETWORKING ON)
set(WITH_XC_BROWSER ON)
+ set(WITH_XC_BROWSER_WEBAUTHN ON)
set(WITH_XC_YUBIKEY ON)
set(WITH_XC_SSHAGENT ON)
set(WITH_XC_KEESHARE ON)
diff --git a/INSTALL.md b/INSTALL.md
index 17bcdae9f1..bea50fe5b3 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -112,6 +112,7 @@ KeePassXC comes with a variety of build options that can turn on/off features. M
-DWITH_XC_AUTOTYPE=[ON|OFF] Enable/Disable Auto-Type (default: ON)
-DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF)
-DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF)
+-DWITH_XC_BROWSER_WEBAUTHN=[ON|OFF] Enable/Disable WebAuthn support for browser integration (default: OFF)
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 3b5ae5f4a8..96698656f1 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -896,6 +896,10 @@ Do you want to delete the entry?
+
+ WebAuthn
+
+
BrowserSettingsWidget
@@ -1135,6 +1139,41 @@ Do you want to delete the entry?
+
+ BrowserWebAuthnConfirmationDialog
+
+ Cancel
+
+
+
+ Authenticate
+
+
+
+ Do you want to register WebAuthn credentials for:
+%1 (%2)?
+
+
+
+ Register
+
+
+
+ Timeout in <b>%n</b> seconds...
+
+
+
+
+
+
+ KeePassXC: WebAuthn credentials
+
+
+
+ Authenticate WebAuthn credentials for:%1?
+
+
+
CloneDialog
@@ -7941,6 +7980,10 @@ Kernel: %3 %4
Access to all entries is denied
+
+ WebAuthn
+
+
QtIOCompressor
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index f37f55b0a8..9f8ee7ebcc 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -236,6 +236,7 @@ set(keepassx_SOURCES_MAINEXE main.cpp)
add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing")
add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)")
add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser")
+add_feature_info(WebAuthn WITH_XC_BROWSER_WEBAUTHN "WebAuthn support for browser integration")
add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent")
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
diff --git a/src/browser/BrowserAccessControlDialog.h b/src/browser/BrowserAccessControlDialog.h
index 28f75303ba..946db16d92 100644
--- a/src/browser/BrowserAccessControlDialog.h
+++ b/src/browser/BrowserAccessControlDialog.h
@@ -16,8 +16,8 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERACCESSCONTROLDIALOG_H
-#define BROWSERACCESSCONTROLDIALOG_H
+#ifndef KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
+#define KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
#include
#include
@@ -53,4 +53,4 @@ class BrowserAccessControlDialog : public QDialog
bool m_entriesAccepted;
};
-#endif // BROWSERACCESSCONTROLDIALOG_H
+#endif // KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp
index 79ff82c571..7dfb54e279 100644
--- a/src/browser/BrowserAction.cpp
+++ b/src/browser/BrowserAction.cpp
@@ -16,9 +16,10 @@
*/
#include "BrowserAction.h"
-#include "BrowserService.h"
#include "BrowserSettings.h"
#include "core/Global.h"
+#include "BrowserMessageBuilder.h"
+#include "BrowserWebAuthn.h"
#include "core/Tools.h"
#include
@@ -39,6 +40,8 @@ static const QString BROWSER_REQUEST_LOCK_DATABASE = QStringLiteral("lock-databa
static const QString BROWSER_REQUEST_REQUEST_AUTOTYPE = QStringLiteral("request-autotype");
static const QString BROWSER_REQUEST_SET_LOGIN = QStringLiteral("set-login");
static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate");
+static const QString BROWSER_REQUEST_WEBAUTHN_GET = QStringLiteral("webauthn-get");
+static const QString BROWSER_REQUEST_WEBAUTHN_REGISTER = QStringLiteral("webauthn-register");
QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json)
{
@@ -104,6 +107,12 @@ QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject&
return handleGlobalAutoType(json, action);
} else if (action.compare("get-database-entries", Qt::CaseSensitive) == 0) {
return handleGetDatabaseEntries(json, action);
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ } else if (action.compare(BROWSER_REQUEST_WEBAUTHN_GET) == 0) {
+ return handleWebAuthnGet(json, action);
+ } else if (action.compare(BROWSER_REQUEST_WEBAUTHN_REGISTER) == 0) {
+ return handleWebAuthnRegister(json, action);
+#endif
}
// Action was not recognized
@@ -226,18 +235,12 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
- const auto keys = browserRequest.getArray("keys");
-
- StringPairList keyList;
- for (const auto val : keys) {
- const auto keyObject = val.toObject();
- keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
- }
-
+ const auto keys = getConnectionKeys(browserRequest);
const auto id = browserRequest.getString("id");
const auto formUrl = browserRequest.getString("submitUrl");
const auto auth = browserRequest.getString("httpAuth");
const bool httpAuth = auth.compare(TRUE_STR) == 0;
+ const auto keyList = getConnectionKeys(browserRequest);
EntryParameters entryParameters;
entryParameters.dbid = id;
@@ -384,10 +387,6 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons
QJsonObject BrowserAction::handleGetDatabaseEntries(const QJsonObject& json, const QString& action)
{
- const QString hash = browserService()->getDatabaseHash();
- const QString nonce = json.value("nonce").toString();
- const QString encrypted = json.value("message").toString();
-
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
@@ -516,6 +515,83 @@ QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const Q
return buildResponse(action, browserRequest.incrementedNonce);
}
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+QJsonObject BrowserAction::handleWebAuthnGet(const QJsonObject& json, const QString& action)
+{
+ if (!m_associated) {
+ return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
+ }
+
+ const auto browserRequest = decodeRequest(json);
+ if (browserRequest.isEmpty()) {
+ return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
+ }
+
+ const auto command = browserRequest.getString("action");
+ if (command.isEmpty() || command.compare(BROWSER_REQUEST_WEBAUTHN_GET) != 0) {
+ return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
+ }
+
+ const auto publicKey = browserRequest.getObject("publicKey");
+ if (publicKey.isEmpty()) {
+ return getErrorReply(action, ERROR_WEBAUTHN_EMPTY_PUBLIC_KEY);
+ }
+
+ const auto origin = browserRequest.getString("origin");
+ if (!origin.startsWith("https://")) {
+ return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
+ }
+
+ const auto keyList = getConnectionKeys(browserRequest);
+ const auto response = browserService()->showWebAuthnAuthenticationPrompt(publicKey, origin, keyList);
+
+ /*auto message = browserMessageBuilder()->buildMessage(browserRequest.incrementedNonce);
+ message["response"] = response;
+
+ return buildResponse(action, message, browserRequest.incrementedNonce);*/
+ const Parameters params{{"response", response}};
+ return buildResponse(action, browserRequest.incrementedNonce, params);
+}
+
+QJsonObject BrowserAction::handleWebAuthnRegister(const QJsonObject& json, const QString& action)
+{
+ if (!m_associated) {
+ return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
+ }
+
+ const auto browserRequest = decodeRequest(json);
+ if (browserRequest.isEmpty()) {
+ return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
+ }
+
+ const auto command = browserRequest.getString("action");
+ if (command.isEmpty() || command.compare(BROWSER_REQUEST_WEBAUTHN_REGISTER) != 0) {
+ return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
+ }
+
+ const auto publicKey = browserRequest.getObject("publicKey");
+ if (publicKey.isEmpty()) {
+ return getErrorReply(action, ERROR_WEBAUTHN_EMPTY_PUBLIC_KEY);
+ }
+
+ const auto origin = browserRequest.getString("origin");
+ if (!origin.startsWith("https://")) {
+ return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
+ }
+
+ const auto keyList = getConnectionKeys(browserRequest);
+ const auto response = browserService()->showWebAuthnRegisterPrompt(publicKey, origin, keyList);
+
+ // Send response
+ /*auto message = browserMessageBuilder()->buildMessage(browserRequest.incrementedNonce);
+ message["response"] = response;
+
+ return buildResponse(action, message, browserRequest.incrementedNonce);*/
+ const Parameters params{{"response", response}};
+ return buildResponse(action, browserRequest.incrementedNonce, params);
+}
+#endif
+
QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce)
{
return browserMessageBuilder()->decryptMessage(message, nonce, m_clientPublicKey, m_secretKey);
@@ -541,3 +617,16 @@ BrowserRequest BrowserAction::decodeRequest(const QJsonObject& json)
browserMessageBuilder()->incrementNonce(nonce),
decryptMessage(encrypted, nonce)};
}
+
+StringPairList BrowserAction::getConnectionKeys(const BrowserRequest& browserRequest)
+{
+ const auto keys = browserRequest.getArray("keys");
+
+ StringPairList keyList;
+ for (const auto val : keys) {
+ const auto keyObject = val.toObject();
+ keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
+ }
+
+ return keyList;
+}
diff --git a/src/browser/BrowserAction.h b/src/browser/BrowserAction.h
index fe65c977a9..fae027d8bf 100644
--- a/src/browser/BrowserAction.h
+++ b/src/browser/BrowserAction.h
@@ -15,10 +15,11 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERACTION_H
-#define BROWSERACTION_H
+#ifndef KEEPASSXC_BROWSERACTION_H
+#define KEEPASSXC_BROWSERACTION_H
#include "BrowserMessageBuilder.h"
+#include "BrowserService.h"
#include
#include
@@ -43,6 +44,11 @@ struct BrowserRequest
return decrypted.value(param).toArray();
}
+ inline QJsonObject getObject(const QString& param) const
+ {
+ return decrypted.value(param).toObject();
+ }
+
inline QString getString(const QString& param) const
{
return decrypted.value(param).toString();
@@ -73,12 +79,17 @@ class BrowserAction
QJsonObject handleGetTotp(const QJsonObject& json, const QString& action);
QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action);
QJsonObject handleGlobalAutoType(const QJsonObject& json, const QString& action);
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ QJsonObject handleWebAuthnGet(const QJsonObject& json, const QString& action);
+ QJsonObject handleWebAuthnRegister(const QJsonObject& json, const QString& action);
+#endif
private:
QJsonObject buildResponse(const QString& action, const QString& nonce, const Parameters& params = {});
QJsonObject getErrorReply(const QString& action, const int errorCode) const;
QJsonObject decryptMessage(const QString& message, const QString& nonce);
BrowserRequest decodeRequest(const QJsonObject& json);
+ StringPairList getConnectionKeys(const BrowserRequest& browserRequest);
private:
static const int MaxUrlLength;
@@ -91,4 +102,4 @@ class BrowserAction
friend class TestBrowser;
};
-#endif // BROWSERACTION_H
+#endif // KEEPASSXC_BROWSERACTION_H
diff --git a/src/browser/BrowserCbor.cpp b/src/browser/BrowserCbor.cpp
new file mode 100644
index 0000000000..40d3fbf679
--- /dev/null
+++ b/src/browser/BrowserCbor.cpp
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "BrowserCbor.h"
+#include "BrowserMessageBuilder.h"
+#include
+#include
+#include
+
+QByteArray BrowserCbor::cborEncodeAttestation(const QByteArray& authData) const
+{
+ Q_UNUSED(authData);
+ QByteArray result;
+ QCborStreamWriter writer(&result);
+
+ writer.startMap(3);
+
+ writer.append("fmt");
+ writer.append("none");
+
+ writer.append("attStmt");
+ writer.startMap(0);
+ writer.endMap();
+
+ writer.append("authData");
+ writer.appendByteString(authData.constData(), authData.size());
+
+ writer.endMap();
+
+ return result;
+}
+
+QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& xPart, const QByteArray& yPart) const
+{
+ QByteArray result;
+ QCborStreamWriter writer(&result);
+
+ writer.startMap(5);
+
+ // Key type
+ writer.append(1);
+ writer.append(getCoseKeyType(alg));
+
+ // Signature algorithm
+ writer.append(3);
+ writer.append(alg);
+
+ // Curve parameter
+ writer.append(-1);
+ writer.append(getCurveParameter(alg));
+
+ // Key x-coordinate
+ writer.append(-2);
+ writer.append(xPart);
+
+ // Key y-coordinate
+ writer.append(-3);
+ writer.append(yPart);
+
+ writer.endMap();
+
+ return result;
+}
+
+// See: https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#user-verification-methods
+QByteArray BrowserCbor::cborEncodeExtensionData(const QJsonObject& extensions) const
+{
+ if (extensions.empty()) {
+ return {};
+ }
+
+ QByteArray result;
+ QCborStreamWriter writer(&result);
+
+ writer.startMap(extensions.keys().count());
+ if (extensions["credProps"].toBool()) {
+ writer.append("credProps");
+ writer.startMap(1);
+ writer.append("rk");
+ writer.append(true);
+ writer.endMap();
+ }
+
+ if (extensions["uvm"].toBool()) {
+ writer.append("uvm");
+
+ writer.startArray(1);
+ writer.startArray(3);
+
+ // userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001)
+ writer.append(1);
+
+ // keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
+ writer.append(1);
+
+ // matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
+ writer.append(1);
+
+ writer.endArray();
+ writer.endArray();
+ }
+ writer.endMap();
+
+ return result;
+}
+
+QJsonObject BrowserCbor::getJsonFromCborData(const QByteArray& byteArray) const
+{
+ auto reader = QCborStreamReader(byteArray);
+ auto contents = QCborValue::fromCbor(reader);
+ if (reader.lastError()) {
+ return {};
+ }
+
+ const auto ret = handleCborValue(contents);
+
+ // Parse variant result to QJsonDocument
+ const auto jsonDocument = QJsonDocument::fromVariant(ret);
+ if (jsonDocument.isNull() || jsonDocument.isEmpty()) {
+ return {};
+ }
+
+ return jsonDocument.object();
+}
+
+QVariant BrowserCbor::handleCborArray(const QCborArray& array) const
+{
+ QVariantList result;
+ result.reserve(array.size());
+
+ for (auto a : array) {
+ result.append(handleCborValue(a));
+ }
+
+ return result;
+}
+
+QVariant BrowserCbor::handleCborMap(const QCborMap& map) const
+{
+ QVariantMap result;
+ for (auto pair : map) {
+ result.insert(handleCborValue(pair.first).toString(), handleCborValue(pair.second));
+ }
+
+ return QVariant::fromValue(result);
+}
+
+QVariant BrowserCbor::handleCborValue(const QCborValue& value) const
+{
+ if (value.isArray()) {
+ return handleCborArray(value.toArray());
+ } else if (value.isMap()) {
+ return handleCborMap(value.toMap());
+ } else if (value.isByteArray()) {
+ auto ba = value.toByteArray();
+
+ // Return base64 instead of raw byte array
+ auto base64Str = browserMessageBuilder()->getBase64FromArray(ba);
+ return QVariant::fromValue(base64Str);
+ }
+
+ return value.toVariant();
+}
+
+unsigned int BrowserCbor::getCurveParameter(int alg) const
+{
+ switch (alg) {
+ case WebAuthnAlgorithms::ES256:
+ return WebAuthnCurveKey::P256;
+ case WebAuthnAlgorithms::ES384:
+ return WebAuthnCurveKey::P384;
+ case WebAuthnAlgorithms::ES512:
+ return WebAuthnCurveKey::P521;
+ case WebAuthnAlgorithms::EDDSA:
+ return WebAuthnCurveKey::ED25519;
+ default:
+ return WebAuthnCurveKey::P256;
+ }
+}
+
+// See: https://www.rfc-editor.org/rfc/rfc8152
+// AES/HMAC/ChaCha20 etc. carries symmetric keys (4) and OKP not supported currently.
+unsigned int BrowserCbor::getCoseKeyType(int alg) const
+{
+ switch (alg) {
+ case WebAuthnAlgorithms::ES256:
+ case WebAuthnAlgorithms::ES384:
+ case WebAuthnAlgorithms::ES512:
+ return WebAuthnCoseKeyType::EC2;
+ case WebAuthnAlgorithms::EDDSA:
+ return WebAuthnCoseKeyType::OKP;
+ default:
+ return WebAuthnCoseKeyType::EC2;
+ }
+}
diff --git a/src/browser/BrowserCbor.h b/src/browser/BrowserCbor.h
new file mode 100644
index 0000000000..789fe6d219
--- /dev/null
+++ b/src/browser/BrowserCbor.h
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_BROWSERCBOR_H
+#define KEEPASSXC_BROWSERCBOR_H
+
+#include
+#include
+#include
+#include
+
+enum WebAuthnAlgorithms : int
+{
+ ES256 = -7,
+ EDDSA = -8,
+ ES384 = -35,
+ ES512 = -36,
+ RS256 = -257
+};
+
+// https://www.rfc-editor.org/rfc/rfc9053#section-7.1
+enum WebAuthnCurveKey : int
+{
+ P256 = 1, // EC2, NIST P-256, also known as secp256r1
+ P384 = 2, // EC2, NIST P-384, also known as secp384r1
+ P521 = 3, // EC2, NIST P-521, also known as secp521r1
+ X25519 = 4, // OKP, X25519 for use w/ ECDH only
+ X448 = 5, // OKP, X448 for use w/ ECDH only
+ ED25519 = 6, // OKP, Ed25519 for use w/ EdDSA only
+ ED448 = 7 // OKP, Ed448 for use w/ EdDSA only
+};
+
+// https://www.rfc-editor.org/rfc/rfc8152
+enum WebAuthnCoseKeyType : int
+{
+ OKP = 1, // Octet Keypair
+ EC2 = 2 // Elliptic Curve
+};
+
+class BrowserCbor
+{
+public:
+ QByteArray cborEncodeAttestation(const QByteArray& authData) const;
+ QByteArray cborEncodePublicKey(int alg, const QByteArray& xPart, const QByteArray& yPart) const;
+ QByteArray cborEncodeExtensionData(const QJsonObject& extensions) const;
+ QJsonObject getJsonFromCborData(const QByteArray& byteArray) const;
+ QVariant handleCborArray(const QCborArray& array) const;
+ QVariant handleCborMap(const QCborMap& map) const;
+ QVariant handleCborValue(const QCborValue& value) const;
+ unsigned int getCoseKeyType(int alg) const;
+ unsigned int getCurveParameter(int alg) const;
+};
+
+#endif // KEEPASSXC_BROWSERCBOR_H
diff --git a/src/browser/BrowserEntryConfig.h b/src/browser/BrowserEntryConfig.h
index 6de4b0bc5c..2cb76d5f53 100644
--- a/src/browser/BrowserEntryConfig.h
+++ b/src/browser/BrowserEntryConfig.h
@@ -16,8 +16,8 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERENTRYCONFIG_H
-#define BROWSERENTRYCONFIG_H
+#ifndef KEEPASSXC_BROWSERENTRYCONFIG_H
+#define KEEPASSXC_BROWSERENTRYCONFIG_H
#include
#include
@@ -55,4 +55,4 @@ class BrowserEntryConfig : public QObject
QString m_realm;
};
-#endif // BROWSERENTRYCONFIG_H
+#endif // KEEPASSXC_BROWSERENTRYCONFIG_H
diff --git a/src/browser/BrowserEntrySaveDialog.h b/src/browser/BrowserEntrySaveDialog.h
index 8675e36faa..44b3d6601f 100644
--- a/src/browser/BrowserEntrySaveDialog.h
+++ b/src/browser/BrowserEntrySaveDialog.h
@@ -16,8 +16,8 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERENTRYSAVEDIALOG_H
-#define BROWSERENTRYSAVEDIALOG_H
+#ifndef KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
+#define KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
#include "gui/DatabaseTabWidget.h"
@@ -45,4 +45,4 @@ class BrowserEntrySaveDialog : public QDialog
QScopedPointer m_ui;
};
-#endif // BROWSERENTRYSAVEDIALOG_H
+#endif // KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
diff --git a/src/browser/BrowserHost.h b/src/browser/BrowserHost.h
index 86f20f1e2d..f3620c04cc 100644
--- a/src/browser/BrowserHost.h
+++ b/src/browser/BrowserHost.h
@@ -15,8 +15,8 @@
* along with this program. If not, see .
*/
-#ifndef NATIVEMESSAGINGHOST_H
-#define NATIVEMESSAGINGHOST_H
+#ifndef KEEPASSXC_NATIVEMESSAGINGHOST_H
+#define KEEPASSXC_NATIVEMESSAGINGHOST_H
#include
#include
@@ -56,4 +56,4 @@ private slots:
QList m_socketList;
};
-#endif // NATIVEMESSAGINGHOST_H
+#endif // KEEPASSXC_NATIVEMESSAGINGHOST_H
diff --git a/src/browser/BrowserMessageBuilder.cpp b/src/browser/BrowserMessageBuilder.cpp
index efbaf8cc23..80a052ee42 100644
--- a/src/browser/BrowserMessageBuilder.cpp
+++ b/src/browser/BrowserMessageBuilder.cpp
@@ -21,6 +21,7 @@
#include "core/Global.h"
#include "core/Tools.h"
+#include
#include
#include
#include
@@ -243,6 +244,11 @@ QJsonObject BrowserMessageBuilder::getJsonObject(const uchar* pArray, const uint
QByteArray arr = getQByteArray(pArray, len);
QJsonParseError err;
QJsonDocument doc(QJsonDocument::fromJson(arr, &err));
+#ifdef QT_DEBUG
+ if (doc.isNull()) {
+ qWarning() << "Cannot create QJsonDocument: " << err.errorString();
+ }
+#endif
return doc.object();
}
@@ -250,6 +256,12 @@ QJsonObject BrowserMessageBuilder::getJsonObject(const QByteArray& ba) const
{
QJsonParseError err;
QJsonDocument doc(QJsonDocument::fromJson(ba, &err));
+#ifdef QT_DEBUG
+ if (doc.isNull()) {
+ qWarning() << "Cannot create QJsonDocument: " << err.errorString();
+ }
+#endif
+
return doc.object();
}
@@ -266,3 +278,65 @@ QString BrowserMessageBuilder::incrementNonce(const QString& nonce)
sodium_increment(n.data(), n.size());
return getQByteArray(n.data(), n.size()).toBase64();
}
+
+QString BrowserMessageBuilder::getRandomBytesAsBase64(int bytes) const
+{
+ if (bytes == 0) {
+ return {};
+ }
+
+ unsigned char buf[bytes];
+ Botan::Sodium::randombytes_buf(buf, bytes);
+
+ return getBase64FromArray(reinterpret_cast(buf), bytes);
+}
+
+QString BrowserMessageBuilder::getBase64FromArray(const char* arr, int len) const
+{
+ if (len < 1) {
+ return {};
+ }
+
+ auto data = QByteArray::fromRawData(arr, len);
+ return getBase64FromArray(data);
+}
+
+// Returns URL encoded base64 with trailing removed
+QString BrowserMessageBuilder::getBase64FromArray(const QByteArray& byteArray) const
+{
+ if (byteArray.length() < 1) {
+ return {};
+ }
+
+ return byteArray.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
+}
+
+QString BrowserMessageBuilder::getBase64FromJson(const QJsonObject& jsonObject) const
+{
+ if (jsonObject.isEmpty()) {
+ return {};
+ }
+
+ const auto dataArray = QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
+ return getBase64FromArray(dataArray);
+}
+
+QByteArray BrowserMessageBuilder::getArrayFromHexString(const QString& hexString) const
+{
+ return QByteArray::fromHex(hexString.toUtf8());
+}
+
+QByteArray BrowserMessageBuilder::getArrayFromBase64(const QString& base64str) const
+{
+ return QByteArray::fromBase64(base64str.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
+}
+
+QByteArray BrowserMessageBuilder::getSha256Hash(const QString& str) const
+{
+ return QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Sha256);
+}
+
+QString BrowserMessageBuilder::getSha256HashAsBase64(const QString& str) const
+{
+ return getBase64FromArray(QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Sha256));
+}
diff --git a/src/browser/BrowserMessageBuilder.h b/src/browser/BrowserMessageBuilder.h
index 1248522afd..ac032d6a45 100644
--- a/src/browser/BrowserMessageBuilder.h
+++ b/src/browser/BrowserMessageBuilder.h
@@ -15,8 +15,8 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERMESSAGEBUILDER_H
-#define BROWSERMESSAGEBUILDER_H
+#ifndef KEEPASSXC_BROWSERMESSAGEBUILDER_H
+#define KEEPASSXC_BROWSERMESSAGEBUILDER_H
#include
#include
@@ -48,7 +48,12 @@ namespace
ERROR_KEEPASS_NO_GROUPS_FOUND = 16,
ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP = 17,
ERROR_KEEPASS_NO_VALID_UUID_PROVIDED = 18,
- ERROR_KEEPASS_ACCESS_TO_ALL_ENTRIES_DENIED = 19
+ ERROR_KEEPASS_ACCESS_TO_ALL_ENTRIES_DENIED = 19,
+ ERROR_WEBAUTHN_ATTESTATION_NOT_SUPPORTED = 20,
+ ERROR_WEBAUTHN_CREDENTIAL_IS_EXCLUDED = 21,
+ ERROR_WEBAUTHN_REQUEST_CANCELED = 22,
+ ERROR_WEBAUTHN_INVALID_USER_VERIFICATION = 23,
+ ERROR_WEBAUTHN_EMPTY_PUBLIC_KEY = 24,
};
}
@@ -84,6 +89,14 @@ class BrowserMessageBuilder
QJsonObject getJsonObject(const QByteArray& ba) const;
QByteArray base64Decode(const QString& str);
QString incrementNonce(const QString& nonce);
+ QString getRandomBytesAsBase64(int bytes) const;
+ QString getBase64FromArray(const char* arr, int len) const;
+ QString getBase64FromArray(const QByteArray& byteArray) const;
+ QString getBase64FromJson(const QJsonObject& jsonObject) const;
+ QByteArray getArrayFromHexString(const QString& hexString) const;
+ QByteArray getArrayFromBase64(const QString& base64str) const;
+ QByteArray getSha256Hash(const QString& str) const;
+ QString getSha256HashAsBase64(const QString& str) const;
private:
Q_DISABLE_COPY(BrowserMessageBuilder);
@@ -96,4 +109,4 @@ static inline BrowserMessageBuilder* browserMessageBuilder()
return BrowserMessageBuilder::instance();
}
-#endif // BROWSERMESSAGEBUILDER_H
+#endif // KEEPASSXC_BROWSERMESSAGEBUILDER_H
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
index f54dae5972..38b3fdef90 100644
--- a/src/browser/BrowserService.cpp
+++ b/src/browser/BrowserService.cpp
@@ -28,6 +28,10 @@
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+#include "BrowserWebAuthn.h"
+#include "BrowserWebAuthnConfirmationDialog.h"
+#endif
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
@@ -59,6 +63,12 @@ const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww")
// Multiple URL's
const QString BrowserService::ADDITIONAL_URL = QStringLiteral("KP2A_URL");
+// WebAuthn
+const QString BrowserService::WEBAUTHN_ATTESTATION_DIRECT = QStringLiteral("direct");
+const QString BrowserService::WEBAUTHN_ATTESTATION_NONE = QStringLiteral("none");
+const QString BrowserService::WEBAUTHN_KEY_FILENAME = QStringLiteral("webauthn.pem");
+const QString BrowserService::WEBAUTHN_SIGNATURE_COUNT = QStringLiteral("WEBAUTHN_SIGNATURE_COUNT");
+
Q_GLOBAL_STATIC(BrowserService, s_browserService);
BrowserService::BrowserService()
@@ -583,10 +593,117 @@ QString BrowserService::getKey(const QString& id)
return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
}
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+// WebAuthn registration
+QJsonObject BrowserService::showWebAuthnRegisterPrompt(const QJsonObject& publicKey,
+ const QString& origin,
+ const StringPairList& keyList)
+{
+ auto db = selectedDatabase();
+ if (!db) {
+ return getWebAuthnError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
+ }
+
+ const auto userJson = publicKey["user"].toObject();
+ const auto username = userJson["name"].toString();
+ const auto siteId = publicKey["rp"]["id"].toString();
+ const auto siteName = publicKey["rp"]["name"].toString();
+ const auto timeoutValue = publicKey["timeout"].toInt();
+ const auto excludeCredentials = publicKey["excludeCredentials"].toArray();
+ const auto attestation = publicKey["attestation"].toString();
+
+ // Only support these two for now
+ if (attestation != WEBAUTHN_ATTESTATION_NONE && attestation != WEBAUTHN_ATTESTATION_DIRECT) {
+ return getWebAuthnError(ERROR_WEBAUTHN_ATTESTATION_NOT_SUPPORTED);
+ }
+
+ const auto authenticatorSelection = publicKey["authenticatorSelection"].toObject();
+ const auto userVerification = authenticatorSelection["userVerification"].toString();
+ if (!browserWebAuthn()->isUserVerificationValid(userVerification)) {
+ return getWebAuthnError(ERROR_WEBAUTHN_INVALID_USER_VERIFICATION);
+ }
+
+ if (!excludeCredentials.isEmpty() && isWebAuthnCredentialExcluded(excludeCredentials, origin, keyList)) {
+ return getWebAuthnError(ERROR_WEBAUTHN_CREDENTIAL_IS_EXCLUDED);
+ }
+
+ const auto timeout = browserWebAuthn()->getTimeout(userVerification, timeoutValue);
+
+ raiseWindow();
+ BrowserWebAuthnConfirmationDialog confirmDialog;
+ confirmDialog.registerCredential(username, siteId, timeout);
+ auto dialogResult = confirmDialog.exec();
+ if (dialogResult == QDialog::Accepted) {
+ const auto publicKeyCredentials = browserWebAuthn()->buildRegisterPublicKeyCredential(publicKey, origin);
+
+ EntryParameters entryParameters;
+ entryParameters.title = QString("%1 (%2)").arg(siteName, tr("WebAuthn"));
+ entryParameters.login = username;
+ entryParameters.password = publicKeyCredentials.id;
+ entryParameters.siteUrl = origin;
+
+ browserService()->addEntry(entryParameters, "", "", false, WEBAUTHN_KEY_FILENAME, publicKeyCredentials.key);
+ hideWindow();
+ return publicKeyCredentials.response;
+ }
+
+ hideWindow();
+ return getWebAuthnError(ERROR_WEBAUTHN_REQUEST_CANCELED);
+}
+
+// WebAuthn authentication
+QJsonObject BrowserService::showWebAuthnAuthenticationPrompt(const QJsonObject& publicKey,
+ const QString& origin,
+ const StringPairList& keyList)
+{
+ auto db = selectedDatabase();
+ if (!db) {
+ return getWebAuthnError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
+ }
+
+ const auto userVerification = publicKey["userVerification"].toString();
+ if (!browserWebAuthn()->isUserVerificationValid(userVerification)) {
+ return getWebAuthnError(ERROR_WEBAUTHN_INVALID_USER_VERIFICATION);
+ }
+
+ // Parse "allowCredentials"
+ const auto entries = getWebAuthnAllowedEntries(publicKey, origin, keyList);
+ if (entries.isEmpty()) {
+ return getWebAuthnError(ERROR_KEEPASS_NO_LOGINS_FOUND);
+ }
+
+ // With single entry, if no verification is needed, return directly
+ if (entries.count() == 1 && userVerification == BrowserWebAuthn::REQUIREMENT_DISCOURAGED) {
+ const auto privateKeyPem = entries.first()->attachments()->value(WEBAUTHN_KEY_FILENAME);
+ const auto id = entries.first()->password();
+ return browserWebAuthn()->buildGetPublicKeyCredential(publicKey, origin, id, privateKeyPem);
+ }
+
+ const auto timeout = publicKey["timeout"].toInt();
+
+ raiseWindow();
+ BrowserWebAuthnConfirmationDialog confirmDialog;
+ confirmDialog.authenticateCredential(entries, origin, timeout);
+ auto dialogResult = confirmDialog.exec();
+ if (dialogResult == QDialog::Accepted) {
+ hideWindow();
+ const auto selectedEntry = confirmDialog.getSelectedEntry();
+ const auto privateKeyPem = selectedEntry->attachments()->value(WEBAUTHN_KEY_FILENAME);
+ const auto id = selectedEntry->password();
+ return browserWebAuthn()->buildGetPublicKeyCredential(publicKey, origin, id, privateKeyPem);
+ }
+
+ hideWindow();
+ return getWebAuthnError(ERROR_WEBAUTHN_REQUEST_CANCELED);
+}
+#endif
+
void BrowserService::addEntry(const EntryParameters& entryParameters,
const QString& group,
const QString& groupUuid,
const bool downloadFavicon,
+ const QString& attachmentFilename,
+ const QByteArray& attachmentFileData,
const QSharedPointer& selectedDb)
{
// TODO: select database based on this key id
@@ -597,12 +714,16 @@ void BrowserService::addEntry(const EntryParameters& entryParameters,
auto* entry = new Entry();
entry->setUuid(QUuid::createUuid());
- entry->setTitle(QUrl(entryParameters.siteUrl).host());
+ entry->setTitle(entryParameters.title.isEmpty() ? QUrl(entryParameters.siteUrl).host() : entryParameters.title);
entry->setUrl(entryParameters.siteUrl);
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
entry->setUsername(entryParameters.login);
entry->setPassword(entryParameters.password);
+ if (!attachmentFilename.isEmpty() && !attachmentFileData.isEmpty()) {
+ entry->attachments()->set(attachmentFilename, attachmentFileData);
+ }
+
// Select a group for the entry
if (!group.isEmpty()) {
if (db->rootGroup()) {
@@ -643,10 +764,10 @@ bool BrowserService::updateEntry(const EntryParameters& entryParameters, const Q
return false;
}
- Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
+ auto entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
if (!entry) {
// If entry is not found for update, add a new one to the selected database
- addEntry(entryParameters, "", "", false, db);
+ addEntry(entryParameters, "", "", false, "", "", db);
return true;
}
@@ -1083,6 +1204,59 @@ bool BrowserService::shouldIncludeEntry(Entry* entry,
return false;
}
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+// Returns all WebAuthn entries for the current site
+QList BrowserService::getWebAuthnEntries(const QString& origin, const StringPairList& keyList)
+{
+ QList entries;
+ for (const auto& entry : searchEntries(origin, "", keyList)) {
+ if (entry->attachments()->hasKey(WEBAUTHN_KEY_FILENAME) && entry->url() == origin) {
+ entries << entry;
+ }
+ }
+
+ return entries;
+}
+
+// Get all entries for the site that are allowed by the server
+QList BrowserService::getWebAuthnAllowedEntries(const QJsonObject& publicKey,
+ const QString& origin,
+ const StringPairList& keyList)
+{
+ QList entries;
+ const auto allowedCredentials = browserWebAuthn()->getAllowedCredentialsFromPublicKey(publicKey);
+
+ for (const auto& entry : getWebAuthnEntries(origin, keyList)) {
+ if (allowedCredentials.contains(entry->password())) {
+ entries << entry;
+ }
+ }
+
+ return entries;
+}
+
+// Checks if the same user ID already exists for the current site
+bool BrowserService::isWebAuthnCredentialExcluded(const QJsonArray& excludeCredentials,
+ const QString& origin,
+ const StringPairList& keyList)
+{
+ QStringList allIds;
+ for (const auto& cred : excludeCredentials) {
+ allIds << cred["id"].toString();
+ }
+
+ const auto webAuthnEntries = getWebAuthnEntries(origin, keyList);
+ return std::any_of(webAuthnEntries.begin(), webAuthnEntries.end(), [&](const auto& entry) {
+ return allIds.contains(entry->password());
+ });
+}
+
+QJsonObject BrowserService::getWebAuthnError(int errorCode) const
+{
+ return QJsonObject({{"errorCode", errorCode}});
+}
+#endif
+
bool BrowserService::handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,
diff --git a/src/browser/BrowserService.h b/src/browser/BrowserService.h
index 9acdfb5583..1e34d979d0 100644
--- a/src/browser/BrowserService.h
+++ b/src/browser/BrowserService.h
@@ -17,10 +17,11 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERSERVICE_H
-#define BROWSERSERVICE_H
+#ifndef KEEPASSXC_BROWSERSERVICE_H
+#define KEEPASSXC_BROWSERSERVICE_H
#include "BrowserAccessControlDialog.h"
+#include "config-keepassx.h"
#include "core/Entry.h"
#include "gui/PasswordGeneratorWidget.h"
@@ -45,6 +46,7 @@ struct KeyPairMessage
struct EntryParameters
{
QString dbid;
+ QString title;
QString login;
QString password;
QString realm;
@@ -82,11 +84,19 @@ class BrowserService : public QObject
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const;
-
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ QJsonObject
+ showWebAuthnRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
+ QJsonObject showWebAuthnAuthenticationPrompt(const QJsonObject& publicKey,
+ const QString& origin,
+ const StringPairList& keyList);
+#endif
void addEntry(const EntryParameters& entryParameters,
const QString& group,
const QString& groupUuid,
const bool downloadFavicon,
+ const QString& attachmentFilename = {},
+ const QByteArray& attachmentFileData = {},
const QSharedPointer& selectedDb = {});
bool updateEntry(const EntryParameters& entryParameters, const QString& uuid);
bool deleteEntry(const QString& uuid);
@@ -101,6 +111,10 @@ class BrowserService : public QObject
static const QString OPTION_NOT_HTTP_AUTH;
static const QString OPTION_OMIT_WWW;
static const QString ADDITIONAL_URL;
+ static const QString WEBAUTHN_ATTESTATION_DIRECT;
+ static const QString WEBAUTHN_ATTESTATION_NONE;
+ static const QString WEBAUTHN_KEY_FILENAME;
+ static const QString WEBAUTHN_SIGNATURE_COUNT;
signals:
void requestUnlock();
@@ -149,6 +163,15 @@ private slots:
bool removeFirstDomain(QString& hostname);
bool
shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false);
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ QList getWebAuthnEntries(const QString& origin, const StringPairList& keyList);
+ QList
+ getWebAuthnAllowedEntries(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
+ bool isWebAuthnCredentialExcluded(const QJsonArray& excludeCredentials,
+ const QString& origin,
+ const StringPairList& keyList);
+ QJsonObject getWebAuthnError(int errorCode) const;
+#endif
bool handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,
@@ -179,6 +202,9 @@ private slots:
Q_DISABLE_COPY(BrowserService);
friend class TestBrowser;
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ friend class TestWebAuthn;
+#endif
};
static inline BrowserService* browserService()
@@ -186,4 +212,4 @@ static inline BrowserService* browserService()
return BrowserService::instance();
}
-#endif // BROWSERSERVICE_H
+#endif // KEEPASSXC_BROWSERSERVICE_H
diff --git a/src/browser/BrowserSettings.h b/src/browser/BrowserSettings.h
index a961a56aea..491ec46ee2 100644
--- a/src/browser/BrowserSettings.h
+++ b/src/browser/BrowserSettings.h
@@ -17,8 +17,8 @@
* along with this program. If not, see .
*/
-#ifndef BROWSERSETTINGS_H
-#define BROWSERSETTINGS_H
+#ifndef KEEPASSXC_BROWSERSETTINGS_H
+#define KEEPASSXC_BROWSERSETTINGS_H
#include "NativeMessageInstaller.h"
@@ -91,4 +91,4 @@ inline BrowserSettings* browserSettings()
return BrowserSettings::instance();
}
-#endif // BROWSERSETTINGS_H
+#endif // KEEPASSXC_BROWSERSETTINGS_H
diff --git a/src/browser/BrowserWebAuthn.cpp b/src/browser/BrowserWebAuthn.cpp
new file mode 100644
index 0000000000..53a1ae59e9
--- /dev/null
+++ b/src/browser/BrowserWebAuthn.cpp
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "BrowserWebAuthn.h"
+#include "BrowserMessageBuilder.h"
+#include "BrowserService.h"
+#include "crypto/Random.h"
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+Q_GLOBAL_STATIC(BrowserWebAuthn, s_browserWebAuthn);
+
+const QString BrowserWebAuthn::PUBLIC_KEY = QStringLiteral("public-key");
+const QString BrowserWebAuthn::REQUIREMENT_DISCOURAGED = QStringLiteral("discouraged");
+const QString BrowserWebAuthn::REQUIREMENT_PREFERRED = QStringLiteral("preferred");
+const QString BrowserWebAuthn::REQUIREMENT_REQUIRED = QStringLiteral("required");
+
+BrowserWebAuthn* BrowserWebAuthn::instance()
+{
+ return s_browserWebAuthn;
+}
+
+PublicKeyCredential BrowserWebAuthn::buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
+ const QString& origin,
+ const PredefinedVariables& predefinedVariables)
+{
+ QJsonObject publicKeyCredential;
+ const auto id = predefinedVariables.credentialId.isEmpty()
+ ? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
+ : predefinedVariables.credentialId;
+
+ // Extensions
+ auto extensionObject = publicKeyCredentialOptions["extensions"].toObject();
+ const auto extensionData = buildExtensionData(extensionObject);
+ const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
+
+ // Response
+ QJsonObject responseObject;
+ const auto clientData = buildClientDataJson(publicKeyCredentialOptions, origin, false);
+ const auto attestationObject =
+ buildAttestationObject(publicKeyCredentialOptions, extensions, id, predefinedVariables);
+ responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientData);
+ responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject.cborEncoded);
+
+ // PublicKeyCredential
+ publicKeyCredential["authenticatorAttachment"] = QString("platform");
+ publicKeyCredential["id"] = id;
+ publicKeyCredential["response"] = responseObject;
+ publicKeyCredential["type"] = PUBLIC_KEY;
+
+ return {id, publicKeyCredential, attestationObject.pem};
+}
+
+QJsonObject BrowserWebAuthn::buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
+ const QString& origin,
+ const QString& id,
+ const QString& privateKeyPem)
+{
+ const auto authenticatorData = buildGetAttestationObject(publicKeyCredentialRequestOptions);
+ const auto clientData = buildClientDataJson(publicKeyCredentialRequestOptions, origin, true);
+ const auto clientDataArray = QJsonDocument(clientData).toJson(QJsonDocument::Compact);
+ const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
+
+ QJsonObject responseObject;
+ responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
+ responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
+ responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
+
+ QJsonObject publicKeyCredential;
+ publicKeyCredential["authenticatorAttachment"] = QString("platform");
+ publicKeyCredential["id"] = id;
+ publicKeyCredential["response"] = responseObject;
+ publicKeyCredential["type"] = PUBLIC_KEY;
+
+ return publicKeyCredential;
+}
+
+bool BrowserWebAuthn::isUserVerificationValid(const QString& userVerification) const
+{
+ return QStringList({REQUIREMENT_PREFERRED, REQUIREMENT_REQUIRED, REQUIREMENT_DISCOURAGED})
+ .contains(userVerification);
+}
+
+// See https://w3c.github.io/webauthn/#sctn-createCredential for default timeout values when not set in the request
+int BrowserWebAuthn::getTimeout(const QString& userVerification, int timeout) const
+{
+ if (timeout == 0) {
+ return userVerification == REQUIREMENT_DISCOURAGED ? DEFAULT_DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT;
+ }
+
+ return timeout;
+}
+
+QStringList BrowserWebAuthn::getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const
+{
+ QStringList allowedCredentials;
+ for (const auto& cred : publicKey["allowCredentials"].toArray()) {
+ const auto c = cred.toObject();
+ const auto id = c["id"].toString();
+
+ if (c["type"].toString() == PUBLIC_KEY && !id.isEmpty()) {
+ allowedCredentials << id;
+ }
+ }
+
+ return allowedCredentials;
+}
+
+QJsonObject BrowserWebAuthn::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get)
+{
+ QJsonObject clientData;
+ clientData["challenge"] = publicKey["challenge"];
+ clientData["crossOrigin"] = false;
+ clientData["origin"] = origin;
+ clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
+
+ return clientData;
+}
+
+// https://w3c.github.io/webauthn/#attestation-object
+PrivateKey BrowserWebAuthn::buildAttestationObject(const QJsonObject& publicKey,
+ const QString& extensions,
+ const QString& id,
+ const PredefinedVariables& predefinedVariables)
+{
+ QByteArray result;
+
+ // Create SHA256 hash from rpId
+ const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rp"]["id"].toString());
+ result.append(rpIdHash);
+
+ // Use default flags
+ const auto flags =
+ setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
+ {"AT", true},
+ {"BS", false},
+ {"BE", false},
+ {"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
+ {"UP", true}}));
+ result.append(flags);
+
+ // Signature counter (not supported, always 0
+ const char counter[4] = {0x00, 0x00, 0x00, 0x00};
+ result.append(QByteArray::fromRawData(counter, 4));
+
+ // AAGUID (use the default/non-set)
+ result.append("\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b");
+
+ // Credential length
+ const char credentialLength[2] = {0x00, 0x20};
+ result.append(QByteArray::fromRawData(credentialLength, 2));
+
+ // Credential Id
+ result.append(QByteArray::fromBase64(
+ predefinedVariables.credentialId.isEmpty() ? id.toUtf8() : predefinedVariables.credentialId.toUtf8(),
+ QByteArray::Base64UrlEncoding));
+
+ // Credential private key
+ const auto credentialPublicKey =
+ buildCredentialPrivateKey(WebAuthnAlgorithms::ES256, predefinedVariables.x, predefinedVariables.y);
+ result.append(credentialPublicKey.cborEncoded);
+
+ // Add extension data if available
+ if (!extensions.isEmpty()) {
+ result.append(browserMessageBuilder()->getArrayFromBase64(extensions));
+ }
+
+ // The final result should be CBOR encoded
+ return {m_browserCbor.cborEncodeAttestation(result), credentialPublicKey.pem};
+}
+
+// Build a short version of the attestation object for webauthn.get
+QByteArray BrowserWebAuthn::buildGetAttestationObject(const QJsonObject& publicKey)
+{
+ QByteArray result;
+
+ const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rpId"].toString());
+ result.append(rpIdHash);
+
+ const auto flags =
+ setFlagsFromJson(QJsonObject({{"ED", false},
+ {"AT", false},
+ {"BS", false},
+ {"BE", false},
+ {"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
+ {"UP", true}}));
+ result.append(flags);
+
+ // Signature counter (not supported, always 0
+ const char counter[4] = {0x00, 0x00, 0x00, 0x00};
+ result.append(QByteArray::fromRawData(counter, 4));
+
+ return result;
+}
+
+// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
+PrivateKey BrowserWebAuthn::buildCredentialPrivateKey(int alg, const QString& predefinedX, const QString& predefinedY)
+{
+ // Only support -7, P256 (EC) for now
+ if (alg != WebAuthnAlgorithms::ES256) {
+ return {};
+ }
+
+ QByteArray xPart;
+ QByteArray yPart;
+ QByteArray pem;
+
+ if (!predefinedX.isEmpty() && !predefinedY.isEmpty()) {
+ xPart = browserMessageBuilder()->getArrayFromBase64(predefinedX);
+ yPart = browserMessageBuilder()->getArrayFromBase64(predefinedY);
+ } else {
+ try {
+ Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"));
+ const auto& publicPoint = privateKey.public_point();
+ auto x = publicPoint.get_affine_x();
+ auto y = publicPoint.get_affine_y();
+ xPart = bigIntToQByteArray(x);
+ yPart = bigIntToQByteArray(y);
+
+ auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
+ pem = QByteArray::fromStdString(privateKeyPem);
+ } catch (std::exception& e) {
+ qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create private key: %s", e.what());
+ return {};
+ }
+ }
+
+ auto result = m_browserCbor.cborEncodePublicKey(alg, xPart, yPart);
+ return {result, pem};
+}
+
+QByteArray BrowserWebAuthn::buildSignature(const QByteArray& authenticatorData,
+ const QByteArray& clientData,
+ const QString& privateKeyPem)
+{
+ const auto clientDataHash = browserMessageBuilder()->getSha256Hash(clientData);
+ const auto attToBeSigned = authenticatorData + clientDataHash;
+
+ try {
+ const auto privateKeyArray = privateKeyPem.toUtf8();
+ Botan::DataSource_Memory dataSource(reinterpret_cast(privateKeyArray.constData()),
+ privateKeyArray.size());
+
+ const auto key = Botan::PKCS8::load_key(dataSource).release();
+ const auto privateKeyBytes = key->private_key_bits();
+ const auto algId = getAlgorithmIdentifier();
+ if (algId.parameters_are_empty() || algId.parameters_are_null()) {
+ return {};
+ }
+
+ // Sign
+ Botan::ECDSA_PrivateKey privateKey(algId, privateKeyBytes);
+ Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::DER_SEQUENCE);
+ signer.update(reinterpret_cast(attToBeSigned.constData()), attToBeSigned.size());
+ auto rawSignature = signer.signature(*randomGen()->getRng());
+ auto signature = QByteArray(reinterpret_cast(rawSignature.data()), rawSignature.size());
+ return signature;
+ } catch (std::exception& e) {
+ qWarning("BrowserWebAuthn::buildSignature: Could not sign key: %s", e.what());
+ return {};
+ }
+}
+
+QByteArray BrowserWebAuthn::buildExtensionData(QJsonObject& extensionObject) const
+{
+ // Only supports "credProps" and "uvm" for now
+ const QStringList allowedKeys = {"credProps", "uvm"};
+
+ // Remove unsupported keys
+ for (const auto& key : extensionObject.keys()) {
+ if (!allowedKeys.contains(key)) {
+ extensionObject.remove(key);
+ }
+ }
+
+ auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
+ if (!extensionData.isEmpty()) {
+ return extensionData;
+ }
+
+ return {};
+}
+
+// Parse authentication data byte array to JSON
+// See: https://www.w3.org/TR/webauthn/images/fido-attestation-structures.svg
+QJsonObject BrowserWebAuthn::parseAuthData(const QByteArray& authData) const
+{
+ auto rpIdHash = authData.mid(AuthDataOffsets::RPIDHASH, HASH_BYTES);
+ auto flags = authData.mid(AuthDataOffsets::FLAGS, 1);
+ auto counter = authData.mid(AuthDataOffsets::SIGNATURE_COUNTER, 4);
+ auto aaGuid = authData.mid(AuthDataOffsets::AAGUID, 16);
+ auto credentialLength = authData.mid(AuthDataOffsets::CREDENTIAL_LENGTH, 2);
+ auto credLen = qFromBigEndian(credentialLength.data());
+ auto credentialId = authData.mid(AuthDataOffsets::CREDENTIAL_ID, credLen);
+ auto publicKey = authData.mid(AuthDataOffsets::CREDENTIAL_ID + credLen);
+
+ QJsonObject credentialDataJson({{"aaguid", browserMessageBuilder()->getBase64FromArray(aaGuid)},
+ {"credentialId", browserMessageBuilder()->getBase64FromArray(credentialId)},
+ {"publicKey", m_browserCbor.getJsonFromCborData(publicKey)}});
+
+ QJsonObject result({{"credentialData", credentialDataJson},
+ {"flags", parseFlags(flags)},
+ {"rpIdHash", browserMessageBuilder()->getBase64FromArray(rpIdHash)},
+ {"signatureCounter", QJsonValue(qFromBigEndian(counter))}});
+
+ return result;
+}
+
+// See: https://w3c.github.io/webauthn/#table-authData
+QJsonObject BrowserWebAuthn::parseFlags(const QByteArray& flags) const
+{
+ if (flags.isEmpty()) {
+ return {};
+ }
+
+ auto flagsByte = static_cast(flags[0]);
+ std::bitset<8> flagBits(flagsByte);
+
+ return QJsonObject({{"ED", flagBits.test(AuthenticatorFlags::ED)},
+ {"AT", flagBits.test(AuthenticatorFlags::AT)},
+ {"BS", flagBits.test(AuthenticatorFlags::BS)},
+ {"BE", flagBits.test(AuthenticatorFlags::BE)},
+ {"UV", flagBits.test(AuthenticatorFlags::UV)},
+ {"UP", flagBits.test(AuthenticatorFlags::UP)}});
+}
+
+char BrowserWebAuthn::setFlagsFromJson(const QJsonObject& flags) const
+{
+ if (flags.isEmpty()) {
+ return 0;
+ }
+
+ char flagBits = 0x00;
+ auto setFlag = [&](const char* key, unsigned char bit) {
+ if (flags[key].toBool()) {
+ flagBits |= 1 << bit;
+ }
+ };
+
+ setFlag("ED", AuthenticatorFlags::ED);
+ setFlag("AT", AuthenticatorFlags::AT);
+ setFlag("BS", AuthenticatorFlags::BS);
+ setFlag("BE", AuthenticatorFlags::BE);
+ setFlag("UV", AuthenticatorFlags::UV);
+ setFlag("UP", AuthenticatorFlags::UP);
+
+ return flagBits;
+}
+
+Botan::AlgorithmIdentifier BrowserWebAuthn::getAlgorithmIdentifier() const
+{
+ try {
+ const auto oidStr = QStringLiteral("1.2.840.10045.2.1"); // Elliptic curve public key cryptography
+ const auto parameters = QVector({6, 8, 42, 134, 72, 206, 61, 3, 1, 7}).toStdVector();
+ const auto oid = Botan::OID(oidStr.toStdString());
+ Botan::AlgorithmIdentifier algId(oid, parameters);
+
+ return algId;
+ } catch (std::exception& e) {
+ qWarning("BrowserWebAuthn::getAlgorithmIdentifier: Could not create AlgorithmIdentifier: %s", e.what());
+ return {};
+ }
+}
+
+QByteArray BrowserWebAuthn::bigIntToQByteArray(Botan::BigInt& bigInt) const
+{
+ if (bigInt.bytes() < HASH_BYTES) {
+ return {};
+ }
+
+ return browserMessageBuilder()->getArrayFromHexString(bigInt.to_hex_string().c_str());
+}
diff --git a/src/browser/BrowserWebAuthn.h b/src/browser/BrowserWebAuthn.h
new file mode 100644
index 0000000000..600876ff47
--- /dev/null
+++ b/src/browser/BrowserWebAuthn.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef BROWSERWEBAUTHN_H
+#define BROWSERWEBAUTHN_H
+
+#include "BrowserCbor.h"
+#include
+#include
+
+#include
+#include
+
+#define ID_BYTES 32
+#define HASH_BYTES 32
+#define DEFAULT_TIMEOUT 300000
+#define DEFAULT_DISCOURAGED_TIMEOUT 120000
+
+enum AuthDataOffsets : int
+{
+ RPIDHASH = 0,
+ FLAGS = 32,
+ SIGNATURE_COUNTER = 33,
+ AAGUID = 37,
+ CREDENTIAL_LENGTH = 53,
+ CREDENTIAL_ID = 55
+};
+
+enum AuthenticatorFlags
+{
+ UP = 0,
+ UV = 2,
+ BE = 3,
+ BS = 4,
+ AT = 6,
+ ED = 7
+};
+
+struct PublicKeyCredential
+{
+ QString id;
+ QJsonObject response;
+ QByteArray key;
+};
+
+struct PrivateKey
+{
+ QByteArray cborEncoded;
+ QByteArray pem;
+};
+
+// Predefined variables used for testing the class
+struct PredefinedVariables
+{
+ QString credentialId;
+ QString x;
+ QString y;
+};
+
+class BrowserWebAuthn : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit BrowserWebAuthn() = default;
+ ~BrowserWebAuthn() = default;
+ static BrowserWebAuthn* instance();
+
+ PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
+ const QString& origin,
+ const PredefinedVariables& predefinedVariables = {});
+ QJsonObject buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
+ const QString& origin,
+ const QString& id,
+ const QString& privateKeyPem);
+ bool isUserVerificationValid(const QString& userVerification) const;
+ int getTimeout(const QString& userVerification, int timeout) const;
+ QStringList getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const;
+
+ static const QString PUBLIC_KEY;
+ static const QString REQUIREMENT_DISCOURAGED;
+ static const QString REQUIREMENT_PREFERRED;
+ static const QString REQUIREMENT_REQUIRED;
+
+private:
+ QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get);
+ PrivateKey buildAttestationObject(const QJsonObject& publicKey,
+ const QString& extensions,
+ const QString& id,
+ const PredefinedVariables& predefinedVariables = {});
+ QByteArray buildGetAttestationObject(const QJsonObject& publicKey);
+ PrivateKey
+ buildCredentialPrivateKey(int alg, const QString& predefinedX = QString(), const QString& predefinedY = QString());
+ QByteArray
+ buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
+ QByteArray buildExtensionData(QJsonObject& extensionObject) const;
+ QJsonObject parseAuthData(const QByteArray& authData) const;
+ QJsonObject parseFlags(const QByteArray& flags) const;
+ char setFlagsFromJson(const QJsonObject& flags) const;
+ Botan::AlgorithmIdentifier getAlgorithmIdentifier() const;
+ QByteArray bigIntToQByteArray(Botan::BigInt& bigInt) const;
+
+ Q_DISABLE_COPY(BrowserWebAuthn);
+
+ friend class TestWebAuthn;
+
+private:
+ BrowserCbor m_browserCbor;
+};
+
+static inline BrowserWebAuthn* browserWebAuthn()
+{
+ return BrowserWebAuthn::instance();
+}
+
+#endif // BROWSERWEBAUTHN_H
diff --git a/src/browser/BrowserWebAuthnConfirmationDialog.cpp b/src/browser/BrowserWebAuthnConfirmationDialog.cpp
new file mode 100644
index 0000000000..2d72759541
--- /dev/null
+++ b/src/browser/BrowserWebAuthnConfirmationDialog.cpp
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "BrowserWebAuthnConfirmationDialog.h"
+#include "ui_BrowserWebAuthnConfirmationDialog.h"
+
+#include "core/Entry.h"
+#include
+#include
+
+#define STEP 1000
+
+BrowserWebAuthnConfirmationDialog::BrowserWebAuthnConfirmationDialog(QWidget* parent)
+ : QDialog(parent)
+ , m_ui(new Ui::BrowserWebAuthnConfirmationDialog())
+{
+ setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
+
+ m_ui->setupUi(this);
+
+ connect(m_ui->credentialsTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(accept()));
+ connect(m_ui->authenticateButton, SIGNAL(clicked()), SLOT(accept()));
+ connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
+
+ connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
+ connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
+}
+
+BrowserWebAuthnConfirmationDialog::~BrowserWebAuthnConfirmationDialog()
+{
+}
+
+void BrowserWebAuthnConfirmationDialog::registerCredential(const QString& username, const QString& siteId, int timeout)
+{
+ m_ui->confirmationLabel->setText(
+ tr("Do you want to register WebAuthn credentials for:\n%1 (%2)?").arg(username, siteId));
+ m_ui->authenticateButton->setText(tr("Register"));
+ m_ui->credentialsTable->setVisible(false);
+
+ startCounter(timeout);
+}
+
+void BrowserWebAuthnConfirmationDialog::authenticateCredential(const QList& entries,
+ const QString& origin,
+ int timeout)
+{
+ m_entries = entries;
+ m_ui->confirmationLabel->setText(tr("Authenticate WebAuthn credentials for:%1?").arg(origin));
+ m_ui->credentialsTable->setRowCount(entries.count());
+ m_ui->credentialsTable->setColumnCount(1);
+
+ int row = 0;
+ for (const auto& entry : entries) {
+ auto item = new QTableWidgetItem();
+ item->setText(entry->title() + " - " + entry->username());
+ m_ui->credentialsTable->setItem(row, 0, item);
+
+ if (row == 0) {
+ item->setSelected(true);
+ }
+
+ ++row;
+ }
+
+ m_ui->credentialsTable->resizeColumnsToContents();
+ m_ui->credentialsTable->horizontalHeader()->setStretchLastSection(true);
+
+ startCounter(timeout);
+}
+
+Entry* BrowserWebAuthnConfirmationDialog::getSelectedEntry() const
+{
+ auto selectedItem = m_ui->credentialsTable->currentItem();
+ return m_entries[selectedItem->row()];
+}
+
+void BrowserWebAuthnConfirmationDialog::updateProgressBar()
+{
+ if (m_counter < m_ui->progressBar->maximum()) {
+ m_ui->progressBar->setValue(m_ui->progressBar->maximum() - m_counter);
+ m_ui->progressBar->update();
+ } else {
+ emit reject();
+ }
+}
+
+void BrowserWebAuthnConfirmationDialog::updateSeconds()
+{
+ ++m_counter;
+ updateTimeoutLabel();
+}
+
+void BrowserWebAuthnConfirmationDialog::startCounter(int timeout)
+{
+ m_counter = 0;
+ m_ui->progressBar->setMaximum(timeout / STEP);
+ updateProgressBar();
+ updateTimeoutLabel();
+ m_timer.start(STEP);
+}
+
+void BrowserWebAuthnConfirmationDialog::updateTimeoutLabel()
+{
+ m_ui->timeoutLabel->setText(tr("Timeout in %n seconds...", "", m_ui->progressBar->maximum() - m_counter));
+}
diff --git a/src/browser/BrowserWebAuthnConfirmationDialog.h b/src/browser/BrowserWebAuthnConfirmationDialog.h
new file mode 100644
index 0000000000..1b759d2464
--- /dev/null
+++ b/src/browser/BrowserWebAuthnConfirmationDialog.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_BROWSERWEBAUTHNCONFIRMATIONDIALOG_H
+#define KEEPASSXC_BROWSERWEBAUTHNCONFIRMATIONDIALOG_H
+
+#include
+#include
+#include
+
+class Entry;
+
+namespace Ui
+{
+ class BrowserWebAuthnConfirmationDialog;
+}
+
+class BrowserWebAuthnConfirmationDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit BrowserWebAuthnConfirmationDialog(QWidget* parent = nullptr);
+ ~BrowserWebAuthnConfirmationDialog() override;
+
+ void registerCredential(const QString& username, const QString& siteId, int timeout);
+ void authenticateCredential(const QList& entries, const QString& origin, int timeout);
+ Entry* getSelectedEntry() const;
+
+private slots:
+ void updateProgressBar();
+ void updateSeconds();
+
+private:
+ void startCounter(int timeout);
+ void updateTimeoutLabel();
+
+private:
+ QScopedPointer m_ui;
+ QList m_entries;
+ QTimer m_timer;
+ int m_counter;
+};
+
+#endif // KEEPASSXC_BROWSERWEBAUTHNCONFIRMATIONDIALOG_H
diff --git a/src/browser/BrowserWebAuthnConfirmationDialog.ui b/src/browser/BrowserWebAuthnConfirmationDialog.ui
new file mode 100755
index 0000000000..09e399085a
--- /dev/null
+++ b/src/browser/BrowserWebAuthnConfirmationDialog.ui
@@ -0,0 +1,108 @@
+
+
+ BrowserWebAuthnConfirmationDialog
+
+
+
+ 0
+ 0
+ 405
+ 243
+
+
+
+ KeePassXC: WebAuthn credentials
+
+
+ -
+
+
+
+ true
+
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ QAbstractItemView::SingleSelection
+
+
+ false
+
+
+ false
+
+
+ false
+
+
+
+ -
+
+
+ 0
+
+
+ false
+
+
+
+ -
+
+
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Cancel
+
+
+ true
+
+
+
+ -
+
+
+ Authenticate
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt
index 9bd0538574..96bc957886 100755
--- a/src/browser/CMakeLists.txt
+++ b/src/browser/CMakeLists.txt
@@ -1,4 +1,3 @@
-# Copyright (C) 2017 Sami Vänttinen
# Copyright (C) 2022 KeePassXC Team
#
# This program is free software: you can redistribute it and/or modify
@@ -29,8 +28,19 @@ if(WITH_XC_BROWSER)
BrowserService.cpp
BrowserSettings.cpp
BrowserShared.cpp
- NativeMessageInstaller.cpp
- )
+ NativeMessageInstaller.cpp)
+
+ if(WITH_XC_BROWSER_WEBAUTHN)
+ # CBOR requires Qt 5.12
+ if(Qt5Core_VERSION VERSION_LESS "5.12.0")
+ message(FATAL_ERROR "Qt version 5.12.0 or higher is required for WebAuthn support")
+ endif()
+
+ list(APPEND keepassxcbrowser_SOURCES
+ BrowserCbor.cpp
+ BrowserWebAuthn.cpp
+ BrowserWebAuthnConfirmationDialog.cpp)
+ endif()
add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES})
target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES})
diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake
index 840ba0d5e2..4c36b8bd52 100644
--- a/src/config-keepassx.h.cmake
+++ b/src/config-keepassx.h.cmake
@@ -15,6 +15,7 @@
#cmakedefine WITH_XC_AUTOTYPE
#cmakedefine WITH_XC_NETWORKING
#cmakedefine WITH_XC_BROWSER
+#cmakedefine WITH_XC_BROWSER_WEBAUTHN
#cmakedefine WITH_XC_YUBIKEY
#cmakedefine WITH_XC_SSHAGENT
#cmakedefine WITH_XC_KEESHARE
diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp
index 6577971169..851554d7e8 100644
--- a/src/core/Tools.cpp
+++ b/src/core/Tools.cpp
@@ -94,6 +94,9 @@ namespace Tools
#ifdef WITH_XC_BROWSER
extensions += "\n- " + QObject::tr("Browser Integration");
#endif
+#ifdef WITH_XC_BROWSER_WEBAUTHN
+ extensions += "\n- " + QObject::tr("WebAuthn");
+#endif
#ifdef WITH_XC_SSHAGENT
extensions += "\n- " + QObject::tr("SSH Agent");
#endif
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index db82da1639..881a9175b7 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,4 +1,4 @@
-# Copyright (C) 2018 KeePassXC Team
+# Copyright (C) 2022 KeePassXC Team
# Copyright (C) 2010 Felix Geyer
#
# This program is free software: you can redistribute it and/or modify
@@ -231,6 +231,11 @@ endif()
if(WITH_XC_BROWSER)
add_unit_test(NAME testbrowser SOURCES TestBrowser.cpp
LIBS ${TEST_LIBRARIES})
+
+ if(WITH_XC_BROWSER_WEBAUTHN)
+ add_unit_test(NAME testwebauthn SOURCES TestWebAuthn.cpp
+ LIBS ${TEST_LIBRARIES})
+ endif()
endif()
add_unit_test(NAME testcli SOURCES TestCli.cpp
diff --git a/tests/TestWebAuthn.cpp b/tests/TestWebAuthn.cpp
new file mode 100644
index 0000000000..477cc14f34
--- /dev/null
+++ b/tests/TestWebAuthn.cpp
@@ -0,0 +1,425 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include "TestWebAuthn.h"
+#include "browser/BrowserCbor.h"
+#include "browser/BrowserMessageBuilder.h"
+#include "browser/BrowserService.h"
+#include "crypto/Crypto.h"
+
+#include
+#include
+#include
+#include
+
+using namespace Botan::Sodium;
+
+QTEST_GUILESS_MAIN(TestWebAuthn)
+
+// Register request
+// clang-format off
+const QString PublicKeyCredentialOptions =
+ QString("{"
+ "\"attestation\": \"none\","
+ "\"authenticatorSelection\": {"
+ "\"residentKey\": \"preferred\","
+ "\"requireResidentKey\": false,"
+ "\"userVerification\": \"required\""
+ "},"
+ "\"challenge\": ""\"lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw\","
+ "\"pubKeyCredParams\": ["
+ "{"
+ "\"type\": \"public-key\","
+ "\"alg\": -7"
+ "},"
+ "{"
+ "\"type\": \"public-key\","
+ "\"alg\": -257"
+ "}"
+ "],"
+ "\"rp\": {"
+ "\"name\": \"webauthn.io\","
+ "\"id\": \"webauthn.io\""
+ "},"
+ "\"timeout\": 60000,"
+ "\"excludeCredentials\": [],"
+ "\"user\": {"
+ "\"displayName\": \"Test User\","
+ "\"id\": \"VkdWemRDQlZjMlZ5\","
+ "\"name\": \"Test User\""
+ "}"
+ "}");
+
+// Register response
+const QString PublicKeyCredential =
+ QString("{"
+ "\"authenticatorAttachment\": \"platform\","
+ "\"id\": \"yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8\","
+ "\"rawId\": \"cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef\","
+ "\"response\": {"
+ "\"attestationObject\": "
+ "\"o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAAECAwQFBgcIAQID"
+ "BAUGBwgAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-"
+ "XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M\","
+ "\"clientDataJSON\": "
+ "\"eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96"
+ "azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIi"
+ "wiY3Jvc3NPcmlnaW4iOmZhbHNlfQ\""
+ "},"
+ "\"type\": \"public-key\""
+ "}");
+
+// Get request
+const QString PublicKeyCredentialRequestOptions =
+ QString("{"
+ "\"allowCredentials\": ["
+ "{"
+ "\"id\": \"yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8\","
+ "\"transports\": [\"internal\"],"
+ "\"type\": \"public-key\""
+ "}"
+ "],"
+ "\"challenge\": ""\"9z36vTfQTL95Lf7WnZgyte7ohGeF-XRiLxkL-LuGU1zopRmMIUA1LVwzGpyIm1fOBn1QnRa0QH27ADAaJGHysQ\","
+ "\"rpId\": \"webauthn.io\","
+ "\"timeout\": 60000,"
+ "\"userVerification\": \"required\""
+ "}");
+
+// Get response
+const QString PublicKeyCredentialForGet =
+ QString("{"
+ "\"authenticatorAttachment\": \"platform\","
+ "\"id\": \"yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8\","
+ "\"rawId\": \"cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef\","
+ "\"response\": {"
+ "\"authenticatorData\": \"dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA\","
+ "\"clientDataJSON\": "
+ "\"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Ux"
+ "em9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3"
+ "Jvc3NPcmlnaW4iOmZhbHNlfQ\","
+ "\"signature\": ""\"MEUCIQCTSgoh4Y85a-Zd4eR-DfwQGoLDT6jOW5B3e6GDLB732QIgfshkZ8bD2PlFVOgV-qFQ6S3SAiuPnqJI0YinApWLY3w\","
+ "\"userHandle\": null"
+ "},"
+ "\"type\": \"public-key\""
+ "}");
+// clang-format off
+
+void TestWebAuthn::initTestCase()
+{
+ QVERIFY(Crypto::init());
+}
+
+void TestWebAuthn::init()
+{
+}
+
+void TestWebAuthn::testBase64WithHexStrings()
+{
+ const size_t bufSize = 64;
+ unsigned char buf[bufSize] = {31, 141, 30, 29, 142, 73, 5, 239, 242, 84, 187, 202, 40, 54, 15, 223,
+ 201, 0, 108, 109, 209, 104, 207, 239, 160, 89, 208, 117, 134, 66, 42, 12,
+ 31, 66, 163, 248, 221, 88, 241, 164, 6, 55, 182, 97, 186, 243, 162, 162,
+ 81, 220, 55, 60, 93, 207, 170, 222, 56, 234, 227, 45, 115, 175, 138, 182};
+
+ auto base64FromArray = browserMessageBuilder()->getBase64FromArray(reinterpret_cast(buf), bufSize);
+ QCOMPARE(base64FromArray,
+ QString("H40eHY5JBe_yVLvKKDYP38kAbG3RaM_voFnQdYZCKgwfQqP43VjxpAY3tmG686KiUdw3PF3Pqt446uMtc6-Ktg"));
+
+ auto arrayFromBase64 = browserMessageBuilder()->getArrayFromBase64(base64FromArray);
+ QCOMPARE(arrayFromBase64.size(), bufSize);
+
+ for (size_t i = 0; i < bufSize; i++) {
+ QCOMPARE(static_cast(arrayFromBase64.at(i)), buf[i]);
+ }
+}
+
+void TestWebAuthn::testDecodeResponseData()
+{
+ const auto publicKeyCredential = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
+ auto response = publicKeyCredential["response"].toObject();
+ auto clientDataJson = response["clientDataJSON"].toString();
+ auto attestationObject = response["attestationObject"].toString();
+
+ QVERIFY(!clientDataJson.isEmpty());
+ QVERIFY(!attestationObject.isEmpty());
+
+ // Parse clientDataJSON
+ auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
+ auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
+ QCOMPARE(clientDataJsonObject["challenge"],
+ QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
+ QCOMPARE(clientDataJsonObject["origin"], QString("https://webauthn.io"));
+ QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
+
+ // Parse attestationObject (CBOR decoding needed)
+ BrowserCbor browserCbor;
+ auto attestationByteArray = browserMessageBuilder()->getArrayFromBase64(attestationObject);
+ auto attestationJsonObject = browserCbor.getJsonFromCborData(attestationByteArray);
+
+ // Parse authData
+ auto authDataJsonObject = attestationJsonObject["authData"].toString();
+ auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
+ QVERIFY(authDataArray.size() >= 37);
+
+ auto authData = browserWebAuthn()->parseAuthData(authDataArray);
+ auto credentialData = authData["credentialData"].toObject();
+ auto flags = authData["flags"].toObject();
+ auto publicKey = credentialData["publicKey"].toObject();
+
+ // The attestationObject should include the same ID after decoding with the response root
+ QCOMPARE(credentialData["credentialId"].toString(), publicKeyCredential["id"].toString());
+ QCOMPARE(credentialData["aaguid"].toString(), QString("AQIDBAUGBwgBAgMEBQYHCA"));
+ QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
+ QCOMPARE(flags["AT"], true);
+ QCOMPARE(flags["UP"], true);
+ QCOMPARE(publicKey["1"], 2);
+ QCOMPARE(publicKey["3"], -7);
+ QCOMPARE(publicKey["-1"], 1);
+ QCOMPARE(publicKey["-2"], QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0"));
+ QCOMPARE(publicKey["-3"], QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M"));
+}
+
+void TestWebAuthn::testLoadingPrivateKeyFromPem()
+{
+ const auto publicKeyCredentialRequestOptions =
+ browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
+ const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
+ "XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
+ "8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
+ "-----END PRIVATE KEY-----");
+
+ const auto authenticatorData =
+ browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
+ const auto clientData = browserMessageBuilder()->getArrayFromBase64(
+ "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
+ "1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
+ "aW4iOmZhbHNlfQ");
+
+ const auto signature = browserWebAuthn()->buildSignature(authenticatorData, clientData, privateKeyPem);
+ QCOMPARE(
+ browserMessageBuilder()->getBase64FromArray(signature.constData(), signature.size()),
+ QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
+}
+
+void TestWebAuthn::testCreatingAttestationObject()
+{
+ // Predefined values for a desired outcome
+ const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
+ const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
+ const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
+
+ const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
+
+ auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
+ QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
+
+ PredefinedVariables predefinedVariables = {id, predefinedX, predefinedY};
+
+ auto result = browserWebAuthn()->buildAttestationObject(publicKeyCredentialOptions, "", id, predefinedVariables);
+ QCOMPARE(
+ QString(result.cborEncoded),
+ QString("\xA3"
+ "cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
+ "9\x7F)%\x0B`\x84\x1E\xF0"
+ "E\x00\x00\x00\x01\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b\x00 \x8B\xB0\xCA"
+ "6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:"
+ "\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X "
+ "e\xE2\xF2\x1F:cq\xD3G\xEA\xE0\xF7\x1F\xCF\xFA\\\xABO\xF6\x86\x88\x80\t\xAE\x81\x8BT\xB2\x9B\x15\x85~"
+ "\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD"
+ "5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7"));
+
+ // Double check that the result can be decoded
+ BrowserCbor browserCbor;
+ auto attestationJsonObject = browserCbor.getJsonFromCborData(result.cborEncoded);
+
+ // Parse authData
+ auto authDataJsonObject = attestationJsonObject["authData"].toString();
+ auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
+ QVERIFY(authDataArray.size() >= 37);
+
+ auto authData = browserWebAuthn()->parseAuthData(authDataArray);
+ auto credentialData = authData["credentialData"].toObject();
+ auto flags = authData["flags"].toObject();
+ auto publicKey = credentialData["publicKey"].toObject();
+
+ // The attestationObject should include the same ID after decoding with the response root
+ QCOMPARE(credentialData["credentialId"].toString(), QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
+ QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
+ QCOMPARE(flags["AT"], true);
+ QCOMPARE(flags["UP"], true);
+ QCOMPARE(publicKey["1"], 2);
+ QCOMPARE(publicKey["3"], -7);
+ QCOMPARE(publicKey["-1"], 1);
+ QCOMPARE(publicKey["-2"], predefinedX);
+ QCOMPARE(publicKey["-3"], predefinedY);
+}
+
+void TestWebAuthn::testRegister()
+{
+ // Predefined values for a desired outcome
+ const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
+ const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
+ const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
+ const auto origin = QString("https://webauthn.io");
+ const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
+ const auto testDataResponse = testDataPublicKey["response"];
+ const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
+
+ PredefinedVariables predefinedVariables = {predefinedId, predefinedX, predefinedY};
+ auto result =
+ browserWebAuthn()->buildRegisterPublicKeyCredential(publicKeyCredentialOptions, origin, predefinedVariables);
+ auto publicKeyCredential = result.response;
+ QCOMPARE(publicKeyCredential["type"], QString("public-key"));
+ QCOMPARE(publicKeyCredential["authenticatorAttachment"], QString("platform"));
+ QCOMPARE(publicKeyCredential["id"], QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
+
+ auto response = publicKeyCredential["response"].toObject();
+ auto attestationObject = response["attestationObject"].toString();
+ auto clientDataJson = response["clientDataJSON"].toString();
+ QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
+
+ // Parse clientDataJSON
+ auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
+ auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
+ QCOMPARE(clientDataJsonObject["challenge"],
+ QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
+ QCOMPARE(clientDataJsonObject["origin"], origin);
+ QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
+}
+
+void TestWebAuthn::testParseGetAuthData()
+{
+ const auto publicKeyCredentialRequestOptions =
+ browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
+
+ auto publicKeyCredentialForGet = browserMessageBuilder()->getJsonObject(PublicKeyCredentialForGet.toUtf8());
+ auto response = publicKeyCredentialForGet["response"].toObject();
+ auto authDataJsonObject = response["authenticatorData"].toString();
+ auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
+ QVERIFY(authDataArray.size() >= 37);
+
+ auto authData = browserWebAuthn()->parseAuthData(authDataArray);
+ auto flags = authData["flags"].toObject();
+ QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
+ QCOMPARE(flags["AT"], false);
+ QCOMPARE(flags["UP"], true);
+ QCOMPARE(flags["UV"], true);
+
+ auto clientDataJson = response["clientDataJSON"].toString();
+ auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
+ auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
+ QCOMPARE(clientDataJsonObject["challenge"].toString(), publicKeyCredentialRequestOptions["challenge"].toString());
+ QCOMPARE(clientDataJsonObject["crossOrigin"].toBool(), false);
+ QCOMPARE(clientDataJsonObject["origin"].toString(), QString("https://webauthn.io"));
+ QCOMPARE(clientDataJsonObject["type"].toString(), QString("webauthn.get"));
+}
+
+void TestWebAuthn::testGet()
+{
+ const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
+ "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
+ "XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
+ "8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
+ "-----END PRIVATE KEY-----");
+ const auto origin = QString("https://webauthn.io");
+ const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
+ const auto publicKeyCredentialRequestOptions =
+ browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
+
+ auto publicKeyCredential =
+ browserWebAuthn()->buildGetPublicKeyCredential(publicKeyCredentialRequestOptions, origin, id, privateKeyPem);
+ QVERIFY(!publicKeyCredential.isEmpty());
+ QCOMPARE(publicKeyCredential["id"].toString(), id);
+
+ auto response = publicKeyCredential["response"].toObject();
+ QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
+ QCOMPARE(response["clientDataJSON"].toString(),
+ QString("eyJjaGFsbGVuZ2UiOiI5ejM2dlRmUVRMOTVMZjdXblpneXRlN29oR2VGLVhSaUx4a0wtTHVHVTF6b3BSbU1JVUExTFZ3ekdwe"
+ "UltMWZPQm4xUW5SYTBRSDI3QURBYUpHSHlzUSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdX"
+ "Robi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ"));
+ QCOMPARE(
+ response["signature"].toString(),
+ QString("MEUCIHFv0lOOGGloi_XoH5s3QDSs__8yAp9ZTMEjNiacMpOxAiEA04LAfO6TE7j12XNxd3zHQpn4kZN82jQFPntPiPBSD5c"));
+
+ auto clientDataJson = response["clientDataJSON"].toString();
+ auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
+ auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
+ QCOMPARE(clientDataJsonObject["challenge"].toString(), publicKeyCredentialRequestOptions["challenge"].toString());
+}
+
+void TestWebAuthn::testExtensions()
+{
+ auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
+ auto result = browserWebAuthn()->buildExtensionData(extensions);
+
+ BrowserCbor cbor;
+ auto extensionJson = cbor.getJsonFromCborData(result);
+ auto uvmArray = extensionJson["uvm"].toArray();
+ QCOMPARE(extensionJson["credProps"].toObject()["rk"].toBool(), true);
+ QCOMPARE(uvmArray.size(), 1);
+ QCOMPARE(uvmArray.first().toArray().size(), 3);
+
+ auto partial = QJsonObject({{"props", true}, {"uvm", true}});
+ auto faulty = QJsonObject({{"uvx", true}});
+ auto partialData = browserWebAuthn()->buildExtensionData(partial);
+ auto faultyData = browserWebAuthn()->buildExtensionData(faulty);
+
+ auto partialJson = cbor.getJsonFromCborData(partialData);
+ QCOMPARE(partialJson["uvm"].toArray().size(), 1);
+
+ auto faultyJson = cbor.getJsonFromCborData(faultyData);
+ QCOMPARE(faultyJson.size(), 0);
+}
+
+void TestWebAuthn::testParseFlags()
+{
+ auto registerResult = browserWebAuthn()->parseFlags("\x45");
+ QCOMPARE(registerResult["ED"], false);
+ QCOMPARE(registerResult["AT"], true);
+ QCOMPARE(registerResult["BS"], false);
+ QCOMPARE(registerResult["BE"], false);
+ QCOMPARE(registerResult["UV"], true);
+ QCOMPARE(registerResult["UP"], true);
+
+ auto getResult = browserWebAuthn()->parseFlags("\x05"); // Only UP and UV
+ QCOMPARE(getResult["ED"], false);
+ QCOMPARE(getResult["AT"], false);
+ QCOMPARE(getResult["BS"], false);
+ QCOMPARE(getResult["BE"], false);
+ QCOMPARE(getResult["UV"], true);
+ QCOMPARE(getResult["UP"], true);
+}
+
+void TestWebAuthn::testSetFlags()
+{
+ auto registerJson =
+ QJsonObject({{"ED", false}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
+ auto registerResult = browserWebAuthn()->setFlagsFromJson(registerJson);
+ QCOMPARE(registerResult, 0x45);
+
+ auto getJson =
+ QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
+ auto getResult = browserWebAuthn()->setFlagsFromJson(getJson);
+ QCOMPARE(getResult, 0x05);
+
+ // With "discouraged", so UV is false
+ auto discouragedJson =
+ QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", false}, {"UP", true}});
+ auto discouragedResult = browserWebAuthn()->setFlagsFromJson(discouragedJson);
+ QCOMPARE(discouragedResult, 0x01);
+}
diff --git a/tests/TestWebAuthn.h b/tests/TestWebAuthn.h
new file mode 100644
index 0000000000..e779fe34af
--- /dev/null
+++ b/tests/TestWebAuthn.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2022 KeePassXC Team
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 or (at your option)
+ * version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef KEEPASSXC_TESTWEBAUTHN_H
+#define KEEPASSXC_TESTWEBAUTHN_H
+
+#include
+
+#include "browser/BrowserWebAuthn.h"
+
+class TestWebAuthn : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void initTestCase();
+ void init();
+
+ void testBase64WithHexStrings();
+ void testDecodeResponseData();
+
+ void testLoadingPrivateKeyFromPem();
+ void testCreatingAttestationObject();
+ void testRegister();
+
+ void testParseGetAuthData();
+ void testGet();
+
+ void testExtensions();
+ void testParseFlags();
+ void testSetFlags();
+
+private:
+ // QPointer m_browserWebAuthn;
+};
+#endif // KEEPASSXC_TESTWEBAUTHN_H