Compare commits

...

6 commits

Author SHA1 Message Date
Lukas Brübach
5e94d0d3da
Lint.
Took 4 minutes

Took 36 seconds
2026-08-30 19:28:37 +02:00
Lukas Brübach
fd865c1ea9
[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
2026-08-30 19:28:37 +02:00
BruebachL
68e4fa054d
[UserList] Add invite button to hover popup (#7144)
Some checks failed
CodeQL / Analyze (cpp) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
Build Desktop / Configure (push) Has been cancelled
Build Docker / Servatrice (arm) (push) Has been cancelled
Build Docker / Servatrice (x86) (push) Has been cancelled
Build Desktop / Debian 13 (push) Has been cancelled
Build Desktop / Debian 12 (push) Has been cancelled
Build Desktop / Fedora 44 (push) Has been cancelled
Build Desktop / Fedora 43 (push) Has been cancelled
Build Desktop / Servatrice_Debian 12 (push) Has been cancelled
Build Desktop / Ubuntu 26.04 (push) Has been cancelled
Build Desktop / Ubuntu 24.04 (push) Has been cancelled
Build Desktop / Arch (push) Has been cancelled
Build Desktop / macOS 13 Intel (push) Has been cancelled
Build Desktop / macOS 14 (push) Has been cancelled
Build Desktop / macOS 15 (push) Has been cancelled
Build Desktop / macOS 26 Debug (push) Has been cancelled
Build Desktop / Windows 10 (push) Has been cancelled
Build Docker / Publish multi-platform Servatrice image (push) Has been cancelled
* [Client] Send game invites from the user context menu via a private message

The user context menu gains an "Invite to Game" submenu listing the
inviteable games in the room (the inviter's own games, honoring the
buddy-only setting). Picking one opens a private message to the target
user with a cockatrice://joingame link naming the game, so the target
gets a clickable invite instead of a raw URL. Multi-game rooms offer a
picker; a single inviteable game sends directly. Sending a message to
an offline user no longer swallows the draft — it reports that the
user is offline and keeps the typed text.

Took 30 seconds

Took 1 minute

* [Client] Open the invite dialog taller by default without enforcing a minimum size

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-29 21:35:28 +02:00
BruebachL
6f86c45ea8
[Refactor] Extract shared deck conversion prompt helper (#7107)
Move the deck-to-.cod conversion prompt logic (format check, saved
preference handling, overwrite confirmation, dialog) out of
DeckPreviewWidget into dlg_convert_deck_to_cod_format so the deck
editor can reuse it without duplicating it.

Took 4 minutes

Took 4 minutes

Took 1 minute

# Commit time for manual adjustment:
# Took 3 minutes

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-08-29 21:12:11 +02:00
dependabot[bot]
dade7ae78a
Bump actions/checkout from 6 to 7 (#7210)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-29 18:23:45 +02:00
dependabot[bot]
8f52223322
Bump actions/download-artifact from 7 to 8 (#7209)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 7 to 8.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-29 18:11:11 +02:00
29 changed files with 238 additions and 96 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -40,7 +40,7 @@ jobs:
steps:
- name: "Checkout repository"
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: "Initialize CodeQL"
uses: github/codeql-action/init@v4

View file

@ -127,7 +127,7 @@ jobs:
steps:
- name: "Download digests"
uses: actions/download-artifact@v7
uses: actions/download-artifact@v8
with:
path: ${{ runner.temp }}/digests
pattern: digest-*

View file

@ -239,11 +239,6 @@ if(WIN32)
find_package(OpenSSL REQUIRED)
if(OPENSSL_FOUND)
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()

View file

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

View file

@ -1,9 +1,17 @@
#include "dlg_convert_deck_to_cod_format.h"
#include "../../../client/settings/cache_settings.h"
#include "../../deck_loader/deck_loader.h"
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QLabel>
#include <QMessageBox>
#include <QVBoxLayout>
#include <libcockatrice/settings/visual_deck_storage_settings.h>
DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent)
{
@ -38,3 +46,71 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const
{
return dontAskAgainCheckbox->isChecked();
}
namespace
{
bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
} // namespace
bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent,
const QString &filePath,
const std::function<bool()> &convert)
{
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
return true;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(parent, filePath)) {
return false;
}
return convert();
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(parent);
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(parent, filePath)) {
return false;
}
if (!convert()) {
return false;
}
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
}

View file

@ -13,6 +13,9 @@
#include <QDialogButtonBox>
#include <QLabel>
#include <QVBoxLayout>
#include <functional>
class QWidget;
class DialogConvertDeckToCodFormat : public QDialog
{
@ -24,6 +27,21 @@ public:
[[nodiscard]] bool dontAskAgain() const;
/**
* @brief Checks whether the deck file at \a filePath can store tags.
*
* If the file is not a .cod deck, prompts the user for conversion to the
* Cockatrice format, honoring the saved "always convert / don't ask again"
* preference. On acceptance \a convert is called to perform the conversion.
*
* @param parent The widget to parent the prompt to.
* @param filePath The path of the deck file to check.
* @param convert Called to convert the deck once the user agrees.
* @return true if tags can be stored (no conversion needed, or the conversion
* was performed), false if the user declined to convert.
*/
static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function<bool()> &convert);
private:
QVBoxLayout *layout;
QLabel *label;

View file

@ -525,6 +525,13 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o
connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); });
add(games);
// ── Invite (only while the inviter has a joinable game for this user) ────
if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) {
auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme);
connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); });
add(invite);
}
// ── Buddy / ignore (registered users only) ────────────────────────────────
if (!isSelf && isReg) {
if (isBuddy) {

View file

@ -9,6 +9,7 @@
#include <QMap>
#include <QPixmap>
#include <QStandardItemModel>
#include <functional>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/serverinfo_game.pb.h>
@ -149,6 +150,17 @@ public:
/** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */
void refreshHeader();
/**
* Sets a predicate evaluated on every action-button rebuild. It receives
* the name of the user the popup currently shows; when it returns true an
* "Invite" button is shown. The popup itself never resolves the invite
* link, it just forwards the request.
*/
void setGameInviteAvailable(std::function<bool(const QString &userName)> available)
{
gameInviteAvailable = std::move(available);
}
signals:
void mouseEnteredPopup();
void mouseLeftPopup();
@ -159,6 +171,7 @@ signals:
// ── Action signals — connect to UserContextMenu::exec*() ──────────────────
void chatRequested(const QString &userName);
void inviteRequested(const QString &userName);
void detailsRequested(const QString &userName);
void showGamesRequested(const QString &userName);
void addBuddyRequested(const QString &userName);
@ -200,6 +213,7 @@ private:
QString currentUser;
ServerInfo_User currentUserInfo;
bool currentOnline = false;
std::function<bool(const QString &userName)> gameInviteAvailable;
UserInfoHeaderWidget *header;
QWidget *actionArea; ///< rebuilt per user

View file

@ -345,6 +345,11 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,
&cardArtProvider->cache(), &cardArtParamsMap,
window()); // parented to main window so it floats above siblings
// The invite availability is scoped to the room this list belongs to,
// and gated on the room's buddy-only setting for the hovered user.
userInfoPopup->setGameInviteAvailable(
[this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); });
userInfoPopup->hide();
userInfoPopup->setWindowOpacity(0.0);
userInfoPopup->installEventFilter(this);
@ -662,6 +667,8 @@ void UserListWidget::connectPopupSignals()
// Wire all action signals to UserContextMenu::exec*()
connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat);
connect(userInfoPopup, &UserInfoPopup::inviteRequested, this,
[this](const QString &userName) { userContextMenu->execInvite(userName); });
connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails);
connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames);
connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy);

View file

@ -22,6 +22,7 @@
#include <QTextEdit>
#include <QTreeWidgetItem>
#include <functional>
#include <libcockatrice/network/server/remote/user_level.h>
#include <libcockatrice/protocol/pb/moderator_commands.pb.h>
class QTreeWidget;

View file

@ -1091,7 +1091,8 @@ QList<GameInviteOption> TabSupervisor::getGameInviteLinksForRoom(int roomId) con
// The inviter may be in several games of the same room (hosting one and
// spectating another, for example). Return every game so the caller can
// let the user choose which one to invite to.
for (TabGame *tab : gameTabs) {
for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) {
TabGame *tab = it.value();
GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo();
if (metaInfo->proto().room_id() != roomId) {
continue;

View file

@ -10,8 +10,6 @@
#include "../visual_deck_storage_widget.h"
#include "deck_preview_deck_tags_display_widget.h"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QInputDialog>
#include <QLabel>
@ -499,21 +497,6 @@ void DeckPreviewWidget::actDeleteFile()
// The folder widget removes this preview once the row is gone.
}
static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
{
QFileInfo fileInfo(filePath);
QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod");
if (QFile::exists(newFileName)) {
QMessageBox::StandardButton reply =
QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"),
QObject::tr("A .cod version of this deck already exists. Overwrite it?"),
QMessageBox::Yes | QMessageBox::No);
return reply == QMessageBox::Yes;
}
return true; // Safe to proceed
}
/**
* Checks if the deck's file format supports tags.
* If not, then prompt the user for file conversion.
@ -521,45 +504,8 @@ static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath)
*/
bool DeckPreviewWidget::promptFileConversionIfRequired()
{
if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) {
return true;
}
// Retrieve saved preference if the prompt is disabled
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) {
if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) {
return false;
}
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] {
model->convertToCockatriceFormat(row());
return true;
}
// Show the dialog to the user
DialogConvertDeckToCodFormat conversionDialog(this);
if (conversionDialog.exec() != QDialog::Accepted) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(
!conversionDialog.dontAskAgain());
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false);
return false;
}
// Try to convert file
if (!confirmOverwriteIfExists(this, filePath)) {
return false;
}
model->convertToCockatriceFormat(row());
if (conversionDialog.dontAskAgain()) {
SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false);
SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true);
}
return true;
});
}

View file

@ -53,6 +53,7 @@
#include <libcockatrice/settings/interface_settings.h>
#include <libcockatrice/settings/network_settings.h>
#include <libcockatrice/settings/personal_settings.h>
#include <libcockatrice/utility/cryptoutil.h>
QTranslator *translator, *qtTranslator;
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;
soundEngine = new SoundEngine;

View file

@ -1,6 +1,5 @@
#include "rng_sfmt.h"
#include <QDateTime>
#include <algorithm>
#include <climits>
#include <stdexcept>
@ -11,10 +10,11 @@
#define UINT64_MAX (~(uint64_t)0)
#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)
sfmt_init_gen_rand(&sfmt, QDateTime::currentDateTime().toSecsSinceEpoch());
// initialize the random number generator with a 64bit seed, e.g. from a CSPRNG
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);
public:
explicit RNG_SFMT(QObject *parent = nullptr);
explicit RNG_SFMT(uint64_t seed, QObject *parent = nullptr);
unsigned int rand(int min, int max) override;
};

View file

@ -6,13 +6,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(UTILITY_SOURCES
libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp libcockatrice/utility/passwordhasher.cpp
libcockatrice/utility/server_rate_limiter.cpp libcockatrice/utility/warning_categories.cpp
libcockatrice/utility/cryptoutil.cpp libcockatrice/utility/expression.cpp libcockatrice/utility/levenshtein.cpp
libcockatrice/utility/passwordhasher.cpp libcockatrice/utility/server_rate_limiter.cpp
libcockatrice/utility/warning_categories.cpp
)
set(UTILITY_HEADERS
libcockatrice/utility/card_ref.h
libcockatrice/utility/color.h
libcockatrice/utility/cryptoutil.h
libcockatrice/utility/expression.h
libcockatrice/utility/levenshtein.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_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)

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 <QCryptographicHash>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/utility/cryptoutil.h>
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
{
@ -21,12 +21,28 @@ QString PasswordHasher::generateRandomSalt(const int len)
static const char alphanum[] = "0123456789"
"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;
int size = sizeof(alphanum) - 1;
ret.reserve(len);
QByteArray random = CryptoUtil::randomBytes(len * 2);
int bytesUsed = 0;
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;
@ -34,5 +50,5 @@ QString PasswordHasher::generateRandomSalt(const int len)
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 <iostream>
#include <libcockatrice/rng/rng_sfmt.h>
#include <libcockatrice/utility/cryptoutil.h>
#include <libcockatrice/utility/passwordhasher.h>
RNG_Abstract *rng;
@ -169,7 +170,7 @@ int main(int argc, char *argv[])
signalhandler = new SignalHandler();
rng = new RNG_SFMT;
rng = new RNG_SFMT(CryptoUtil::randomUInt64());
std::cerr << "Servatrice " << VERSION_STRING << " starting." << std::endl;
std::cerr << "-------------------------" << std::endl;

View file

@ -1,25 +1,9 @@
#include "gtest/gtest.h"
#include <libcockatrice/rng/rng_abstract.h>
#include <libcockatrice/rng/rng_sfmt.h>
#include <cstring>
#include <libcockatrice/utility/passwordhasher.h>
RNG_Abstract *rng;
namespace
{
class PasswordHashTest : public ::testing::Test
{
protected:
void SetUp() override
{
rng = new RNG_SFMT;
}
void TearDown() override
{
delete rng;
}
};
TEST(PasswordHashTest, RegressionTest)
{
@ -29,6 +13,29 @@ TEST(PasswordHashTest, RegressionTest)
QString hash = PasswordHasher::computeHash(password, salt);
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
int main(int argc, char **argv)