Add basic support for WebAuthn (Passkeys) (#8825)

---------

Co-authored-by: varjolintu <sami.vanttinen@protonmail.com>
Co-authored-by: droidmonkey <support@dmapps.us>
This commit is contained in:
Sami Vänttinen 2023-10-25 17:12:55 +03:00 committed by GitHub
parent 378c2992cd
commit 6f2354c0e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 4464 additions and 144 deletions

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERACCESSCONTROLDIALOG_H
#define BROWSERACCESSCONTROLDIALOG_H
#ifndef KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
#define KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
#include <QDialog>
#include <QTableWidget>
@ -64,4 +64,4 @@ private:
QList<Entry*> m_entriesToConfirm;
};
#endif // BROWSERACCESSCONTROLDIALOG_H
#endif // KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H

View file

@ -16,7 +16,10 @@
*/
#include "BrowserAction.h"
#include "BrowserService.h"
#include "BrowserMessageBuilder.h"
#ifdef WITH_XC_BROWSER_PASSKEYS
#include "BrowserPasskeys.h"
#endif
#include "BrowserSettings.h"
#include "core/Global.h"
#include "core/Tools.h"
@ -36,6 +39,8 @@ static const QString BROWSER_REQUEST_GET_DATABASE_GROUPS = QStringLiteral("get-d
static const QString BROWSER_REQUEST_GET_LOGINS = QStringLiteral("get-logins");
static const QString BROWSER_REQUEST_GET_TOTP = QStringLiteral("get-totp");
static const QString BROWSER_REQUEST_LOCK_DATABASE = QStringLiteral("lock-database");
static const QString BROWSER_REQUEST_PASSKEYS_GET = QStringLiteral("passkeys-get");
static const QString BROWSER_REQUEST_PASSKEYS_REGISTER = QStringLiteral("passkeys-register");
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");
@ -104,6 +109,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_PASSKEYS
} else if (action.compare(BROWSER_REQUEST_PASSKEYS_GET) == 0) {
return handlePasskeysGet(json, action);
} else if (action.compare(BROWSER_REQUEST_PASSKEYS_REGISTER) == 0) {
return handlePasskeysRegister(json, action);
#endif
}
// Action was not recognized
@ -226,18 +237,11 @@ 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 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 +388,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 +516,74 @@ QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const Q
return buildResponse(action, browserRequest.incrementedNonce);
}
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject BrowserAction::handlePasskeysGet(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_PASSKEYS_GET) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto publicKey = browserRequest.getObject("publicKey");
if (publicKey.isEmpty()) {
return getErrorReply(action, ERROR_PASSKEYS_EMPTY_PUBLIC_KEY);
}
const auto origin = browserRequest.getString("origin");
if (!origin.startsWith("https://")) {
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
}
const auto keyList = getConnectionKeys(browserRequest);
const auto response = browserService()->showPasskeysAuthenticationPrompt(publicKey, origin, keyList);
const Parameters params{{"response", response}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handlePasskeysRegister(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_PASSKEYS_REGISTER) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto publicKey = browserRequest.getObject("publicKey");
if (publicKey.isEmpty()) {
return getErrorReply(action, ERROR_PASSKEYS_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()->showPasskeysRegisterPrompt(publicKey, origin, keyList);
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 +609,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;
}

View file

@ -15,10 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERACTION_H
#define BROWSERACTION_H
#ifndef KEEPASSXC_BROWSERACTION_H
#define KEEPASSXC_BROWSERACTION_H
#include "BrowserMessageBuilder.h"
#include "BrowserService.h"
#include <QJsonArray>
#include <QJsonObject>
@ -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 @@ private:
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_PASSKEYS
QJsonObject handlePasskeysGet(const QJsonObject& json, const QString& action);
QJsonObject handlePasskeysRegister(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 @@ private:
friend class TestBrowser;
};
#endif // BROWSERACTION_H
#endif // KEEPASSXC_BROWSERACTION_H

253
src/browser/BrowserCbor.cpp Normal file
View file

@ -0,0 +1,253 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "BrowserCbor.h"
#include "BrowserMessageBuilder.h"
#include <QCborStreamReader>
#include <QCborStreamWriter>
#include <QJsonDocument>
// https://w3c.github.io/webauthn/#sctn-none-attestation
// https://w3c.github.io/webauthn/#sctn-generating-an-attestation-object
QByteArray BrowserCbor::cborEncodeAttestation(const QByteArray& authData) const
{
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;
}
// https://w3c.github.io/webauthn/#authdata-attestedcredentialdata-credentialpublickey
QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, const QByteArray& second) const
{
QByteArray result;
QCborStreamWriter writer(&result);
if (alg == WebAuthnAlgorithms::ES256) {
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(first);
// Key y-coordinate
writer.append(-3);
writer.append(second);
writer.endMap();
} else if (alg == WebAuthnAlgorithms::RS256) {
writer.startMap(4);
// Key type
writer.append(1);
writer.append(getCoseKeyType(alg));
// Signature algorithm
writer.append(3);
writer.append(alg);
// Key modulus
writer.append(-1);
writer.append(first);
// Key exponent
writer.append(-2);
writer.append(second);
writer.endMap();
} else if (alg == WebAuthnAlgorithms::EDDSA) {
// https://www.rfc-editor.org/rfc/rfc8152#section-13.2
writer.startMap(3);
// Curve parameter
writer.append(-1);
writer.append(getCurveParameter(alg));
// Public key
writer.append(-2);
writer.append(first);
// Private key
writer.append(-4);
writer.append(second);
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(quint32(1));
// keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
writer.append(quint16(1));
// matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
writer.append(quint16(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();
}
// https://www.rfc-editor.org/rfc/rfc8152#section-13.1
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;
case WebAuthnAlgorithms::RS256:
return WebAuthnCoseKeyType::RSA;
default:
return WebAuthnCoseKeyType::EC2;
}
}

70
src/browser/BrowserCbor.h Normal file
View file

@ -0,0 +1,70 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_BROWSERCBOR_H
#define KEEPASSXC_BROWSERCBOR_H
#include <QByteArray>
#include <QCborArray>
#include <QCborMap>
#include <QJsonObject>
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
// For RSA: https://www.rfc-editor.org/rfc/rfc8230#section-4
enum WebAuthnCoseKeyType : int
{
OKP = 1, // Octet Keypair
EC2 = 2, // Elliptic Curve
RSA = 3 // RSA
};
class BrowserCbor
{
public:
QByteArray cborEncodeAttestation(const QByteArray& authData) const;
QByteArray cborEncodePublicKey(int alg, const QByteArray& first, const QByteArray& second) 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

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERENTRYCONFIG_H
#define BROWSERENTRYCONFIG_H
#ifndef KEEPASSXC_BROWSERENTRYCONFIG_H
#define KEEPASSXC_BROWSERENTRYCONFIG_H
#include <QObject>
#include <QSet>
@ -55,4 +55,4 @@ private:
QString m_realm;
};
#endif // BROWSERENTRYCONFIG_H
#endif // KEEPASSXC_BROWSERENTRYCONFIG_H

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERENTRYSAVEDIALOG_H
#define BROWSERENTRYSAVEDIALOG_H
#ifndef KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
#define KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
#include "gui/DatabaseTabWidget.h"
@ -45,4 +45,4 @@ private:
QScopedPointer<Ui::BrowserEntrySaveDialog> m_ui;
};
#endif // BROWSERENTRYSAVEDIALOG_H
#endif // KEEPASSXC_BROWSERENTRYSAVEDIALOG_H

View file

@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>KeePassXC-Browser Save Entry</string>
<string>KeePassXC - Select Database</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>

View file

@ -15,8 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef NATIVEMESSAGINGHOST_H
#define NATIVEMESSAGINGHOST_H
#ifndef KEEPASSXC_NATIVEMESSAGINGHOST_H
#define KEEPASSXC_NATIVEMESSAGINGHOST_H
#include <QJsonObject>
#include <QObject>
@ -56,4 +56,4 @@ private:
QList<QLocalSocket*> m_socketList;
};
#endif // NATIVEMESSAGINGHOST_H
#endif // KEEPASSXC_NATIVEMESSAGINGHOST_H

