Revert "[Security] Add challenge-response auth with scrypt verifiers and stop storing plaintext passwords"

This reverts commit 088e932882.


Took 6 minutes
This commit is contained in:
Lukas Brübach 2026-08-25 20:52:25 +02:00
parent 088e932882
commit 47df709f37
33 changed files with 23 additions and 675 deletions

View file

@ -21,8 +21,7 @@
#include <libcockatrice/protocol/pending_command.h>
AbstractClient::AbstractClient(QObject *parent)
: QObject(parent), nextCmdId(0), status(StatusDisconnected), serverSupportsPasswordHash(false),
serverSupportsChallengeResponse(false)
: QObject(parent), nextCmdId(0), status(StatusDisconnected), serverSupportsPasswordHash(false)
{
qRegisterMetaType<QVariant>("QVariant");
qRegisterMetaType<CommandContainer>("CommandContainer");

View file

@ -94,7 +94,6 @@ protected:
QMap<int, PendingCommand *> pendingCommands;
QString userName, password, email, country, realName, token;
bool serverSupportsPasswordHash;
bool serverSupportsChallengeResponse;
void setStatus(ClientStatus _status);
int getNewCmdId()
{
@ -118,10 +117,6 @@ public:
{
return serverSupportsPasswordHash;
}
bool getServerSupportsChallengeResponse() const
{
return serverSupportsChallengeResponse;
}
const QString &getUserName() const
{
return userName;

View file

@ -27,8 +27,7 @@ static const unsigned int protocolVersion = 14;
RemoteClient::RemoteClient(QObject *parent, INetworkSettingsProvider *_networkSettingsProvider)
: AbstractClient(parent), networkSettingsProvider(_networkSettingsProvider), timeRunning(0), lastDataReceived(0),
messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword(),
passwordNeedsMigration(false)
messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword()
{
clearNewClientFeatures();
@ -115,8 +114,6 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
return;
}
serverSupportsPasswordHash = event.server_options() & Event_ServerIdentification::SupportsPasswordHash;
serverSupportsChallengeResponse =
event.server_options() & Event_ServerIdentification::SupportsChallengeResponseAuth;
if (getStatus() == StatusRequestingForgotPassword) {
Command_ForgotPasswordRequest cmdForgotPasswordRequest;
@ -133,10 +130,7 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
cmdForgotPasswordReset.set_user_name(userName.toStdString());
cmdForgotPasswordReset.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdForgotPasswordReset.set_token(token.toStdString());
if (!password.isEmpty() && serverSupportsChallengeResponse) {
hashedPassword = PasswordHasher::generatePasswordVerifier(password);
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
} else if (!password.isEmpty() && serverSupportsPasswordHash) {
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
@ -164,10 +158,7 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
if (getStatus() == StatusRegistering) {
Command_Register cmdRegister;
cmdRegister.set_user_name(userName.toStdString());
if (!password.isEmpty() && serverSupportsChallengeResponse) {
hashedPassword = PasswordHasher::generatePasswordVerifier(password);
cmdRegister.set_hashed_password(hashedPassword.toStdString());
} else if (!password.isEmpty() && serverSupportsPasswordHash) {
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdRegister.set_hashed_password(hashedPassword.toStdString());
@ -232,9 +223,7 @@ Command_Login RemoteClient::generateCommandLogin()
void RemoteClient::doLogin()
{
if ((!password.isEmpty() || !storedVerifier.isEmpty()) && serverSupportsChallengeResponse) {
doRequestPasswordSalt(); // ask salt + nonce to build the challenge response
} else if (!password.isEmpty() && serverSupportsPasswordHash) {
if (!password.isEmpty() && serverSupportsPasswordHash) {
//! \todo Store and log in using stored hashed password.
if (hashedPassword.isEmpty()) {
doRequestPasswordSalt(); // ask salt to create hashedPassword, then log in
@ -268,30 +257,6 @@ void RemoteClient::doHashedLogin()
sendCommand(pend);
}
void RemoteClient::doSubmitPasswordVerifier()
{
pendingVerifier = PasswordHasher::generatePasswordVerifier(password);
Command_SubmitPasswordVerifier cmdSubmitVerifier;
cmdSubmitVerifier.set_password_verifier(pendingVerifier.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdSubmitVerifier);
connect(pend, &PendingCommand::finished, this, &RemoteClient::submitPasswordVerifierResponse);
sendCommand(pend);
}
void RemoteClient::submitPasswordVerifierResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
qCDebug(RemoteClientLog) << "Password verifier migrated successfully";
if (!pendingVerifier.isEmpty()) {
emit sigPasswordVerifierReady(lastHostname, userName, pendingVerifier);
pendingVerifier.clear();
}
} else {
qCWarning(RemoteClientLog) << "Failed to migrate password verifier:" << response.response_code();
}
}
void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /*event*/)
{
doDisconnectFromServer();
@ -304,47 +269,7 @@ void RemoteClient::passwordSaltResponse(const Response &response)
auto passwordSalt = QString::fromStdString(resp.password_salt());
if (passwordSalt.isEmpty()) { // the server does not recognize the user but allows them to enter unregistered
password.clear(); // the password will not be used
storedVerifier.clear();
doLogin();
} else if (serverSupportsChallengeResponse && resp.has_nonce()) {
const QByteArray nonce = QByteArray::fromStdString(resp.nonce());
QByteArray key;
if (resp.needs_migration()) {
// The account still uses the legacy format; the legacy full hash
// is only derivable from the plaintext password.
if (password.isEmpty()) {
emit loginError(Response::RespClientUpdateRequired,
QStringLiteral("This account must be logged in with its password once."), 0, {});
return;
}
key = PasswordHasher::computeHash(password, passwordSalt).toUtf8();
} else if (!password.isEmpty()) {
const int n = resp.has_n() ? resp.n() : SCRYPT_N;
const int r = resp.has_r() ? resp.r() : SCRYPT_R;
const int p = resp.has_p() ? resp.p() : SCRYPT_P;
key = PasswordHasher::deriveKey(password, QByteArray::fromBase64(passwordSalt.toUtf8()), n, r, p);
derivedVerifier = QString("$scrypt$%1$%2$%3$%4$%5")
.arg(n)
.arg(r)
.arg(p)
.arg(passwordSalt)
.arg(QString(key.toBase64()));
} else if (!storedVerifier.isEmpty()) {
const PasswordVerifier verifier = PasswordHasher::parsePasswordVerifier(storedVerifier);
if (!verifier.isValid) {
emit loginError(Response::RespClientUpdateRequired, QStringLiteral("Stored verifier is invalid."),
0, {});
return;
}
key = verifier.verifier;
} else {
emit loginError(Response::RespLoginNeeded, {}, 0, {});
return;
}
passwordNeedsMigration = resp.needs_migration();
const QByteArray responseBytes = PasswordHasher::computeResponse(key, nonce);
hashedPassword = "$challenge$" + QString(nonce.toBase64()) + "$" + QString(responseBytes.toBase64());
doHashedLogin();
} else {
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
doHashedLogin();
@ -369,14 +294,6 @@ void RemoteClient::loginResponse(const Response &response)
setStatus(StatusLoggedIn);
emit userInfoChanged(resp.user_info());
if (passwordNeedsMigration) {
// The account still used the legacy password format; upgrade it to scrypt.
doSubmitPasswordVerifier();
} else if (!derivedVerifier.isEmpty()) {
emit sigPasswordVerifierReady(lastHostname, userName, derivedVerifier);
derivedVerifier.clear();
}
QList<ServerInfo_User> buddyList;
for (int i = resp.buddy_list_size() - 1; i >= 0; --i) {
buddyList.append(resp.buddy_list(i));
@ -632,8 +549,6 @@ void RemoteClient::doDisconnectFromServer()
websocket->close();
}
socket->close();
derivedVerifier.clear();
pendingVerifier.clear();
}
void RemoteClient::ping()
@ -796,9 +711,6 @@ void RemoteClient::submitForgotPasswordResetResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
emit sigForgotPasswordSuccess();
if (!hashedPassword.isEmpty()) {
emit sigPasswordVerifierReady(lastHostname, userName, hashedPassword);
}
} else {
emit sigForgotPasswordError();
}

View file

@ -54,9 +54,6 @@ signals:
unsigned int port,
const QString &_userName,
const QString &_email);
//! \brief Emitted once a scrypt verifier for the given account is known and
//! can be persisted instead of the plaintext password.
void sigPasswordVerifierReady(const QString &hostname, const QString &userName, const QString &verifier);
private slots:
void slotConnected();
void readData();
@ -83,8 +80,6 @@ private slots:
void doLogin();
void doHashedLogin();
Command_Login generateCommandLogin();
void doSubmitPasswordVerifier();
void submitPasswordVerifierResponse(const Response &response);
void doDisconnectFromServer();
void doActivateToServer(const QString &_token);
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);
@ -116,14 +111,6 @@ private:
QString lastHostname;
unsigned int lastPort;
QString hashedPassword;
bool passwordNeedsMigration;
//! \brief A previously stored "$scrypt$..." verifier used to authenticate
//! without the plaintext password.
QString storedVerifier;
//! \brief Verifier derived during the current login, persisted after success.
QString derivedVerifier;
//! \brief Verifier sent for migration, persisted once the server accepts it.
QString pendingVerifier;
QString getSrvClientID(const QString &_hostname);
bool newMissingFeatureFound(const QString &_serversMissingFeatures);
@ -162,12 +149,6 @@ public:
}
void
connectToServer(const QString &hostname, unsigned int port, const QString &_userName, const QString &_password);
//! \brief Provide a stored "$scrypt$..." verifier so the client can
//! authenticate without the plaintext password.
void setStoredVerifier(const QString &verifier)
{
storedVerifier = verifier;
}
void registerToServer(const QString &hostname,
unsigned int port,
const QString &_userName,

View file

@ -84,11 +84,6 @@ public:
{
return QMap<QString, bool>();
}
/** @brief True when only challenge-response logins are accepted (strict mode). */
virtual bool requiresChallengeResponseAuth() const
{
return false;
}
void addClient(Server_ProtocolHandler *player);
void removeClient(Server_ProtocolHandler *player);
QList<QString> getOnlineModeratorList() const;

View file

@ -40,14 +40,6 @@ public:
{
return {};
}
virtual QString getUserPasswordData(const QString & /* user */)
{
return {};
}
virtual bool submitPasswordVerifier(const QString & /* user */, const QString & /* passwordVerifier */)
{
return false;
}
virtual QMap<QString, ServerInfo_User> getBuddyList(const QString & /* name */)
{
return QMap<QString, ServerInfo_User>();

View file

@ -43,22 +43,6 @@ Server_ProtocolHandler::~Server_ProtocolHandler()
{
}
void Server_ProtocolHandler::setAuthNonce(const QByteArray &nonce)
{
authNonce = nonce;
authNonceCreated = QDateTime::currentDateTimeUtc();
}
bool Server_ProtocolHandler::isAuthNonceValid(const QByteArray &nonce) const
{
return !authNonce.isEmpty() && authNonce == nonce && authNonceCreated.secsTo(QDateTime::currentDateTimeUtc()) < 60;
}
void Server_ProtocolHandler::clearAuthNonce()
{
authNonce.clear();
}
// This function must only be called from the thread this object lives in.
// Except when the server is shutting down.
// The thread must not hold any server locks when calling this (e.g. clientsLock, roomsLock).
@ -501,16 +485,6 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
return Response::RespContextError;
}
// In strict mode only challenge-response logins are accepted.
if (server->requiresChallengeResponseAuth() &&
(!cmd.has_hashed_password() || cmd.hashed_password().rfind("$challenge$", 0) != 0)) {
auto *re = new Response_Login;
re->set_denied_reason_str("Client upgrade required");
re->add_missing_features("challenge_response_auth");
rc.setResponseExtension(re);
return Response::RespClientUpdateRequired;
}
// check client feature set against server feature set
FeatureSet features;
QMap<QString, bool> receivedClientFeatures;

View file

@ -4,8 +4,6 @@
#include "server.h"
#include "server_abstractuserinterface.h"
#include <QByteArray>
#include <QDateTime>
#include <QObject>
#include <libcockatrice/protocol/pb/response.pb.h>
#include <libcockatrice/protocol/pb/server_message.pb.h>
@ -57,8 +55,6 @@ protected:
bool acceptsUserListChanges;
bool acceptsRoomListChanges;
bool idleClientWarningSent;
QByteArray authNonce;
QDateTime authNonceCreated;
virtual void logDebugMessage(const QString & /* message */)
{
}
@ -128,13 +124,6 @@ public:
return databaseInterface;
}
/** @brief Store a fresh challenge nonce for the next challenge-response login attempt. */
void setAuthNonce(const QByteArray &nonce);
/** @brief True if nonce matches the pending one and was issued less than 60 seconds ago. */
bool isAuthNonceValid(const QByteArray &nonce) const;
/** @brief Invalidate the pending nonce (single-use). */
void clearAuthNonce();
int getLastCommandTime() const
{
return timeRunning - lastDataReceived;