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