View file

@ -19,11 +19,14 @@
#include "BrowserShared.h"
#include "config-keepassx.h"
#include "core/Global.h"
#include "core/Tools.h"
#include <QCryptographicHash>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#ifdef QT_DEBUG
#include <QDebug>
#endif
#include <botan/sodium.h>
@ -243,6 +246,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 +258,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 +280,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 {};
}
std::shared_ptr<unsigned char[]> buf(new unsigned char[bytes]);
Botan::Sodium::randombytes_buf(buf.get(), bytes);
return getBase64FromArray(reinterpret_cast<const char*>(buf.get()), 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));
}

View file

@ -15,8 +15,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERMESSAGEBUILDER_H
#define BROWSERMESSAGEBUILDER_H
#ifndef KEEPASSXC_BROWSERMESSAGEBUILDER_H
#define KEEPASSXC_BROWSERMESSAGEBUILDER_H
#include <QPair>
#include <QString>
@ -48,7 +48,13 @@ 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_PASSKEYS_ATTESTATION_NOT_SUPPORTED = 20,
ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED = 21,
ERROR_PASSKEYS_REQUEST_CANCELED = 22,
ERROR_PASSKEYS_INVALID_USER_VERIFICATION = 23,
ERROR_PASSKEYS_EMPTY_PUBLIC_KEY = 24,
ERROR_PASSKEYS_INVALID_URL_PROVIDED = 25
};
}
@ -84,6 +90,14 @@ public:
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 +110,4 @@ static inline BrowserMessageBuilder* browserMessageBuilder()
return BrowserMessageBuilder::instance();
}
#endif // BROWSERMESSAGEBUILDER_H
#endif // KEEPASSXC_BROWSERMESSAGEBUILDER_H

View file

@ -0,0 +1,465 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "BrowserPasskeys.h"
#include "BrowserMessageBuilder.h"
#include "BrowserService.h"
#include "crypto/Random.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtEndian>
#include <botan/data_src.h>
#include <botan/ec_group.h>
#include <botan/ecdsa.h>
#include <botan/ed25519.h>
#include <botan/pkcs8.h>
#include <botan/pubkey.h>
#include <botan/rsa.h>
#include <botan/sodium.h>
#include <bitset>
Q_GLOBAL_STATIC(BrowserPasskeys, s_browserPasskeys);
const QString BrowserPasskeys::PUBLIC_KEY = QStringLiteral("public-key");
const QString BrowserPasskeys::REQUIREMENT_DISCOURAGED = QStringLiteral("discouraged");
const QString BrowserPasskeys::REQUIREMENT_PREFERRED = QStringLiteral("preferred");
const QString BrowserPasskeys::REQUIREMENT_REQUIRED = QStringLiteral("required");
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
const QString BrowserPasskeys::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
const QString BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
const QString BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
const QString BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
const QString BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
BrowserPasskeys* BrowserPasskeys::instance()
{
return s_browserPasskeys;
}
PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
const QString& origin,
const TestingVariables& testingVariables)
{
QJsonObject publicKeyCredential;
const auto id = testingVariables.credentialId.isEmpty() ? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
: testingVariables.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, testingVariables);
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 BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
const QString& origin,
const QString& userId,
const QString& userHandle,
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);
responseObject["userHandle"] = userHandle;
QJsonObject publicKeyCredential;
publicKeyCredential["authenticatorAttachment"] = QString("platform");
publicKeyCredential["id"] = userId;
publicKeyCredential["response"] = responseObject;
publicKeyCredential["type"] = PUBLIC_KEY;
return publicKeyCredential;
}
bool BrowserPasskeys::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 BrowserPasskeys::getTimeout(const QString& userVerification, int timeout) const
{
if (timeout == 0) {
return userVerification == REQUIREMENT_DISCOURAGED ? DEFAULT_DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT;
}
return timeout;
}
QStringList BrowserPasskeys::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 BrowserPasskeys::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 BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
const QString& extensions,
const QString& id,
const TestingVariables& testingVariables)
{
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(
testingVariables.credentialId.isEmpty() ? id.toUtf8() : testingVariables.credentialId.toUtf8(),
QByteArray::Base64UrlEncoding));
// Credential private key
const auto alg = getAlgorithmFromPublicKey(publicKey);
const auto credentialPublicKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
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 BrowserPasskeys::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
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
{
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
return {};
}
QByteArray firstPart;
QByteArray secondPart;
QByteArray pem;
if (!predefinedFirst.isEmpty() && !predefinedSecond.isEmpty()) {
firstPart = browserMessageBuilder()->getArrayFromBase64(predefinedFirst);
secondPart = browserMessageBuilder()->getArrayFromBase64(predefinedSecond);
} else {
if (alg == WebAuthnAlgorithms::ES256) {
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();
firstPart = bigIntToQByteArray(x);
secondPart = bigIntToQByteArray(y);
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EC2 private key: %s", e.what());
return {};
}
} else if (alg == WebAuthnAlgorithms::RS256) {
try {
Botan::RSA_PrivateKey privateKey(*randomGen()->getRng(), RSA_BITS, RSA_EXPONENT);
auto modulus = privateKey.get_n();
auto exponent = privateKey.get_e();
firstPart = bigIntToQByteArray(modulus);
secondPart = bigIntToQByteArray(exponent);
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create RSA private key: %s", e.what());
return {};
}
} else if (alg == WebAuthnAlgorithms::EDDSA) {
try {
Botan::Ed25519_PrivateKey key(*randomGen()->getRng());
auto publicKey = key.get_public_key();
auto privateKey = key.get_private_key();
firstPart = browserMessageBuilder()->getQByteArray(publicKey.data(), publicKey.size());
secondPart = browserMessageBuilder()->getQByteArray(privateKey.data(), privateKey.size());
auto privateKeyPem = Botan::PKCS8::PEM_encode(key);
pem = QByteArray::fromStdString(privateKeyPem);
} catch (std::exception& e) {
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
e.what());
return {};
}
}
}
auto result = m_browserCbor.cborEncodePublicKey(alg, firstPart, secondPart);
return {result, pem};
}
QByteArray BrowserPasskeys::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<const uint8_t*>(privateKeyArray.constData()),
privateKeyArray.size());
const auto key = Botan::PKCS8::load_key(dataSource).release();
const auto privateKeyBytes = key->private_key_bits();
const auto algName = key->algo_name();
const auto algId = key->algorithm_identifier();
std::vector<uint8_t> rawSignature;
if (algName == "ECDSA") {
Botan::ECDSA_PrivateKey privateKey(algId, privateKeyBytes);
#ifdef WITH_XC_BOTAN3
Botan::PK_Signer signer(
privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::Signature_Format::DerSequence);
#else
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::DER_SEQUENCE);
#endif
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
rawSignature = signer.signature(*randomGen()->getRng());
} else if (algName == "RSA") {
Botan::RSA_PrivateKey privateKey(algId, privateKeyBytes);
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA3(SHA-256)");
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
rawSignature = signer.signature(*randomGen()->getRng());
} else if (algName == "Ed25519") {
Botan::Ed25519_PrivateKey privateKey(algId, privateKeyBytes);
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "SHA-512");
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
rawSignature = signer.signature(*randomGen()->getRng());
} else {
qWarning("BrowserWebAuthn::buildSignature: Algorithm not supported");
return {};
}
auto signature = QByteArray(reinterpret_cast<char*>(rawSignature.data()), rawSignature.size());
return signature;
} catch (std::exception& e) {
qWarning("BrowserWebAuthn::buildSignature: Could not sign key: %s", e.what());
return {};
}
}
QByteArray BrowserPasskeys::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
// And: https://w3c.github.io/webauthn/#attested-credential-data
QJsonObject BrowserPasskeys::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<quint16>(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<int>(counter))}});
return result;
}
// See: https://w3c.github.io/webauthn/#table-authData
QJsonObject BrowserPasskeys::parseFlags(const QByteArray& flags) const
{
if (flags.isEmpty()) {
return {};
}
auto flagsByte = static_cast<uint8_t>(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 BrowserPasskeys::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;
}
// Returns the first supported algorithm from the pubKeyCredParams list (only support ES256, RS256 and EdDSA for now)
WebAuthnAlgorithms BrowserPasskeys::getAlgorithmFromPublicKey(const QJsonObject& publicKey) const
{
const auto pubKeyCredParams = publicKey["pubKeyCredParams"].toArray();
if (!pubKeyCredParams.isEmpty()) {
const auto alg = pubKeyCredParams.first()["alg"].toInt();
if (alg == WebAuthnAlgorithms::ES256 || alg == WebAuthnAlgorithms::RS256 || alg == WebAuthnAlgorithms::EDDSA) {
return static_cast<WebAuthnAlgorithms>(alg);
}
}
return WebAuthnAlgorithms::ES256;
}
QByteArray BrowserPasskeys::bigIntToQByteArray(Botan::BigInt& bigInt) const
{
auto hexString = QString(bigInt.to_hex_string().c_str());
// Botan might add a leading "0x" to the hex string depending on the version. Remove it.
if (hexString.startsWith(("0x"))) {
hexString.remove(0, 2);
}
return browserMessageBuilder()->getArrayFromHexString(hexString);
}

View file

@ -0,0 +1,143 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERPASSKEYS_H
#define BROWSERPASSKEYS_H
#include "BrowserCbor.h"
#include <QJsonObject>
#include <QObject>
#include <botan/asn1_obj.h>
#include <botan/bigint.h>
#define ID_BYTES 32
#define HASH_BYTES 32
#define DEFAULT_TIMEOUT 300000
#define DEFAULT_DISCOURAGED_TIMEOUT 120000
#define RSA_BITS 2048
#define RSA_EXPONENT 65537
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 TestingVariables
{
QString credentialId;
QString first;
QString second;
};
class BrowserPasskeys : public QObject
{
Q_OBJECT
public:
explicit BrowserPasskeys() = default;
~BrowserPasskeys() = default;
static BrowserPasskeys* instance();
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
const QString& origin,
const TestingVariables& predefinedVariables = {});
QJsonObject buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
const QString& origin,
const QString& userId,
const QString& userHandle,
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;
static const QString PASSKEYS_ATTESTATION_DIRECT;
static const QString PASSKEYS_ATTESTATION_NONE;
static const QString KPEX_PASSKEY_USERNAME;
static const QString KPEX_PASSKEY_GENERATED_USER_ID;
static const QString KPEX_PASSKEY_PRIVATE_KEY_PEM;
static const QString KPEX_PASSKEY_RELYING_PARTY;
static const QString KPEX_PASSKEY_USER_HANDLE;
private:
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get);
PrivateKey buildAttestationObject(const QJsonObject& publicKey,
const QString& extensions,
const QString& id,
const TestingVariables& predefinedVariables = {});
QByteArray buildGetAttestationObject(const QJsonObject& publicKey);
PrivateKey buildCredentialPrivateKey(int alg,
const QString& predefinedFirst = QString(),
const QString& predefinedSecond = 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;
WebAuthnAlgorithms getAlgorithmFromPublicKey(const QJsonObject& publicKey) const;
QByteArray bigIntToQByteArray(Botan::BigInt& bigInt) const;
Q_DISABLE_COPY(BrowserPasskeys);
friend class TestPasskeys;
private:
BrowserCbor m_browserCbor;
};
static inline BrowserPasskeys* browserPasskeys()
{
return BrowserPasskeys::instance();
}
#endif // BROWSERPASSKEYS_H

