diff --git a/.gitmodules b/.gitmodules index 53e6d1f..ffb426d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "3rdparty/qrcode-svg"] path = 3rdparty/qrcode-svg url = https://github.com/papnkukn/qrcode-svg.git +[submodule "3rdparty/Bcrypt.cpp"] + path = 3rdparty/Bcrypt.cpp + url = https://github.com/hilch/Bcrypt.cpp.git diff --git a/3rdparty/Bcrypt.cpp b/3rdparty/Bcrypt.cpp new file mode 160000 index 0000000..0d18b6a --- /dev/null +++ b/3rdparty/Bcrypt.cpp @@ -0,0 +1 @@ +Subproject commit 0d18b6a99e8c57627910db4ef9a7706c009b12ad diff --git a/3rdparty/CMakeLists.txt b/3rdparty/CMakeLists.txt index 16b4091..7d1a3fe 100644 --- a/3rdparty/CMakeLists.txt +++ b/3rdparty/CMakeLists.txt @@ -1,2 +1,15 @@ add_subdirectory(qmsgpack SYSTEM) add_subdirectory(QtZeroConf SYSTEM) + +# Bcrypt.cpp's upstream CMake file still requires compatibility with CMake 3.2, +# which is no longer supported by current CMake releases. +add_library(bcrypt STATIC + Bcrypt.cpp/src/bcrypt.cpp + Bcrypt.cpp/src/blowfish.cpp +) +set_target_properties(bcrypt PROPERTIES + AUTOMOC OFF + POSITION_INDEPENDENT_CODE ON +) +target_include_directories(bcrypt SYSTEM PUBLIC Bcrypt.cpp/include) +target_include_directories(bcrypt PRIVATE Bcrypt.cpp/src) diff --git a/evcharger-app/CMakeLists.txt b/evcharger-app/CMakeLists.txt index f62b817..0aa634d 100644 --- a/evcharger-app/CMakeLists.txt +++ b/evcharger-app/CMakeLists.txt @@ -181,6 +181,11 @@ target_link_libraries(evcharger-app PUBLIC goecommon ) +target_link_libraries(evcharger-app PRIVATE bcrypt) +target_include_directories(evcharger-app PRIVATE + ${PROJECT_SOURCE_DIR}/3rdparty/Bcrypt.cpp/src +) + install(TARGETS evcharger-app BUNDLE DESTINATION . RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} diff --git a/evcharger-app/deviceconnection.cpp b/evcharger-app/deviceconnection.cpp index d0b21dd..4dbb1e5 100644 --- a/evcharger-app/deviceconnection.cpp +++ b/evcharger-app/deviceconnection.cpp @@ -14,6 +14,7 @@ #include #include "msgpack.h" +#include "node_blf.h" using namespace std::chrono_literals; @@ -23,6 +24,33 @@ auto enumToString(const QEnum value) { return QMetaEnum::fromType().valueToKey(std::to_underlying(value)); } + +QString deriveBcryptPassword(const QString &password, const QString &serial) +{ + constexpr qsizetype bcryptSaltBytes = 16; + constexpr auto bcryptCost = 8; + + auto serialBytes = serial.toUtf8(); + if (serialBytes.size() < bcryptSaltBytes) + serialBytes.prepend(QByteArray(bcryptSaltBytes - serialBytes.size(), '\0')); + else if (serialBytes.size() > bcryptSaltBytes) + serialBytes.truncate(bcryptSaltBytes); + + // This reproduces bcryptjs' "$2a$08$" + encodeBase64(serial.padStart(16, "\\0"), 16). + QByteArray salt(_SALT_LEN, '\0'); + bcrypt_gensalt('a', bcryptCost, reinterpret_cast(serialBytes.data()), salt.data()); + + const auto passwordSha256 = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha256).toHex(); + QByteArray fullHash(61, '\0'); + node_bcrypt(passwordSha256.constData(), passwordSha256.size(), salt.constData(), fullHash.data()); + + salt.truncate(qstrlen(salt.constData())); + fullHash.truncate(qstrlen(fullHash.constData())); + if (!fullHash.startsWith(salt)) + return {}; + + return QString::fromLatin1(fullHash.mid(salt.size(), 32)); +} } DeviceConnection::DeviceConnection(QObject *parent) : @@ -271,6 +299,32 @@ void DeviceConnection::messageReceived(const QVariant &variant) m_token2 = token2Variant.toString(); } + m_passwordHashAlgorithm = PasswordHashAlgorithm::Pbkdf2; + if (auto iter = map.find("hash"); iter != std::cend(map)) + { + const auto &hashVariant = *iter; + if (hashVariant.typeId() != QMetaType::QString) + { + emit logMessage(tr("Received authRequired with a non-string hash algorithm!")); + m_passwordHashAlgorithm = PasswordHashAlgorithm::Unsupported; + emit authImpossible(); + return; + } + + const auto hashAlgorithm = hashVariant.toString(); + if (hashAlgorithm == "pbkdf2") + m_passwordHashAlgorithm = PasswordHashAlgorithm::Pbkdf2; + else if (hashAlgorithm == "bcrypt") + m_passwordHashAlgorithm = PasswordHashAlgorithm::Bcrypt; + else + { + emit logMessage(tr("Received authRequired with unsupported hash algorithm %0!").arg(hashAlgorithm)); + m_passwordHashAlgorithm = PasswordHashAlgorithm::Unsupported; + emit authImpossible(); + return; + } + } + if (m_password.isEmpty()) emit authRequired(); else @@ -438,15 +492,30 @@ void DeviceConnection::sendMessage(const QVariantMap &variant) void DeviceConnection::sendAuth(const QString &password) { - const QString data = password; - const QString salt = m_serial; - constexpr auto iterations = 100000; - constexpr auto keylen = 64; - - m_password = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha512, data.toUtf8(), salt.toUtf8(), iterations, keylen).toBase64(); - - if (m_password.size() > 32) - m_password.resize(32); + switch (m_passwordHashAlgorithm) + { + case PasswordHashAlgorithm::Pbkdf2: + { + constexpr auto iterations = 100000; + constexpr auto keylen = 64; + m_password = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha512, password.toUtf8(), m_serial.toUtf8(), iterations, keylen).toBase64(); + if (m_password.size() > 32) + m_password.resize(32); + break; + } + case PasswordHashAlgorithm::Bcrypt: + m_password = deriveBcryptPassword(password, m_serial); + if (m_password.isEmpty()) + { + emit logMessage(tr("Could not derive bcrypt authentication hash!")); + emit authImpossible(); + return; + } + break; + case PasswordHashAlgorithm::Unsupported: + emit authImpossible(); + return; + } if (m_websocket.state() != QAbstractSocket::ConnectedState) return; diff --git a/evcharger-app/deviceconnection.h b/evcharger-app/deviceconnection.h index 56b4187..aeb71dd 100644 --- a/evcharger-app/deviceconnection.h +++ b/evcharger-app/deviceconnection.h @@ -81,6 +81,13 @@ private slots: void handshakeInterruptedOnError(const QSslError &error); private: + enum class PasswordHashAlgorithm + { + Pbkdf2, + Bcrypt, + Unsupported + }; + void sendAuthInternal(); void authResponse(const QString &type, const QVariantMap &message); @@ -100,6 +107,7 @@ private: QString m_token1; QString m_token2; + PasswordHashAlgorithm m_passwordHashAlgorithm{PasswordHashAlgorithm::Pbkdf2}; QString m_password;