diff options
Diffstat (limited to 'src/browser/BrowserAction.cpp')
-rwxr-xr-x | src/browser/BrowserAction.cpp | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp new file mode 100755 index 000000000..b15d8ed59 --- /dev/null +++ b/src/browser/BrowserAction.cpp @@ -0,0 +1,554 @@ +/* +* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com> +* Copyright (C) 2017 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 <QJsonDocument> +#include <QJsonParseError> +#include "BrowserAction.h" +#include "BrowserSettings.h" +#include "sodium.h" +#include "sodium/crypto_box.h" +#include "sodium/randombytes.h" +#include "config-keepassx.h" + +BrowserAction::BrowserAction(BrowserService& browserService) : + m_mutex(QMutex::Recursive), + m_browserService(browserService), + m_associated(false) +{ + +} + +QJsonObject BrowserAction::readResponse(const QJsonObject& json) +{ + if (json.isEmpty()) { + return QJsonObject(); + } + + bool triggerUnlock = false; + const QString trigger = json.value("triggerUnlock").toString(); + if (!trigger.isEmpty() && trigger.compare("true", Qt::CaseSensitive) == 0) { + triggerUnlock = true; + } + + const QString action = json.value("action").toString(); + if (action.isEmpty()) { + return QJsonObject(); + } + + QMutexLocker locker(&m_mutex); + if (action.compare("change-public-keys", Qt::CaseSensitive) != 0 && !m_browserService.isDatabaseOpened()) { + if (m_clientPublicKey.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } else if (!m_browserService.openDatabase(triggerUnlock)) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + } + + return handleAction(json); +} + + +// Private functions +/////////////////////// + +QJsonObject BrowserAction::handleAction(const QJsonObject& json) +{ + QString action = json.value("action").toString(); + if (action.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); + } + + if (action.compare("change-public-keys", Qt::CaseSensitive) == 0) { + return handleChangePublicKeys(json, action); + } else if (action.compare("get-databasehash", Qt::CaseSensitive) == 0) { + return handleGetDatabaseHash(json, action); + } else if (action.compare("associate", Qt::CaseSensitive) == 0) { + return handleAssociate(json, action); + } else if (action.compare("test-associate", Qt::CaseSensitive) == 0) { + return handleTestAssociate(json, action); + } else if (action.compare("get-logins", Qt::CaseSensitive) == 0) { + return handleGetLogins(json, action); + } else if (action.compare("generate-password", Qt::CaseSensitive) == 0) { + return handleGeneratePassword(json, action); + } else if (action.compare("set-login", Qt::CaseSensitive) == 0) { + return handleSetLogin(json, action); + } else if (action.compare("lock-database", Qt::CaseSensitive) == 0) { + return handleLockDatabase(json, action); + } + + // Action was not recognized + return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION); +} + +QJsonObject BrowserAction::handleChangePublicKeys(const QJsonObject& json, const QString& action) +{ + QMutexLocker locker(&m_mutex); + const QString nonce = json.value("nonce").toString(); + const QString clientPublicKey = json.value("publicKey").toString(); + + if (clientPublicKey.isEmpty() || nonce.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED); + } + + m_associated = false; + unsigned char pk[crypto_box_PUBLICKEYBYTES]; + unsigned char sk[crypto_box_SECRETKEYBYTES]; + crypto_box_keypair(pk, sk); + + const QString publicKey = getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES); + const QString secretKey = getBase64FromKey(sk, crypto_box_SECRETKEYBYTES); + m_clientPublicKey = clientPublicKey; + m_publicKey = publicKey; + m_secretKey = secretKey; + + QJsonObject response = buildMessage(incrementNonce(nonce)); + response["action"] = action; + response["publicKey"] = publicKey; + + return response; +} + +QJsonObject BrowserAction::handleGetDatabaseHash(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("get-databasehash", Qt::CaseSensitive) == 0) { + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +QJsonObject BrowserAction::handleAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString key = decrypted.value("key").toString(); + if (key.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + QMutexLocker locker(&m_mutex); + if (key.compare(m_clientPublicKey, Qt::CaseSensitive) == 0) { + const QString id = m_browserService.storeKey(key); + if (id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); +} + +QJsonObject BrowserAction::handleTestAssociate(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString responseKey = decrypted.value("key").toString(); + const QString id = decrypted.value("id").toString(); + if (responseKey.isEmpty() || id.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_NOT_OPENED); + } + + QMutexLocker locker(&m_mutex); + const QString key = m_browserService.getKey(id); + if (key.isEmpty() || key.compare(responseKey, Qt::CaseSensitive) != 0) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + m_associated = true; + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + + QMutexLocker locker(&m_mutex); + if (!m_associated) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString submit = decrypted.value("submitUrl").toString(); + const QJsonArray users = m_browserService.findMatchingEntries(id, url, submit, ""); + + if (users.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_LOGINS_FOUND); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = users.count(); + message["entries"] = users; + message["hash"] = hash; + message["id"] = id; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleGeneratePassword(const QJsonObject& json, const QString& action) +{ + const QString nonce = json.value("nonce").toString(); + const QString password = BrowserSettings::generatePassword(); + const QString bits = QString::number(BrowserSettings::getbits()); // For some reason this always returns 1140 bits? + + if (nonce.isEmpty() || password.isEmpty()) { + return QJsonObject(); + } + + QJsonArray arr; + QJsonObject passwd; + passwd["login"] = QString::number(password.length() * 8); //bits; + passwd["password"] = password; + arr.append(passwd); + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["entries"] = arr; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + + QMutexLocker locker(&m_mutex); + if (!m_associated) { + return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED); + } + + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + const QString url = decrypted.value("url").toString(); + if (url.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED); + } + + const QString id = decrypted.value("id").toString(); + const QString login = decrypted.value("login").toString(); + const QString password = decrypted.value("password").toString(); + const QString submitUrl = decrypted.value("submitUrl").toString(); + const QString uuid = decrypted.value("uuid").toString(); + const QString realm; + + if (uuid.isEmpty()) { + m_browserService.addEntry(id, login, password, url, submitUrl, realm); + } else { + m_browserService.updateEntry(id, uuid, login, password, url); + } + + const QString newNonce = incrementNonce(nonce); + + QJsonObject message = buildMessage(newNonce); + message["count"] = QJsonValue::Null; + message["entries"] = QJsonValue::Null; + message["error"] = ""; + message["hash"] = hash; + + return buildResponse(action, message, newNonce); +} + +QJsonObject BrowserAction::handleLockDatabase(const QJsonObject& json, const QString& action) +{ + const QString hash = getDatabaseHash(); + const QString nonce = json.value("nonce").toString(); + const QString encrypted = json.value("message").toString(); + const QJsonObject decrypted = decryptMessage(encrypted, nonce, action); + + if (decrypted.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); + } + + if (hash.isEmpty()) { + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); + } + + QString command = decrypted.value("action").toString(); + if (!command.isEmpty() && command.compare("lock-database", Qt::CaseSensitive) == 0) { + QMutexLocker locker(&m_mutex); + m_browserService.lockDatabase(); + + const QString newNonce = incrementNonce(nonce); + QJsonObject message = buildMessage(newNonce); + + return buildResponse(action, message, newNonce); + } + + return getErrorReply(action, ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED); +} + +QJsonObject BrowserAction::getErrorReply(const QString& action, const int errorCode) const +{ + QJsonObject response; + response["action"] = action; + response["errorCode"] = QString::number(errorCode); + response["error"] = getErrorMessage(errorCode); + return response; +} + +QJsonObject BrowserAction::buildMessage(const QString& nonce) const +{ + QJsonObject message; + message["version"] = KEEPASSX_VERSION; + message["success"] = "true"; + message["nonce"] = nonce; + return message; +} + +QJsonObject BrowserAction::buildResponse(const QString& action, const QJsonObject& message, const QString& nonce) +{ + QJsonObject response; + response["action"] = action; + response["message"] = encryptMessage(message, nonce); + response["nonce"] = nonce; + return response; +} + +QString BrowserAction::getErrorMessage(const int errorCode) const +{ + switch (errorCode) { + case ERROR_KEEPASS_DATABASE_NOT_OPENED: return QObject::tr("Database not opened"); + case ERROR_KEEPASS_DATABASE_HASH_NOT_RECEIVED: return QObject::tr("Database hash not available"); + case ERROR_KEEPASS_CLIENT_PUBLIC_KEY_NOT_RECEIVED: return QObject::tr("Client public key not received"); + case ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE: return QObject::tr("Cannot decrypt message"); + case ERROR_KEEPASS_TIMEOUT_OR_NOT_CONNECTED: return QObject::tr("Timeout or cannot connect to KeePassXC"); + case ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED: return QObject::tr("Action cancelled or denied"); + case ERROR_KEEPASS_CANNOT_ENCRYPT_MESSAGE: return QObject::tr("Cannot encrypt message or public key not found. Is Native Messaging enabled in KeePassXC?"); + case ERROR_KEEPASS_ASSOCIATION_FAILED: return QObject::tr("KeePassXC association failed, try again"); + case ERROR_KEEPASS_KEY_CHANGE_FAILED: return QObject::tr("Key change was not successful"); + case ERROR_KEEPASS_ENCRYPTION_KEY_UNRECOGNIZED: return QObject::tr("Encryption key is not recognized"); + case ERROR_KEEPASS_NO_SAVED_DATABASES_FOUND: return QObject::tr("No saved databases found"); + case ERROR_KEEPASS_INCORRECT_ACTION: return QObject::tr("Incorrect action"); + case ERROR_KEEPASS_EMPTY_MESSAGE_RECEIVED: return QObject::tr("Empty message received"); + case ERROR_KEEPASS_NO_URL_PROVIDED: return QObject::tr("No URL provided"); + case ERROR_KEEPASS_NO_LOGINS_FOUND: return QObject::tr("No logins found"); + default: return QObject::tr("Unknown error"); + } +} + +QString BrowserAction::getDatabaseHash() +{ + QMutexLocker locker(&m_mutex); + QByteArray hash = QCryptographicHash::hash( + (m_browserService.getDatabaseRootUuid() + m_browserService.getDatabaseRecycleBinUuid()).toUtf8(), + QCryptographicHash::Sha256).toHex(); + return QString(hash); +} + +QString BrowserAction::encryptMessage(const QJsonObject& message, const QString& nonce) +{ + if (message.isEmpty() || nonce.isEmpty()) { + return QString(); + } + + const QString reply(QJsonDocument(message).toJson()); + if (!reply.isEmpty()) { + return encrypt(reply, nonce); + } + + return QString(); +} + +QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce, const QString& action) +{ + if (message.isEmpty() || nonce.isEmpty()) { + return QJsonObject(); + } + + QByteArray ba = decrypt(message, nonce); + if (!ba.isEmpty()) { + return getJsonObject(ba); + } + + return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE); +} + +QString BrowserAction::encrypt(const QString plaintext, const QString nonce) +{ + QMutexLocker locker(&m_mutex); + const QByteArray ma = plaintext.toUtf8(); + const QByteArray na = base64Decode(nonce); + const QByteArray ca = base64Decode(m_clientPublicKey); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector<unsigned char> m(ma.cbegin(), ma.cend()); + std::vector<unsigned char> n(na.cbegin(), na.cend()); + std::vector<unsigned char> ck(ca.cbegin(), ca.cend()); + std::vector<unsigned char> sk(sa.cbegin(), sa.cend()); + + std::vector<unsigned char> e; + e.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QString(); + } + + if (crypto_box_easy(e.data(), m.data(), m.size(), n.data(), ck.data(), sk.data()) == 0) { + QByteArray res = getQByteArray(e.data(), (crypto_box_MACBYTES + ma.length())); + return res.toBase64(); + } + + return QString(); +} + +QByteArray BrowserAction::decrypt(const QString encrypted, const QString nonce) +{ + QMutexLocker locker(&m_mutex); + const QByteArray ma = base64Decode(encrypted); + const QByteArray na = base64Decode(nonce); + const QByteArray ca = base64Decode(m_clientPublicKey); + const QByteArray sa = base64Decode(m_secretKey); + + std::vector<unsigned char> m(ma.cbegin(), ma.cend()); + std::vector<unsigned char> n(na.cbegin(), na.cend()); + std::vector<unsigned char> ck(ca.cbegin(), ca.cend()); + std::vector<unsigned char> sk(sa.cbegin(), sa.cend()); + + std::vector<unsigned char> d; + d.resize(max_length); + + if (m.empty() || n.empty() || ck.empty() || sk.empty()) { + return QByteArray(); + } + + if (crypto_box_open_easy(d.data(), m.data(), ma.length(), n.data(), ck.data(), sk.data()) == 0) { + return getQByteArray(d.data(), std::char_traits<char>::length(reinterpret_cast<const char *>(d.data()))); + } + + return QByteArray(); +} + +QString BrowserAction::getBase64FromKey(const uchar* array, const uint len) +{ + return getQByteArray(array, len).toBase64(); +} + +QByteArray BrowserAction::getQByteArray(const uchar* array, const uint len) const +{ + QByteArray qba; + qba.reserve(len); + for (uint i = 0; i < len; ++i) { + qba.append(static_cast<char>(array[i])); + } + return qba; +} + +QJsonObject BrowserAction::getJsonObject(const uchar* pArray, const uint len) const +{ + QByteArray arr = getQByteArray(pArray, len); + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(arr, &err)); + return doc.object(); +} + +QJsonObject BrowserAction::getJsonObject(const QByteArray ba) const +{ + QJsonParseError err; + QJsonDocument doc(QJsonDocument::fromJson(ba, &err)); + return doc.object(); +} + +QByteArray BrowserAction::base64Decode(const QString str) +{ + return QByteArray::fromBase64(str.toUtf8()); +} + +QString BrowserAction::incrementNonce(const QString& nonce) +{ + const QByteArray nonceArray = base64Decode(nonce); + std::vector<unsigned char> n(nonceArray.cbegin(), nonceArray.cend()); + + sodium_increment(n.data(), n.size()); + return getQByteArray(n.data(), n.size()).toBase64(); +} + +void BrowserAction::removeSharedEncryptionKeys() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeSharedEncryptionKeys(); +} + +void BrowserAction::removeStoredPermissions() +{ + QMutexLocker locker(&m_mutex); + m_browserService.removeStoredPermissions(); +} |