View file

@ -0,0 +1,153 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "BrowserPasskeysConfirmationDialog.h"
#include "ui_BrowserPasskeysConfirmationDialog.h"
#include "core/Entry.h"
#include <QCloseEvent>
#include <QUrl>
#define STEP 1000
BrowserPasskeysConfirmationDialog::BrowserPasskeysConfirmationDialog(QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::BrowserPasskeysConfirmationDialog())
, m_passkeyUpdated(false)
{
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
m_ui->setupUi(this);
m_ui->updateButton->setVisible(false);
connect(m_ui->credentialsTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(accept()));
connect(m_ui->confirmButton, SIGNAL(clicked()), SLOT(accept()));
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
connect(m_ui->updateButton, SIGNAL(clicked()), SLOT(updatePasskey()));
connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
}
BrowserPasskeysConfirmationDialog::~BrowserPasskeysConfirmationDialog()
{
}
void BrowserPasskeysConfirmationDialog::registerCredential(const QString& username,
const QString& siteId,
const QList<Entry*>& existingEntries,
int timeout)
{
m_ui->firstLabel->setText(tr("Do you want to register Passkey for:"));
m_ui->dataLabel->setText(tr("%1 (%2)").arg(username, siteId));
m_ui->secondLabel->setText("");
if (!existingEntries.isEmpty()) {
m_ui->firstLabel->setText(tr("Existing Passkey found.\nDo you want to register a new Passkey for:"));
m_ui->secondLabel->setText(tr("Select the existing Passkey and press Update to replace it."));
m_ui->updateButton->setVisible(true);
m_ui->confirmButton->setText(tr("Register new"));
updateEntriesToTable(existingEntries);
} else {
m_ui->confirmButton->setText(tr("Register"));
m_ui->credentialsTable->setVisible(false);
}
startCounter(timeout);
}
void BrowserPasskeysConfirmationDialog::authenticateCredential(const QList<Entry*>& entries,
const QString& origin,
int timeout)
{
m_ui->firstLabel->setText(tr("Authenticate Passkey credentials for:"));
m_ui->dataLabel->setText(origin);
m_ui->secondLabel->setText("");
updateEntriesToTable(entries);
startCounter(timeout);
}
Entry* BrowserPasskeysConfirmationDialog::getSelectedEntry() const
{
auto selectedItem = m_ui->credentialsTable->currentItem();
return selectedItem ? m_entries[selectedItem->row()] : nullptr;
}
bool BrowserPasskeysConfirmationDialog::isPasskeyUpdated() const
{
return m_passkeyUpdated;
}
void BrowserPasskeysConfirmationDialog::updatePasskey()
{
m_passkeyUpdated = true;
emit accept();
}
void BrowserPasskeysConfirmationDialog::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 BrowserPasskeysConfirmationDialog::updateSeconds()
{
++m_counter;
updateTimeoutLabel();
}
void BrowserPasskeysConfirmationDialog::startCounter(int timeout)
{
m_counter = 0;
m_ui->progressBar->setMaximum(timeout / STEP);
updateProgressBar();
updateTimeoutLabel();
m_timer.start(STEP);
}
void BrowserPasskeysConfirmationDialog::updateTimeoutLabel()
{
m_ui->timeoutLabel->setText(tr("Timeout in <b>%n</b> seconds...", "", m_ui->progressBar->maximum() - m_counter));
}
void BrowserPasskeysConfirmationDialog::updateEntriesToTable(const QList<Entry*>& entries)
{
m_entries = entries;
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);
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H
#define KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H
#include <QDialog>
#include <QTableWidget>
#include <QTimer>
class Entry;
namespace Ui
{
class BrowserPasskeysConfirmationDialog;
}
class BrowserPasskeysConfirmationDialog : public QDialog
{
Q_OBJECT
public:
explicit BrowserPasskeysConfirmationDialog(QWidget* parent = nullptr);
~BrowserPasskeysConfirmationDialog() override;
void registerCredential(const QString& username,
const QString& siteId,
const QList<Entry*>& existingEntries,
int timeout);
void authenticateCredential(const QList<Entry*>& entries, const QString& origin, int timeout);
Entry* getSelectedEntry() const;
bool isPasskeyUpdated() const;
private slots:
void updatePasskey();
void updateProgressBar();
void updateSeconds();
private:
void startCounter(int timeout);
void updateTimeoutLabel();
void updateEntriesToTable(const QList<Entry*>& entries);
private:
QScopedPointer<Ui::BrowserPasskeysConfirmationDialog> m_ui;
QList<Entry*> m_entries;
QTimer m_timer;
int m_counter;
bool m_passkeyUpdated;
};
#endif // KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H

View file

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>BrowserPasskeysConfirmationDialog</class>
<widget class="QDialog" name="BrowserPasskeysConfirmationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>405</width>
<height>282</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>KeePassXC: Passkey credentials</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="firstLabel">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="dataLabel">
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="secondLabel">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QTableWidget" name="credentialsTable">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="cornerButtonEnabled">
<bool>false</bool>
</property>
<attribute name="horizontalHeaderVisible">
<bool>false</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="timeoutLabel"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateButton">
<property name="text">
<string>Update</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="confirmButton">
<property name="text">
<string>Authenticate</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View file

@ -29,6 +29,10 @@
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "gui/osutils/OSUtils.h"
#ifdef WITH_XC_BROWSER_PASSKEYS
#include "BrowserPasskeys.h"
#include "BrowserPasskeysConfirmationDialog.h"
#endif
#ifdef Q_OS_MACOS
#include "gui/osutils/macutils/MacUtils.h"
#endif
@ -48,6 +52,9 @@ const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
#ifdef WITH_XC_BROWSER_PASSKEYS
static int KEEPASSXCBROWSER_PASSKEY_ICON = 13;
#endif
// These are for the settings and password conversion
static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
@ -607,6 +614,177 @@ QString BrowserService::getKey(const QString& id)
return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
}
#ifdef WITH_XC_BROWSER_PASSKEYS
// Passkey registration
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKey,
const QString& origin,
const StringPairList& keyList)
{
auto db = selectedDatabase();
if (!db) {
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
const auto userJson = publicKey["user"].toObject();
const auto username = userJson["name"].toString();
const auto userHandle = userJson["id"].toString();
const auto rpId = publicKey["rp"]["id"].toString();
const auto rpName = 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 != BrowserPasskeys::PASSKEYS_ATTESTATION_NONE
&& attestation != BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT) {
return getPasskeyError(ERROR_PASSKEYS_ATTESTATION_NOT_SUPPORTED);
}
const auto authenticatorSelection = publicKey["authenticatorSelection"].toObject();
const auto userVerification = authenticatorSelection["userVerification"].toString();
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
}
if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, origin, keyList)) {
return getPasskeyError(ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED);
}
const auto existingEntries = getPasskeyEntries(rpId, keyList);
const auto timeout = browserPasskeys()->getTimeout(userVerification, timeoutValue);
raiseWindow();
BrowserPasskeysConfirmationDialog confirmDialog;
confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
auto dialogResult = confirmDialog.exec();
if (dialogResult == QDialog::Accepted) {
const auto publicKeyCredentials = browserPasskeys()->buildRegisterPublicKeyCredential(publicKey, origin);
if (confirmDialog.isPasskeyUpdated()) {
addPasskeyToEntry(confirmDialog.getSelectedEntry(),
rpId,
rpName,
username,
publicKeyCredentials.id,
userHandle,
publicKeyCredentials.key);
} else {
addPasskeyToGroup(
nullptr, origin, rpId, rpName, username, publicKeyCredentials.id, userHandle, publicKeyCredentials.key);
}
hideWindow();
return publicKeyCredentials.response;
}
hideWindow();
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
}
// Passkey authentication
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
const QString& origin,
const StringPairList& keyList)
{
auto db = selectedDatabase();
if (!db) {
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
const auto userVerification = publicKey["userVerification"].toString();
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
}
// Parse "allowCredentials"
const auto rpId = publicKey["rpId"].toString();
const auto entries = getPasskeyAllowedEntries(publicKey, rpId, keyList);
if (entries.isEmpty()) {
return getPasskeyError(ERROR_KEEPASS_NO_LOGINS_FOUND);
}
// With single entry, if no verification is needed, return directly
if (entries.count() == 1 && userVerification == BrowserPasskeys::REQUIREMENT_DISCOURAGED) {
return getPublicKeyCredentialFromEntry(entries.first(), publicKey, origin);
}
const auto timeout = publicKey["timeout"].toInt();
raiseWindow();
BrowserPasskeysConfirmationDialog confirmDialog;
confirmDialog.authenticateCredential(entries, origin, timeout);
auto dialogResult = confirmDialog.exec();
if (dialogResult == QDialog::Accepted) {
hideWindow();
const auto selectedEntry = confirmDialog.getSelectedEntry();
return getPublicKeyCredentialFromEntry(selectedEntry, publicKey, origin);
}
hideWindow();
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
}
void BrowserService::addPasskeyToGroup(Group* group,
const QString& url,
const QString& rpId,
const QString& rpName,
const QString& username,
const QString& userId,
const QString& userHandle,
const QString& privateKey)
{
// If no group provided, use the default browser group of the selected database
if (!group) {
auto db = selectedDatabase();
if (!db) {
return;
}
group = getDefaultEntryGroup(db);
}
auto* entry = new Entry();
entry->setUuid(QUuid::createUuid());
entry->setGroup(group);
entry->setTitle(tr("%1 (Passkey)").arg(rpName));
entry->setUsername(username);
entry->setUrl(url);
entry->setIcon(KEEPASSXCBROWSER_PASSKEY_ICON);
addPasskeyToEntry(entry, rpId, rpName, username, userId, userHandle, privateKey);
// Remove blank entry history
entry->removeHistoryItems(entry->historyItems());
}
void BrowserService::addPasskeyToEntry(Entry* entry,
const QString& rpId,
const QString& rpName,
const QString& username,
const QString& userId,
const QString& userHandle,
const QString& privateKey)
{
// Reserved for future use
Q_UNUSED(rpName)
Q_ASSERT(entry);
if (!entry) {
return;
}
entry->beginUpdate();
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID, userId, true);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
entry->endUpdate();
}
#endif
void BrowserService::addEntry(const EntryParameters& entryParameters,
const QString& group,
const QString& groupUuid,
@ -621,7 +799,7 @@ 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);
@ -667,7 +845,7 @@ 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);
@ -746,8 +924,10 @@ bool BrowserService::deleteEntry(const QString& uuid)
return true;
}
QList<Entry*>
BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl)
QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
const QString& siteUrl,
const QString& formUrl,
bool passkey)
{
QList<Entry*> entries;
auto* rootGroup = db->rootGroup();
@ -771,10 +951,17 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
continue;
}
if (!shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
if (!passkey && !shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
continue;
}
#ifdef WITH_XC_BROWSER_PASSKEYS
// With Passkeys, check for the Relying Party instead of URL
if (passkey && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
continue;
}
#endif
// Additional URL check may have already inserted the entry to the list
if (!entries.contains(entry)) {
entries.append(entry);
@ -785,8 +972,10 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
return entries;
}
QList<Entry*>
BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList)
QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
const QString& formUrl,
const StringPairList& keyList,
bool passkey)
{
// Check if database is connected with KeePassXC-Browser
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
@ -820,7 +1009,7 @@ BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, co
QList<Entry*> entries;
do {
for (const auto& db : databases) {
entries << searchEntries(db, siteUrl, formUrl);
entries << searchEntries(db, siteUrl, formUrl, passkey);
}
} while (entries.isEmpty() && removeFirstDomain(hostname));
@ -1094,6 +1283,74 @@ bool BrowserService::shouldIncludeEntry(Entry* entry,
return false;
}
#ifdef WITH_XC_BROWSER_PASSKEYS
// Returns all Passkey entries for the current Relying Party
QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const StringPairList& keyList)
{
QList<Entry*> entries;
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
if (entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
entries << entry;
}
}
return entries;
}
// Get all entries for the site that are allowed by the server
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& publicKey,
const QString& rpId,
const StringPairList& keyList)
{
QList<Entry*> entries;
const auto allowedCredentials = browserPasskeys()->getAllowedCredentialsFromPublicKey(publicKey);
for (const auto& entry : getPasskeyEntries(rpId, keyList)) {
// If allowedCredentials.isEmpty() check if entry contains an extra attribute for user handle.
// If that is found, the entry should be allowed.
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
if (allowedCredentials.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID))
|| (allowedCredentials.isEmpty()
&& entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
entries << entry;
}
}
return entries;
}
QJsonObject
BrowserService::getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin)
{
const auto privateKeyPem = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
const auto userId = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID);
const auto userHandle = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
return browserPasskeys()->buildGetPublicKeyCredential(publicKey, origin, userId, userHandle, privateKeyPem);
}
// Checks if the same user ID already exists for the current site
bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
const QString& origin,
const StringPairList& keyList)
{
QStringList allIds;
for (const auto& cred : excludeCredentials) {
allIds << cred["id"].toString();
}
const auto passkeyEntries = getPasskeyEntries(origin, keyList);
return std::any_of(passkeyEntries.begin(), passkeyEntries.end(), [&](const auto& entry) {
return allIds.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID));
});
}
QJsonObject BrowserService::getPasskeyError(int errorCode) const
{
return QJsonObject({{"errorCode", errorCode}});
}
#endif
bool BrowserService::handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,

