keepassxc/src/browser/BrowserAction.cpp
Sami Vänttinen ac2b445db6
Passkeys improvements (#10318)
Refactors the Passkey implementation to include more checks and a structure that is more aligned with the official specification.
Notable changes:
- _BrowserService_ no longer does the checks by itself. A new class _BrowserPasskeysClient_ constructs the relevant objects, acting as a client. _BrowserService_ only acts as a bridge between the client and _BrowserPasskeys_ (authenticator) and calls the relevant popups for user interaction.
- A new helper class _PasskeyUtils_ includes the actual checks and parses the objects.
- _BrowserPasskeys_ is pretty much intact, but some functions have been moved to PasskeyUtils.
- Fixes Ed25519 encoding in _BrowserCBOR_.
- Adds new error messages.
- User confirmation for Passkey retrieval is also asked even if `discouraged` is used. This goes against the specification, but currently there's no other way to verify the user.
- `cross-platform` is also accepted for compatibility. This could be removed if there's a potential issue with it.
- Extension data is now handled correctly during Authentication.
- Allowed and excluded credentials are now handled correctly.
- `KPEX_PASSKEY_GENERATED_USER_ID` is renamed to `KPEX_PASSKEY_CREDENTIAL_ID`
- Adds a new option "Allow localhost with Passkeys" to Browser Integration -> Advanced tab. By default it's not allowed to access HTTP sites, but `http://localhost` can be allowed for debugging and testing purposes for local servers.
- Add tag `Passkey` to a Passkey entry, or an entry with an imported Passkey.

Fixes #10287.
2024-03-06 07:42:01 -05:00

627 lines
24 KiB
C++

/*
* Copyright (C) 2024 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 "BrowserAction.h"
#include "BrowserMessageBuilder.h"
#ifdef WITH_XC_BROWSER_PASSKEYS
#include "BrowserPasskeys.h"
#include "PasskeyUtils.h"
#endif
#include "BrowserSettings.h"
#include "core/Global.h"
#include "core/Tools.h"
#include <QJsonDocument>
#include <QLocalSocket>
const int BrowserAction::MaxUrlLength = 256;
static const QString BROWSER_REQUEST_ASSOCIATE = QStringLiteral("associate");
static const QString BROWSER_REQUEST_CHANGE_PUBLIC_KEYS = QStringLiteral("change-public-keys");
static const QString BROWSER_REQUEST_CREATE_NEW_GROUP = QStringLiteral("create-new-group");
static const QString BROWSER_REQUEST_DELETE_ENTRY = QStringLiteral("delete-entry");
static const QString BROWSER_REQUEST_GENERATE_PASSWORD = QStringLiteral("generate-password");
static const QString BROWSER_REQUEST_GET_DATABASEHASH = QStringLiteral("get-databasehash");
static const QString BROWSER_REQUEST_GET_DATABASE_GROUPS = QStringLiteral("get-database-groups");
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");
QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json)
{
if (json.isEmpty()) {
return getErrorReply("", ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED);
}
bool triggerUnlock = false;
const auto trigger = json.value("triggerUnlock").toString();
if (!trigger.isEmpty() && trigger.compare(TRUE_STR) == 0) {
triggerUnlock = true;
}
const auto action = json.value("action").toString();
if (action.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
if (action.compare(BROWSER_REQUEST_CHANGE_PUBLIC_KEYS) != 0 && action.compare(BROWSER_REQUEST_REQUEST_AUTOTYPE) != 0
&& !browserService()->isDatabaseOpened()) {
if (m_clientPublicKey.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED);
} else if (!browserService()->openDatabase(triggerUnlock)) {
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
}
return handleAction(socket, json);
}
// Private functions
///////////////////////
QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject& json)
{
QString action = json.value("action").toString();
if (action.compare(BROWSER_REQUEST_CHANGE_PUBLIC_KEYS) == 0) {
return handleChangePublicKeys(json, action);
} else if (action.compare(BROWSER_REQUEST_GET_DATABASEHASH) == 0) {
return handleGetDatabaseHash(json, action);
} else if (action.compare(BROWSER_REQUEST_ASSOCIATE) == 0) {
return handleAssociate(json, action);
} else if (action.compare(BROWSER_REQUEST_TEST_ASSOCIATE) == 0) {
return handleTestAssociate(json, action);
} else if (action.compare(BROWSER_REQUEST_GET_LOGINS) == 0) {
return handleGetLogins(json, action);
} else if (action.compare(BROWSER_REQUEST_GENERATE_PASSWORD) == 0) {
return handleGeneratePassword(socket, json, action);
} else if (action.compare(BROWSER_REQUEST_SET_LOGIN) == 0) {
return handleSetLogin(json, action);
} else if (action.compare(BROWSER_REQUEST_LOCK_DATABASE) == 0) {
return handleLockDatabase(json, action);
} else if (action.compare(BROWSER_REQUEST_GET_DATABASE_GROUPS) == 0) {
return handleGetDatabaseGroups(json, action);
} else if (action.compare(BROWSER_REQUEST_CREATE_NEW_GROUP) == 0) {
return handleCreateNewGroup(json, action);
} else if (action.compare(BROWSER_REQUEST_GET_TOTP) == 0) {
return handleGetTotp(json, action);
} else if (action.compare(BROWSER_REQUEST_DELETE_ENTRY) == 0) {
return handleDeleteEntry(json, action);
} else if (action.compare(BROWSER_REQUEST_REQUEST_AUTOTYPE) == 0) {
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
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action)
{
const auto nonce = json.value("nonce").toString();
const auto clientPublicKey = json.value("publicKey").toString();
if (clientPublicKey.isEmpty() || nonce.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED);
}
m_associated = false;
auto keyPair = browserMessageBuilder()->getKeyPair();
if (keyPair.first.isEmpty() || keyPair.second.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED);
}
m_clientPublicKey = clientPublicKey;
m_publicKey = keyPair.first;
m_secretKey = keyPair.second;
auto response = browserMessageBuilder()->buildMessage(browserMessageBuilder()->incrementNonce(nonce));
response["action"] = action;
response["publicKey"] = keyPair.first;
return response;
}
QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
if (browserRequest.hash.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED);
}
const auto command = browserRequest.getString("action");
if (!command.isEmpty() && command.compare(BROWSER_REQUEST_GET_DATABASEHASH) == 0) {
const Parameters params{{"hash", browserRequest.hash}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
const auto key = browserRequest.getString("key");
if (key.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
if (key.compare(m_clientPublicKey) == 0) {
// Check for identification key. If it's not found, ensure backwards compatibility and use the current public
// key
const auto idKey = browserRequest.getString("idKey");
const auto id = browserService()->storeKey((idKey.isEmpty() ? key : idKey));
if (id.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
}
m_associated = true;
const Parameters params{{"hash", browserRequest.hash}, {"id", id}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
const auto responseKey = browserRequest.getString("key");
const auto id = browserRequest.getString("id");
if (responseKey.isEmpty() || id.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED);
}
const auto key = browserService()->getKey(id);
if (key.isEmpty() || key.compare(responseKey) != 0) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
m_associated = true;
const Parameters params{{"hash", browserRequest.hash}, {"id", id}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGetLogins(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 siteUrl = browserRequest.getString("url");
if (siteUrl.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
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;
entryParameters.hash = browserRequest.hash;
entryParameters.siteUrl = siteUrl;
entryParameters.formUrl = formUrl;
entryParameters.httpAuth = httpAuth;
bool entriesFound = false;
const auto entries = browserService()->findEntries(entryParameters, keyList, &entriesFound);
if (!entriesFound) {
return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND);
}
const Parameters params{
{"count", entries.count()}, {"entries", entries}, {"hash", browserRequest.hash}, {"id", id}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGeneratePassword(QLocalSocket* socket, const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
const auto requestId = browserRequest.getString("requestID");
// Do not allow multiple requests from the same client
if (browserService()->isPasswordGeneratorRequested()) {
auto errorReply = getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
// Append requestID to the response if found
if (!requestId.isEmpty()) {
errorReply["requestID"] = requestId;
}
// Show the existing password generator
browserService()->showPasswordGenerator({});
return errorReply;
}
KeyPairMessage keyPairMessage{socket, browserRequest.incrementedNonce, m_clientPublicKey, m_secretKey};
browserService()->showPasswordGenerator(keyPairMessage);
return {};
}
QJsonObject BrowserAction::handleSetLogin(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 url = browserRequest.getString("url");
if (url.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
const auto id = browserRequest.getString("id");
const auto login = browserRequest.getString("login");
const auto password = browserRequest.getString("password");
const auto submitUrl = browserRequest.getString("submitUrl");
const auto uuid = browserRequest.getString("uuid");
const auto group = browserRequest.getString("group");
const auto groupUuid = browserRequest.getString("groupUuid");
const auto downloadFavicon = browserRequest.getString("downloadFavicon");
const QString realm;
EntryParameters entryParameters;
entryParameters.dbid = id;
entryParameters.login = login;
entryParameters.password = password;
entryParameters.siteUrl = url;
entryParameters.formUrl = submitUrl;
entryParameters.realm = realm;
bool result = true;
if (uuid.isEmpty()) {
auto dlFavicon = !downloadFavicon.isEmpty() && downloadFavicon.compare(TRUE_STR) == 0;
browserService()->addEntry(entryParameters, group, groupUuid, dlFavicon);
} else {
if (!Tools::isValidUuid(uuid)) {
return getErrorReply(action, ERROR_KEEPASS_NO_VALID_UUID_PROVIDED);
}
result = browserService()->updateEntry(entryParameters, uuid);
}
const Parameters params{{"count", QJsonValue::Null},
{"entries", QJsonValue::Null},
{"error", result ? QStringLiteral("success") : QStringLiteral("error")},
{"hash", browserRequest.hash}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action)
{
const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}
if (browserRequest.hash.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED);
}
const auto command = browserRequest.getString("action");
if (!command.isEmpty() && command.compare(BROWSER_REQUEST_LOCK_DATABASE) == 0) {
browserService()->lockDatabase();
return buildResponse(action, browserRequest.incrementedNonce);
}
return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED);
}
QJsonObject BrowserAction::handleGetDatabaseGroups(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_GET_DATABASE_GROUPS) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto groups = browserService()->getDatabaseGroups();
if (groups.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND);
}
const Parameters params{{"groups", groups}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGetDatabaseEntries(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("get-database-entries") != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
if (!browserSettings()->allowGetDatabaseEntriesRequest()) {
return getErrorReply(action, ERROR_KEEPASS_ACCESS_TO_ALL_ENTRIES_DENIED);
}
const QJsonArray entries = browserService()->getDatabaseEntries();
if (entries.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_NO_GROUPS_FOUND);
}
const Parameters params{{"entries", entries}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleCreateNewGroup(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_CREATE_NEW_GROUP) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto group = browserRequest.getString("groupName");
const auto newGroup = browserService()->createNewGroup(group);
if (newGroup.isEmpty() || newGroup["name"].toString().isEmpty() || newGroup["uuid"].toString().isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP);
}
const Parameters params{{"name", newGroup["name"]}, {"uuid", newGroup["uuid"]}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGetTotp(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_GET_TOTP) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto uuid = browserRequest.getString("uuid");
if (!Tools::isValidUuid(uuid)) {
return getErrorReply(action, ERROR_KEEPASS_NO_VALID_UUID_PROVIDED);
}
const Parameters params{{"totp", browserService()->getCurrentTotp(uuid)}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleDeleteEntry(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_DELETE_ENTRY) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto uuid = browserRequest.getString("uuid");
if (!Tools::isValidUuid(uuid)) {
return getErrorReply(action, ERROR_KEEPASS_NO_VALID_UUID_PROVIDED);
}
const auto result = browserService()->deleteEntry(uuid);
const Parameters params{{"success", result ? TRUE_STR : FALSE_STR}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const QString& action)
{
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_REQUEST_AUTOTYPE) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}
const auto topLevelDomain = browserRequest.getString("search");
if (topLevelDomain.length() > BrowserAction::MaxUrlLength) {
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}
browserService()->requestGlobalAutoType(topLevelDomain);
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 (!passkeyUtils()->isOriginAllowedWithLocalhost(browserSettings()->allowLocalhostWithPasskeys(), origin)) {
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 (!passkeyUtils()->isOriginAllowedWithLocalhost(browserSettings()->allowLocalhostWithPasskeys(), origin)) {
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
}
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);
}
QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const
{
return browserMessageBuilder()->getErrorReply(action, errorCode);
}
QJsonObject BrowserAction::buildResponse(const QString& action, const QString& nonce, const Parameters& params)
{
return browserMessageBuilder()->buildResponse(action, nonce, params, m_clientPublicKey, m_secretKey);
}
BrowserRequest BrowserAction::decodeRequest(const QJsonObject& json)
{
const auto nonce = json.value("nonce").toString();
const auto encrypted = json.value("message").toString();
return {browserService()->getDatabaseHash(),
nonce,
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;
}