Skip to content

Commit

Permalink
Add basic support for WebAuthn
Browse files Browse the repository at this point in the history
  • Loading branch information
varjolintu authored and varjolintu committed May 8, 2023
1 parent 85d4743 commit f6e8191
Show file tree
Hide file tree
Showing 28 changed files with 2,064 additions and 46 deletions.
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ set(WITH_XC_ALL OFF CACHE BOOL "Build in all available plugins")
option(WITH_XC_AUTOTYPE "Include Auto-Type." ON)
option(WITH_XC_NETWORKING "Include networking code (e.g. for downloading website icons)." OFF)
option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF)
option(WITH_XC_BROWSER_WEBAUTHN "WebAuthn support for browser integration." OFF)
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
Expand Down Expand Up @@ -98,6 +99,7 @@ if(WITH_XC_ALL)
set(WITH_XC_AUTOTYPE ON)
set(WITH_XC_NETWORKING ON)
set(WITH_XC_BROWSER ON)
set(WITH_XC_BROWSER_WEBAUTHN ON)
set(WITH_XC_YUBIKEY ON)
set(WITH_XC_SSHAGENT ON)
set(WITH_XC_KEESHARE ON)
Expand Down
1 change: 1 addition & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ KeePassXC comes with a variety of build options that can turn on/off features. M
-DWITH_XC_AUTOTYPE=[ON|OFF] Enable/Disable Auto-Type (default: ON)
-DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF)
-DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF)
-DWITH_XC_BROWSER_WEBAUTHN=[ON|OFF] Enable/Disable WebAuthn support for browser integration (default: OFF)
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
-DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
Expand Down
43 changes: 43 additions & 0 deletions share/translations/keepassxc_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,10 @@ Do you want to delete the entry?
</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>WebAuthn</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BrowserSettingsWidget</name>
Expand Down Expand Up @@ -1135,6 +1139,41 @@ Do you want to delete the entry?
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>BrowserWebAuthnConfirmationDialog</name>
<message>
<source>Cancel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Authenticate</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Do you want to register WebAuthn credentials for:
%1 (%2)?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Register</source>
<translation type="unfinished"></translation>
</message>
<message numerus="yes">
<source>Timeout in &lt;b&gt;%n&lt;/b&gt; seconds...</source>
<translation type="unfinished">
<numerusform></numerusform>
<numerusform></numerusform>
</translation>
</message>
<message>
<source>KeePassXC: WebAuthn credentials</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Authenticate WebAuthn credentials for:%1?</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>CloneDialog</name>
<message>
Expand Down Expand Up @@ -7941,6 +7980,10 @@ Kernel: %3 %4</source>
<source>Access to all entries is denied</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>WebAuthn</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ set(keepassx_SOURCES_MAINEXE main.cpp)
add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing")
add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)")
add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser")
add_feature_info(WebAuthn WITH_XC_BROWSER_WEBAUTHN "WebAuthn support for browser integration")
add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent")
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
Expand Down
6 changes: 3 additions & 3 deletions src/browser/BrowserAccessControlDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -53,4 +53,4 @@ class BrowserAccessControlDialog : public QDialog
bool m_entriesAccepted;
};

#endif // BROWSERACCESSCONTROLDIALOG_H
#endif // KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
115 changes: 102 additions & 13 deletions src/browser/BrowserAction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
*/

#include "BrowserAction.h"
#include "BrowserService.h"
#include "BrowserSettings.h"
#include "core/Global.h"
#include "BrowserMessageBuilder.h"
#include "BrowserWebAuthn.h"
#include "core/Tools.h"

#include <QJsonDocument>
Expand All @@ -39,6 +40,8 @@ static const QString BROWSER_REQUEST_LOCK_DATABASE = QStringLiteral("lock-databa
static const QString BROWSER_REQUEST_REQUEST_AUTOTYPE = QStringLiteral("request-autotype");
static const QString BROWSER_REQUEST_SET_LOGIN = QStringLiteral("set-login");
static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate");
static const QString BROWSER_REQUEST_WEBAUTHN_GET = QStringLiteral("webauthn-get");
static const QString BROWSER_REQUEST_WEBAUTHN_REGISTER = QStringLiteral("webauthn-register");

QJsonObject BrowserAction::processClientMessage(QLocalSocket* socket, const QJsonObject& json)
{
Expand Down Expand Up @@ -104,6 +107,12 @@ QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject&
return handleGlobalAutoType(json, action);
} else if (action.compare("get-database-entries", Qt::CaseSensitive) == 0) {
return handleGetDatabaseEntries(json, action);
#ifdef WITH_XC_BROWSER_WEBAUTHN
} else if (action.compare(BROWSER_REQUEST_WEBAUTHN_GET) == 0) {
return handleWebAuthnGet(json, action);
} else if (action.compare(BROWSER_REQUEST_WEBAUTHN_REGISTER) == 0) {
return handleWebAuthnRegister(json, action);
#endif
}

// Action was not recognized
Expand Down Expand Up @@ -226,18 +235,12 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
}

const auto keys = browserRequest.getArray("keys");

StringPairList keyList;
for (const auto val : keys) {
const auto keyObject = val.toObject();
keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
}

const auto keys = getConnectionKeys(browserRequest);
const auto id = browserRequest.getString("id");
const auto formUrl = browserRequest.getString("submitUrl");
const auto auth = browserRequest.getString("httpAuth");
const bool httpAuth = auth.compare(TRUE_STR) == 0;
const auto keyList = getConnectionKeys(browserRequest);

EntryParameters entryParameters;
entryParameters.dbid = id;
Expand Down Expand Up @@ -384,10 +387,6 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons

QJsonObject BrowserAction::handleGetDatabaseEntries(const QJsonObject& json, const QString& action)
{
const QString hash = browserService()->getDatabaseHash();
const QString nonce = json.value("nonce").toString();
const QString encrypted = json.value("message").toString();

if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}
Expand Down Expand Up @@ -516,6 +515,83 @@ QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const Q
return buildResponse(action, browserRequest.incrementedNonce);
}

#ifdef WITH_XC_BROWSER_WEBAUTHN
QJsonObject BrowserAction::handleWebAuthnGet(const QJsonObject& json, const QString& action)
{
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}

const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}

const auto command = browserRequest.getString("action");
if (command.isEmpty() || command.compare(BROWSER_REQUEST_WEBAUTHN_GET) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

const auto publicKey = browserRequest.getObject("publicKey");
if (publicKey.isEmpty()) {
return getErrorReply(action, ERROR_WEBAUTHN_EMPTY_PUBLIC_KEY);
}

const auto origin = browserRequest.getString("origin");
if (!origin.startsWith("https://")) {
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
}

const auto keyList = getConnectionKeys(browserRequest);
const auto response = browserService()->showWebAuthnAuthenticationPrompt(publicKey, origin, keyList);

/*auto message = browserMessageBuilder()->buildMessage(browserRequest.incrementedNonce);
message["response"] = response;
return buildResponse(action, message, browserRequest.incrementedNonce);*/
const Parameters params{{"response", response}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}

QJsonObject BrowserAction::handleWebAuthnRegister(const QJsonObject& json, const QString& action)
{
if (!m_associated) {
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
}

const auto browserRequest = decodeRequest(json);
if (browserRequest.isEmpty()) {
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
}

const auto command = browserRequest.getString("action");
if (command.isEmpty() || command.compare(BROWSER_REQUEST_WEBAUTHN_REGISTER) != 0) {
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
}

const auto publicKey = browserRequest.getObject("publicKey");
if (publicKey.isEmpty()) {
return getErrorReply(action, ERROR_WEBAUTHN_EMPTY_PUBLIC_KEY);
}

const auto origin = browserRequest.getString("origin");
if (!origin.startsWith("https://")) {
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
}

const auto keyList = getConnectionKeys(browserRequest);
const auto response = browserService()->showWebAuthnRegisterPrompt(publicKey, origin, keyList);

// Send response
/*auto message = browserMessageBuilder()->buildMessage(browserRequest.incrementedNonce);
message["response"] = response;
return buildResponse(action, message, browserRequest.incrementedNonce);*/
const Parameters params{{"response", response}};
return buildResponse(action, browserRequest.incrementedNonce, params);
}
#endif

QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce)
{
return browserMessageBuilder()->decryptMessage(message, nonce, m_clientPublicKey, m_secretKey);
Expand All @@ -541,3 +617,16 @@ BrowserRequest BrowserAction::decodeRequest(const QJsonObject& json)
browserMessageBuilder()->incrementNonce(nonce),
decryptMessage(encrypted, nonce)};
}

StringPairList BrowserAction::getConnectionKeys(const BrowserRequest& browserRequest)
{
const auto keys = browserRequest.getArray("keys");

StringPairList keyList;
for (const auto val : keys) {
const auto keyObject = val.toObject();
keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
}

return keyList;
}
17 changes: 14 additions & 3 deletions src/browser/BrowserAction.h
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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();
Expand Down Expand Up @@ -73,12 +79,17 @@ class BrowserAction
QJsonObject handleGetTotp(const QJsonObject& json, const QString& action);
QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action);
QJsonObject handleGlobalAutoType(const QJsonObject& json, const QString& action);
#ifdef WITH_XC_BROWSER_WEBAUTHN
QJsonObject handleWebAuthnGet(const QJsonObject& json, const QString& action);
QJsonObject handleWebAuthnRegister(const QJsonObject& json, const QString& action);
#endif

private:
QJsonObject buildResponse(const QString& action, const QString& nonce, const Parameters& params = {});
QJsonObject getErrorReply(const QString& action, const int errorCode) const;
QJsonObject decryptMessage(const QString& message, const QString& nonce);
BrowserRequest decodeRequest(const QJsonObject& json);
StringPairList getConnectionKeys(const BrowserRequest& browserRequest);

private:
static const int MaxUrlLength;
Expand All @@ -91,4 +102,4 @@ class BrowserAction
friend class TestBrowser;
};

#endif // BROWSERACTION_H
#endif // KEEPASSXC_BROWSERACTION_H
Loading

0 comments on commit f6e8191

Please sign in to comment.