View file

@ -17,10 +17,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#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,7 +84,30 @@ public:
QString getCurrentTotp(const QString& uuid);
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
bool isPasswordGeneratorRequested() const;
bool isUrlIdentical(const QString& first, const QString& second) const;
QSharedPointer<Database> selectedDatabase();
#ifdef WITH_XC_BROWSER_PASSKEYS
QJsonObject
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
QJsonObject showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
const QString& origin,
const StringPairList& keyList);
void addPasskeyToGroup(Group* group,
const QString& url,
const QString& rpId,
const QString& rpName,
const QString& username,
const QString& userId,
const QString& userHandle,
const QString& privateKey);
void addPasskeyToEntry(Entry* entry,
const QString& rpId,
const QString& rpName,
const QString& username,
const QString& userId,
const QString& userHandle,
const QString& privateKey);
#endif
void addEntry(const EntryParameters& entryParameters,
const QString& group,
const QString& groupUuid,
@ -129,8 +154,12 @@ private:
Hidden
};
QList<Entry*> searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl);
QList<Entry*> searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList);
QList<Entry*> searchEntries(const QSharedPointer<Database>& db,
const QString& siteUrl,
const QString& formUrl,
bool passkey = false);
QList<Entry*>
searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList, bool passkey = false);
QList<Entry*> sortEntries(QList<Entry*>& entries, const QString& siteUrl, const QString& formUrl);
QList<Entry*> confirmEntries(QList<Entry*>& entriesToConfirm,
const EntryParameters& entryParameters,
@ -148,12 +177,22 @@ private:
bool removeFirstDomain(QString& hostname);
bool
shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false);
#ifdef WITH_XC_BROWSER_PASSKEYS
QList<Entry*> getPasskeyEntries(const QString& rpId, const StringPairList& keyList);
QList<Entry*>
getPasskeyAllowedEntries(const QJsonObject& publicKey, const QString& rpId, const StringPairList& keyList);
QJsonObject
getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin);
bool isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
const QString& origin,
const StringPairList& keyList);
QJsonObject getPasskeyError(int errorCode) const;
#endif
bool handleURL(const QString& entryUrl,
const QString& siteUrl,
const QString& formUrl,
const bool omitWwwSubdomain = false);
QSharedPointer<Database> getDatabase();
QSharedPointer<Database> selectedDatabase();
QString getDatabaseRootUuid();
QString getDatabaseRecycleBinUuid();
bool checkLegacySettings(QSharedPointer<Database> db);
@ -176,6 +215,9 @@ private:
Q_DISABLE_COPY(BrowserService);
friend class TestBrowser;
#ifdef WITH_XC_BROWSER_PASSKEYS
friend class TestPasskeys;
#endif
};
static inline BrowserService* browserService()
@ -183,4 +225,4 @@ static inline BrowserService* browserService()
return BrowserService::instance();
}
#endif // BROWSERSERVICE_H
#endif // KEEPASSXC_BROWSERSERVICE_H

View file

@ -17,8 +17,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BROWSERSETTINGS_H
#define BROWSERSETTINGS_H
#ifndef KEEPASSXC_BROWSERSETTINGS_H
#define KEEPASSXC_BROWSERSETTINGS_H
#include "NativeMessageInstaller.h"
@ -92,4 +92,4 @@ inline BrowserSettings* browserSettings()
return BrowserSettings::instance();
}
#endif // BROWSERSETTINGS_H
#endif // KEEPASSXC_BROWSERSETTINGS_H

View file

@ -1,5 +1,4 @@
# Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
# Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
#
# 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
@ -30,8 +29,14 @@ if(WITH_XC_BROWSER)
BrowserSettings.cpp
BrowserShared.cpp
CustomTableWidget.cpp
NativeMessageInstaller.cpp
)
NativeMessageInstaller.cpp)
if(WITH_XC_BROWSER_PASSKEYS)
list(APPEND keepassxcbrowser_SOURCES
BrowserCbor.cpp
BrowserPasskeys.cpp
BrowserPasskeysConfirmationDialog.cpp)
endif()
add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES})
target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES})