mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-05-02 14:46:07 -04:00
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:
parent
378c2992cd
commit
6f2354c0e9
66 changed files with 4464 additions and 144 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
253
src/browser/BrowserCbor.cpp
Normal 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
70
src/browser/BrowserCbor.h
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
465
src/browser/BrowserPasskeys.cpp
Normal file
465
src/browser/BrowserPasskeys.cpp
Normal 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);
|
||||
}
|
143
src/browser/BrowserPasskeys.h
Normal file
143
src/browser/BrowserPasskeys.h
Normal 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
|
153
src/browser/BrowserPasskeysConfirmationDialog.cpp
Normal file
153
src/browser/BrowserPasskeysConfirmationDialog.cpp
Normal 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);
|
||||
}
|
66
src/browser/BrowserPasskeysConfirmationDialog.h
Normal file
66
src/browser/BrowserPasskeysConfirmationDialog.h
Normal 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
|
159
src/browser/BrowserPasskeysConfirmationDialog.ui
Executable file
159
src/browser/BrowserPasskeysConfirmationDialog.ui
Executable 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>
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue