[Security] Use a CSPRNG for salts, tokens, and RNG seeding (#7192)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* [Security] Use a CSPRNG for salts, tokens, and RNG seeding

Password salts and activation tokens were generated with the global SFMT
RNG, which was seeded from a 32-bit timestamp, making registration
salts and activation tokens predictable. The game RNG used the same
timestamp seed across restarts.

Add CryptoUtil backed by OpenSSL RAND_bytes and use it for salt/token
generation and to seed RNG_SFMT with a 64-bit CSPRNG value in both the
client and server. Link libcockatrice_utility against OpenSSL::Crypto.

Took 30 seconds

Took 25 minutes

* Lint.

Took 4 minutes

Took 36 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-09-04 13:49:18 +02:00 committed by GitHub
parent 3ec62df3e7
commit d6fbfb32a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 109 additions and 37 deletions

View file

@ -8,6 +8,7 @@ RUN pacman --sync --refresh --sysupgrade --needed --noconfirm \
gtest \ gtest \
mariadb-libs \ mariadb-libs \
ninja \ ninja \
openssl \
protobuf \ protobuf \
qt6-base \ qt6-base \
qt6-declarative \ qt6-declarative \

View file

@ -15,6 +15,7 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
ninja-build \ ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \

View file

@ -16,6 +16,7 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
ninja-build \ ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \

View file

@ -7,6 +7,7 @@ RUN dnf install -y \
git \ git \
mariadb-devel \ mariadb-devel \
ninja-build \ ninja-build \
openssl-devel \
protobuf-devel \ protobuf-devel \
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \ qt6-qtimageformats \

View file

@ -7,6 +7,7 @@ RUN dnf install -y \
git \ git \
mariadb-devel \ mariadb-devel \
ninja-build \ ninja-build \
openssl-devel \
protobuf-devel \ protobuf-devel \
qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \ qt6-{qtdeclarative,qtshadertools,qttools,qtsvg,qtmultimedia,qtwebsockets}-devel \
qt6-qtimageformats \ qt6-qtimageformats \

View file

@ -12,6 +12,7 @@ RUN apt-get update && \
libmariadb-dev-compat \ libmariadb-dev-compat \
libprotobuf-dev \ libprotobuf-dev \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
ninja-build \ ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-tools-dev \ qt6-tools-dev \

View file

@ -15,6 +15,7 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
ninja-build \ ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \

View file

@ -16,6 +16,7 @@ RUN apt-get update && \
libprotobuf-dev \ libprotobuf-dev \
libqt6multimedia6 \ libqt6multimedia6 \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
ninja-build \ ninja-build \
protobuf-compiler \ protobuf-compiler \
qt6-image-formats-plugins \ qt6-image-formats-plugins \

View file

@ -245,11 +245,6 @@ if(WIN32)
find_package(OpenSSL REQUIRED) find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND) if(OPENSSL_FOUND)
include_directories(${OPENSSL_INCLUDE_DIRS}) include_directories(${OPENSSL_INCLUDE_DIRS})
else()
message(
WARNING
"Could not find OpenSSL runtime libraries. They are not required for compiling, but needs to be available at runtime."
)
endif() endif()
endif() endif()

View file

@ -14,6 +14,7 @@ RUN apt-get update \
libmariadb-dev-compat \ libmariadb-dev-compat \
libprotobuf-dev \ libprotobuf-dev \
libqt6sql6-mysql \ libqt6sql6-mysql \
libssl-dev \
qt6-websockets-dev \ qt6-websockets-dev \
protobuf-compiler \ protobuf-compiler \
qt6-tools-dev \ qt6-tools-dev \
@ -42,6 +43,7 @@ RUN apt-get update \
libprotobuf32t64 \ libprotobuf32t64 \
libqt6sql6-mysql \ libqt6sql6-mysql \
libqt6websockets6 \ libqt6websockets6 \
libssl3 \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*

View file

@ -53,6 +53,7 @@
#include <libcockatrice/settings/interface_settings.h> #include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/network_settings.h> #include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/personal_settings.h> #include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/cryptoutil.h>
QTranslator *translator, *qtTranslator; QTranslator *translator, *qtTranslator;
RNG_Abstract *rng; RNG_Abstract *rng;
@ -292,7 +293,7 @@ int main(int argc, char *argv[])
} }
} }
rng = new RNG_SFMT; rng = new RNG_SFMT(CryptoUtil::randomUInt64());
themeManager = new ThemeManager; themeManager = new ThemeManager;
soundEngine = new SoundEngine; soundEngine = new SoundEngine;

View file

@ -1,6 +1,5 @@
#include "rng_sfmt.h" #include "rng_sfmt.h"
#include <QDateTime>
#include <algorithm> #include <algorithm>
#include <climits> #include <climits>
#include <stdexcept> #include <stdexcept>
@ -11,10 +10,11 @@
#define UINT64_MAX (~(uint64_t)0) #define UINT64_MAX (~(uint64_t)0)
#endif #endif
RNG_SFMT::RNG_SFMT(QObject *parent) : RNG_Abstract(parent) RNG_SFMT::RNG_SFMT(uint64_t seed, QObject *parent) : RNG_Abstract(parent)
{ {
// initialize the random number generator with a 32bit integer seed (timestamp) // initialize the random number generator with a 64bit seed, e.g. from a CSPRNG
sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch()); uint32_t seedArray[2] = {static_cast<uint32_t>(seed), static_cast<uint32_t>(seed >> 32)};
sfmt_init_by_array(&sfmt, seedArray, 2);
} }
/** /**

View file

@ -36,7 +36,7 @@ private:
unsigned int cdf(unsigned int min, unsigned int max); unsigned int cdf(unsigned int min, unsigned int max);
public: public:
explicit RNG_SFMT(QObject *parent = nullptr); explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr);
unsigned int rand(int min, int max) override; unsigned int rand(int min, int max) override;
}; };

View file

@ -6,13 +6,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON) set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES set(UTILITY_SOURCES
libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp
libcockatrice/utility/warning_categories.cpp
) )
set(UTILITY_HEADERS set(UTILITY_HEADERS
libcockatrice/utility/card_ref.h libcockatrice/utility/card_ref.h
libcockatrice/utility/color.h libcockatrice/utility/color.h
libcockatrice/utility/cryptoutil.h
libcockatrice/utility/expression.h libcockatrice/utility/expression.h
libcockatrice/utility/levenshtein.h libcockatrice/utility/levenshtein.h
libcockatrice/utility/macros.h libcockatrice/utility/macros.h
@ -32,7 +34,9 @@ add_library(libcockatrice_utility STATIC ${UTILITY_SOURCES} ${UTILITY_HEADERS})
target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(libcockatrice_utility PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng ${QT_CORE_MODULE}) find_package(OpenSSL REQUIRED)
target_link_libraries(libcockatrice_utility PUBLIC libcockatrice_rng OpenSSL::Crypto ${QT_CORE_MODULE})
set(ORACLE_LIBS) set(ORACLE_LIBS)

View file

@ -0,0 +1,25 @@
#include "cryptoutil.h"
#include <openssl/rand.h>
namespace CryptoUtil
{
QByteArray randomBytes(int count)
{
QByteArray bytes(count, '\0');
if (RAND_bytes(reinterpret_cast<unsigned char *>(bytes.data()), count) != 1) {
// Randomness failure is fatal: never fall back to a predictable source.
qFatal("CryptoUtil::randomBytes: RAND_bytes failed");
}
return bytes;
}
quint64 randomUInt64()
{
quint64 value;
if (RAND_bytes(reinterpret_cast<unsigned char *>(&value), sizeof(value)) != 1) {
qFatal("CryptoUtil::randomUInt64: RAND_bytes failed");
}
return value;
}
} // namespace CryptoUtil

View file

@ -0,0 +1,13 @@
#ifndef CRYPTOUTIL_H
#define CRYPTOUTIL_H
#include <QByteArray>
#include <QtGlobal>
namespace CryptoUtil
{
QByteArray randomBytes(int count);
quint64 randomUInt64();
} // namespace CryptoUtil
#endif

View file

@ -1,7 +1,7 @@
#include "passwordhasher.h" #include "passwordhasher.h"
#include <QCryptographicHash> #include <QCryptographicHash>
#include <libcockatrice/rng/rng_sfmt.h> #include <libcockatrice/utility/cryptoutil.h>
QString PasswordHasher::computeHash(const QString &password, const QString &salt) QString PasswordHasher::computeHash(const QString &password, const QString &salt)
{ {
@ -21,12 +21,28 @@ QString PasswordHasher::generateRandomSalt(const int len)
static const char alphanum[] = "0123456789" static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"; "abcdefghijklmnopqrstuvwxyz";
const int size = sizeof(alphanum) - 1;
// Two bytes per character, corrected for modulo bias via rejection sampling.
const int bucketSize = 65536 / size;
const int limit = bucketSize * size;
QString ret; QString ret;
int size = sizeof(alphanum) - 1; ret.reserve(len);
QByteArray random = CryptoUtil::randomBytes(len * 2);
int bytesUsed = 0;
for (int i = 0; i < len; ++i) { for (int i = 0; i < len; ++i) {
ret.append(alphanum[rng->rand(0, size)]); unsigned int value;
do {
if (bytesUsed >= random.size()) {
random = CryptoUtil::randomBytes(len * 2);
bytesUsed = 0;
}
value = static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed))) << 8 |
static_cast<unsigned int>(static_cast<unsigned char>(random.at(bytesUsed + 1)));
bytesUsed += 2;
} while (value >= limit);
ret.append(alphanum[value / bucketSize]);
} }
return ret; return ret;
@ -34,5 +50,5 @@ QString PasswordHasher::generateRandomSalt(const int len)
QString PasswordHasher::generateActivationToken() QString PasswordHasher::generateActivationToken()
{ {
return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16); return QString(CryptoUtil::randomBytes(16).toBase64().left(16));
} }

View file

@ -33,6 +33,7 @@
#include <QtGlobal> #include <QtGlobal>
#include <iostream> #include <iostream>
#include <libcockatrice/rng/rng_sfmt.h> #include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/utility/cryptoutil.h>
#include <libcockatrice/utility/passwordhasher.h> #include <libcockatrice/utility/passwordhasher.h>
RNG_Abstract *rng; RNG_Abstract *rng;
@ -169,7 +170,7 @@ int main(int argc, char *argv[])
signalhandler = new SignalHandler(); signalhandler = new SignalHandler();
rng = new RNG_SFMT; rng = new RNG_SFMT(CryptoUtil::randomUInt64());
std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl; std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl;
std::cerr << "-------------------------" << std::endl; std::cerr << "-------------------------" << std::endl;

View file

@ -1,25 +1,9 @@
#include "gtest/gtest.h" #include "gtest/gtest.h"
#include <libcockatrice/rng/rng_abstract.h> #include <cstring>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/utility/passwordhasher.h> #include <libcockatrice/utility/passwordhasher.h>
RNG_Abstract *rng;
namespace namespace
{ {
class PasswordHashTest : public ::testing::Test
{
protected:
void SetUp() override
{
rng = new RNG_SFMT;
}
void TearDown() override
{
delete rng;
}
};
TEST(PasswordHashTest, RegressionTest) TEST(PasswordHashTest, RegressionTest)
{ {
@ -29,6 +13,29 @@ TEST(PasswordHashTest, RegressionTest)
QString hash = PasswordHasher::computeHash(password, salt); QString hash = PasswordHasher::computeHash(password, salt);
ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same"; ASSERT_EQ(hash, salt + expected) << "The computed hash value remains the same";
} }
TEST(PasswordHashTest, SaltUsesAlphanumericCharset)
{
static const char alphanum[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const QString salt = PasswordHasher::generateRandomSalt();
ASSERT_EQ(salt.size(), 16);
for (const QChar &c : salt) {
ASSERT_NE(strchr(alphanum, c.toLatin1()), nullptr);
}
}
TEST(PasswordHashTest, SaltsAreUnique)
{
const QString salt1 = PasswordHasher::generateRandomSalt();
const QString salt2 = PasswordHasher::generateRandomSalt();
ASSERT_NE(salt1, salt2);
}
TEST(PasswordHashTest, TokenHasExpectedLength)
{
const QString token = PasswordHasher::generateActivationToken();
ASSERT_EQ(token.size(), 16);
}
} // namespace } // namespace
int main(int argc, char **argv) int main(int argc, char **argv)