Implement bcrypt password support
Build Linux and Android / build (push) Successful in 3m25s

This commit is contained in:
feedc0de committed 2026-08-13 14:02:31 +02:00
1 parent 7975e7c36f
commit 2d95b705cd
6 files changed
+108 -9

No files matched your search

+3
View File
@@ -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
Vendored Submodule
+1
Submodule 3rdparty/Bcrypt.cpp added at 0d18b6a99e.
+13
View File
@@ -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)
+5
View File
@@ -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}
+78 -9
View File
@@ -14,6 +14,7 @@
#include <utility>
#include "msgpack.h"
#include "node_blf.h"
using namespace std::chrono_literals;
@@ -23,6 +24,33 @@ auto enumToString(const QEnum value)
{
return QMetaEnum::fromType<QEnum>().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<u_int8_t *>(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;
+8
View File
@@ -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;