allow login using hashed passwords (#4464)

* Support getting a user's password salt via initial websocket connection (added to Event_ServerIdentification)

* Nonsense stuff to figure out later

* move passwordhasher to correct location

* protobuf changes

* add ext to protobuf

* implement request password salt server side

* add supportspasswordhash to server identification

* check backwards compatibility

* reset some changes to master

* implement get password salt client side

* implement checking hashed passwords on server login

* check for registration requirement on getting password salt

* properly check password salt response and show errors

* remove unused property

* add password salt to list of response types

Co-authored-by: ZeldaZach <zahalpern+github@gmail.com>
This commit is contained in:
ebbit1q 2021-11-10 02:00:41 +01:00 committed by GitHub
parent b0845837c2
commit 45d86e7ab7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 193 additions and 26 deletions

View file

@ -40,6 +40,7 @@ enum ClientStatus
StatusRequestingForgotPassword,
StatusSubmitForgotPasswordReset,
StatusSubmitForgotPasswordChallenge,
StatusGettingPasswordSalt,
};
class AbstractClient : public QObject

View file

@ -42,7 +42,8 @@ AuthenticationResult LocalServer_DatabaseInterface::checkUserPassword(Server_Pro
const QString & /* password */,
const QString & /* clientId */,
QString & /* reasonStr */,
int & /* secondsLeft */)
int & /* banSecondsLeft */,
bool /* passwordNeedsHash */)
{
return UnknownUser;
}

View file

@ -32,7 +32,8 @@ public:
const QString &password,
const QString &clientId,
QString &reasonStr,
int &secondsLeft);
int &secondsLeft,
bool passwordNeedsHash);
int getNextGameId()
{
return localServer->getNextLocalGameId();

View file

@ -1,11 +1,12 @@
#include "remoteclient.h"
#include "main.h"
#include "pb/commands.pb.h"
#include "passwordhasher.h"
#include "pb/event_server_identification.pb.h"
#include "pb/response_activate.pb.h"
#include "pb/response_forgotpasswordrequest.pb.h"
#include "pb/response_login.pb.h"
#include "pb/response_password_salt.pb.h"
#include "pb/response_register.pb.h"
#include "pb/server_message.pb.h"
#include "pb/session_commands.pb.h"
@ -175,15 +176,30 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
return;
}
if (!password.isEmpty() && event.server_options() & Event_ServerIdentification::SupportsPasswordHash) {
// TODO store and log in using stored hashed password
doRequestPasswordSalt(); // log in using password salt
} else {
// TODO add setting for client to reject unhashed logins
doLogin();
}
}
void RemoteClient::doLogin()
void RemoteClient::doRequestPasswordSalt()
{
setStatus(StatusGettingPasswordSalt);
Command_RequestPasswordSalt cmdRqSalt;
cmdRqSalt.set_user_name(userName.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdRqSalt);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(passwordSaltResponse(Response)));
sendCommand(pend);
}
Command_Login RemoteClient::generateCommandLogin()
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin;
cmdLogin.set_user_name(userName.toStdString());
cmdLogin.set_password(password.toStdString());
cmdLogin.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdLogin.set_clientver(VERSION_STRING);
@ -192,6 +208,29 @@ void RemoteClient::doLogin()
for (i = clientFeatures.begin(); i != clientFeatures.end(); ++i)
cmdLogin.add_clientfeatures(i.key().toStdString().c_str());
}
return cmdLogin;
}
void RemoteClient::doLogin()
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
cmdLogin.set_password(password.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
}
void RemoteClient::doLogin(const QString &passwordSalt)
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
const auto hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdLogin.set_hashed_password(hashedPassword.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
@ -202,6 +241,22 @@ void RemoteClient::processConnectionClosedEvent(const Event_ConnectionClosed & /
doDisconnectFromServer();
}
void RemoteClient::passwordSaltResponse(const Response &response)
{
if (response.response_code() == Response::RespOk) {
const Response_PasswordSalt &resp = response.GetExtension(Response_PasswordSalt::ext);
QString salt = QString::fromStdString(resp.password_salt());
if (salt.isEmpty()) { // the server does not recognize the user but allows them to enter unregistered
password = ""; // the password will not be used
doLogin();
} else {
doLogin(salt);
}
} else if (response.response_code() != Response::RespNotConnected) {
emit loginError(response.response_code(), {}, 0, {});
}
}
void RemoteClient::loginResponse(const Response &response)
{
const Response_Login &resp = response.GetExtension(Response_Login::ext);

View file

@ -2,6 +2,7 @@
#define REMOTECLIENT_H
#include "abstractclient.h"
#include "pb/commands.pb.h"
#include <QTcpSocket>
#include <QWebSocket>
@ -55,6 +56,7 @@ private slots:
void ping();
void processServerIdentificationEvent(const Event_ServerIdentification &event);
void processConnectionClosedEvent(const Event_ConnectionClosed &event);
void passwordSaltResponse(const Response &response);
void loginResponse(const Response &response);
void registerResponse(const Response &response);
void activateResponse(const Response &response);
@ -68,7 +70,10 @@ private slots:
int _gender,
const QString &_country,
const QString &_realname);
void doRequestPasswordSalt();
void doLogin();
void doLogin(const QString &passwordSalt);
Command_Login generateCommandLogin();
void doDisconnectFromServer();
void doActivateToServer(const QString &_token);
void doRequestForgotPasswordToServer(const QString &hostname, unsigned int port, const QString &_userName);

View file

@ -6,8 +6,10 @@ add_subdirectory(pb)
SET(common_SOURCES
decklist.cpp
expression.cpp
featureset.cpp
get_pb_extension.cpp
passwordhasher.cpp
rng_abstract.cpp
rng_sfmt.cpp
server.cpp
@ -17,8 +19,8 @@ SET(common_SOURCES
server_card.cpp
server_cardzone.cpp
server_counter.cpp
server_game.cpp
server_database_interface.cpp
server_game.cpp
server_player.cpp
server_protocolhandler.cpp
server_remoteuserinterface.cpp
@ -26,7 +28,6 @@ SET(common_SOURCES
server_room.cpp
serverinfo_user_container.cpp
sfmt/SFMT.c
expression.cpp
)
set(ORACLE_LIBS)

View file

@ -15,6 +15,7 @@ QMap<QString, bool> FeatureSet::getDefaultFeatureList()
void FeatureSet::initalizeFeatureList(QMap<QString, bool> &featureList)
{
// default features [name], [is required to connect]
featureList.insert("client_id", false);
featureList.insert("client_ver", false);
featureList.insert("feature_set", false);
@ -25,6 +26,7 @@ void FeatureSet::initalizeFeatureList(QMap<QString, bool> &featureList)
featureList.insert("idle_client", false);
featureList.insert("forgot_password", false);
featureList.insert("websocket", false);
featureList.insert("hashed_password_login", false);
// These are temp to force users onto a newer client
featureList.insert("2.7.0_min_version", false);
featureList.insert("2.8.0_min_version", false);

View file

@ -115,6 +115,7 @@ SET(PROTO_FILES
moderator_commands.proto
move_card_to_zone.proto
response_activate.proto
response_adjust_mod.proto
response_ban_history.proto
response_deck_download.proto
response_deck_list.proto
@ -126,13 +127,13 @@ SET(PROTO_FILES
response_join_room.proto
response_list_users.proto
response_login.proto
response_password_salt.proto
response_register.proto
response_replay_download.proto
response_replay_list.proto
response_adjust_mod.proto
response_viewlog_history.proto
response_warn_history.proto
response_warn_list.proto
response_viewlog_history.proto
response.proto
room_commands.proto
room_event.proto

View file

@ -5,7 +5,12 @@ message Event_ServerIdentification {
extend SessionEvent {
optional Event_ServerIdentification ext = 500;
}
enum ServerOptions {
NoOptions = 0;
SupportsPasswordHash = 1;
}
optional string server_name = 1;
optional string server_version = 2;
optional uint32 protocol_version = 3;
optional ServerOptions server_options = 4 [default = NoOptions];
}

View file

@ -61,6 +61,7 @@ message Response {
WARN_LIST = 1014;
VIEW_LOG = 1015;
FORGOT_PASSWORD_REQUEST = 1016;
PASSWORD_SALT = 1017;
REPLAY_LIST = 1100;
REPLAY_DOWNLOAD = 1101;
}

View file

@ -0,0 +1,9 @@
syntax = "proto2";
import "response.proto";
message Response_PasswordSalt {
extend Response {
optional Response_PasswordSalt ext = 1017;
}
optional string password_salt = 1;
}

View file

@ -27,6 +27,7 @@ message SessionCommand {
FORGOT_PASSWORD_REQUEST = 1021;
FORGOT_PASSWORD_RESET = 1022;
FORGOT_PASSWORD_CHALLENGE = 1023;
REQUEST_PASSWORD_SALT = 1024;
REPLAY_LIST = 1100;
REPLAY_DOWNLOAD = 1101;
REPLAY_MODIFY_MATCH = 1102;
@ -50,6 +51,7 @@ message Command_Login {
optional string clientid = 3;
optional string clientver = 4;
repeated string clientfeatures = 5;
optional string hashed_password = 6;
}
message Command_Message {
@ -191,3 +193,10 @@ message Command_ForgotPasswordChallenge {
optional string clientid = 2;
optional string email = 3;
}
message Command_RequestPasswordSalt {
extend SessionCommand {
optional Command_RequestPasswordSalt ext = 1024;
}
required string user_name = 1;
}

View file

@ -79,6 +79,7 @@ Server_DatabaseInterface *Server::getDatabaseInterface() const
AuthenticationResult Server::loginUser(Server_ProtocolHandler *session,
QString &name,
const QString &password,
bool passwordNeedsHash,
QString &reasonStr,
int &secondsLeft,
QString &clientid,
@ -99,8 +100,8 @@ AuthenticationResult Server::loginUser(Server_ProtocolHandler *session,
Server_DatabaseInterface *databaseInterface = getDatabaseInterface();
AuthenticationResult authState =
databaseInterface->checkUserPassword(session, name, password, clientid, reasonStr, secondsLeft);
AuthenticationResult authState = databaseInterface->checkUserPassword(session, name, password, clientid, reasonStr,
secondsLeft, passwordNeedsHash);
if (authState == NotLoggedIn || authState == UserIsBanned || authState == UsernameInvalid ||
authState == UserIsInactive)
return authState;

View file

@ -62,6 +62,7 @@ public:
AuthenticationResult loginUser(Server_ProtocolHandler *session,
QString &name,
const QString &password,
bool passwordNeedsHash,
QString &reason,
int &secondsLeft,
QString &clientid,

View file

@ -18,7 +18,8 @@ public:
const QString &password,
const QString &clientId,
QString &reasonStr,
int &secondsLeft) = 0;
int &secondsLeft,
bool passwordNeedsHash) = 0;
virtual bool checkUserIsBanned(const QString & /* ipAddress */,
const QString & /* userName */,
const QString & /* clientId */,
@ -35,6 +36,10 @@ public:
{
return false;
}
virtual QString getUserSalt(const QString & /* user */)
{
return {};
}
virtual QMap<QString, ServerInfo_User> getBuddyList(const QString & /* name */)
{
return QMap<QString, ServerInfo_User>();

View file

@ -440,21 +440,32 @@ Response::ResponseCode Server_ProtocolHandler::cmdPing(const Command_Ping & /*cm
Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd, ResponseContainer &rc)
{
QString userName = QString::fromStdString(cmd.user_name()).simplified();
QString clientId = QString::fromStdString(cmd.clientid()).simplified();
QString clientVersion = QString::fromStdString(cmd.clientver()).simplified();
if (userInfo != 0)
QString password;
bool needsHash = false;
if (cmd.has_password()) {
password = QString::fromStdString(cmd.password());
needsHash = true;
} else if (cmd.has_hashed_password()) {
password = QString::fromStdString(cmd.hashed_password());
} else {
return Response::RespContextError;
}
if (userInfo != 0) {
return Response::RespContextError;
}
// check client feature set against server feature set
FeatureSet features;
QMap<QString, bool> receivedClientFeatures;
QMap<QString, bool> missingClientFeatures;
for (int i = 0; i < cmd.clientfeatures().size(); ++i)
for (int i = 0; i < cmd.clientfeatures().size(); ++i) {
receivedClientFeatures.insert(QString::fromStdString(cmd.clientfeatures(i)).simplified(), false);
}
missingClientFeatures =
features.identifyMissingFeatures(receivedClientFeatures, server->getServerRequiredFeatureList());
@ -464,8 +475,9 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
Response_Login *re = new Response_Login;
re->set_denied_reason_str("Client upgrade required");
QMap<QString, bool>::iterator i;
for (i = missingClientFeatures.begin(); i != missingClientFeatures.end(); ++i)
for (i = missingClientFeatures.begin(); i != missingClientFeatures.end(); ++i) {
re->add_missing_features(i.key().toStdString().c_str());
}
rc.setResponseExtension(re);
return Response::RespClientUpdateRequired;
}
@ -474,8 +486,8 @@ Response::ResponseCode Server_ProtocolHandler::cmdLogin(const Command_Login &cmd
QString reasonStr;
int banSecondsLeft = 0;
QString connectionType = getConnectionType();
AuthenticationResult res = server->loginUser(this, userName, QString::fromStdString(cmd.password()), reasonStr,
banSecondsLeft, clientId, clientVersion, connectionType);
AuthenticationResult res = server->loginUser(this, userName, password, needsHash, reasonStr, banSecondsLeft,
clientId, clientVersion, connectionType);
switch (res) {
case UserIsBanned: {
Response_Login *re = new Response_Login;

View file

@ -6,7 +6,6 @@ PROJECT(Servatrice VERSION "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${
SET(servatrice_SOURCES
src/main.cpp
src/passwordhasher.cpp
src/servatrice.cpp
src/servatrice_connection_pool.cpp
src/servatrice_database_interface.cpp

View file

@ -287,7 +287,8 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
const QString &password,
const QString &clientId,
QString &reasonStr,
int &banSecondsLeft)
int &banSecondsLeft,
bool passwordNeedsHash)
{
switch (server->getAuthenticationMethod()) {
case Servatrice::AuthenticationNone:
@ -324,7 +325,13 @@ AuthenticationResult Servatrice_DatabaseInterface::checkUserPassword(Server_Prot
qDebug("Login denied: user not active");
return UserIsInactive;
}
if (correctPassword == PasswordHasher::computeHash(password, correctPassword.left(16))) {
QString hashedPassword;
if (passwordNeedsHash) {
hashedPassword = PasswordHasher::computeHash(password, correctPassword.left(16));
} else {
hashedPassword = password;
}
if (correctPassword == hashedPassword) {
qDebug("Login accepted: password right");
return PasswordRight;
} else {
@ -490,6 +497,28 @@ bool Servatrice_DatabaseInterface::userExists(const QString &user)
return false;
}
QString Servatrice_DatabaseInterface::getUserSalt(const QString &user)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
checkSql();
QSqlQuery *query =
prepareQuery("SELECT SUBSTRING(password_sha512, 1, 16) FROM {prefix}_users WHERE name = :name");
query->bindValue(":name", user);
if (!execSqlQuery(query)) {
return {};
}
if (!query->next()) {
return {};
}
return query->value(0).toString();
}
return {};
}
int Servatrice_DatabaseInterface::getUserIdInDB(const QString &name)
{
if (server->getAuthenticationMethod() == Servatrice::AuthenticationSql) {

View file

@ -35,7 +35,8 @@ protected:
const QString &password,
const QString &clientId,
QString &reasonStr,
int &secondsLeft);
int &banSecondsLeft,
bool passwordNeedsHash);
public slots:
void initDatabase(const QSqlDatabase &_sqlDatabase);
@ -59,6 +60,7 @@ public:
bool activeUserExists(const QString &user);
bool userExists(const QString &user);
QString getUserSalt(const QString &user);
int getUserIdInDB(const QString &name);
QMap<QString, ServerInfo_User> getBuddyList(const QString &name);
QMap<QString, ServerInfo_User> getIgnoreList(const QString &name);

View file

@ -45,6 +45,7 @@
#include "pb/response_deck_list.pb.h"
#include "pb/response_deck_upload.pb.h"
#include "pb/response_forgotpasswordrequest.pb.h"
#include "pb/response_password_salt.pb.h"
#include "pb/response_register.pb.h"
#include "pb/response_replay_download.pb.h"
#include "pb/response_replay_list.pb.h"
@ -95,6 +96,9 @@ bool AbstractServerSocketInterface::initSession()
identEvent.set_server_name(servatrice->getServerName().toStdString());
identEvent.set_server_version(VERSION_STRING);
identEvent.set_protocol_version(protocolVersion);
if (servatrice->getAuthenticationMethod() == Servatrice::AuthenticationSql) {
identEvent.set_server_options(Event_ServerIdentification::SupportsPasswordHash);
}
SessionEvent *identSe = prepareSessionEvent(identEvent);
sendProtocolItem(*identSe);
delete identSe;
@ -193,6 +197,9 @@ Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionComm
return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc);
case SessionCommand::ACCOUNT_PASSWORD:
return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc);
case SessionCommand::REQUEST_PASSWORD_SALT:
return cmdRequestPasswordSalt(cmd.GetExtension(Command_RequestPasswordSalt::ext), rc);
break;
default:
return Response::RespFunctionNotAllowed;
}
@ -1480,6 +1487,25 @@ AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPa
return continuePasswordRequest(userName, clientId, rc, true);
}
Response::ResponseCode AbstractServerSocketInterface::cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd,
ResponseContainer &rc)
{
const QString userName = QString::fromStdString(cmd.user_name());
QString passwordSalt = sqlInterface->getUserSalt(userName);
if (passwordSalt.isEmpty()) {
if (server->getRegOnlyServerEnabled()) {
return Response::RespRegistrationRequired;
} else {
// user does not exist but is allowed to log in unregistered without password
return Response::RespOk;
}
}
auto *re = new Response_PasswordSalt;
re->set_password_salt(passwordSalt.toStdString());
rc.setResponseExtension(re);
return Response::RespOk;
}
// ADMIN FUNCTIONS.
// Permission is checked by the calling function.

View file

@ -116,6 +116,7 @@ private:
Response::ResponseCode cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd, ResponseContainer &rc);
Response::ResponseCode cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd,
ResponseContainer &rc);
Response::ResponseCode cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, ResponseContainer &rc);
Response::ResponseCode processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc);
Response::ResponseCode
processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc);
@ -198,7 +199,7 @@ class WebsocketServerSocketInterface : public AbstractServerSocketInterface
public:
WebsocketServerSocketInterface(Servatrice *_server,
Servatrice_DatabaseInterface *_databaseInterface,
QObject *parent = 0);
QObject *parent = nullptr);
~WebsocketServerSocketInterface();
QHostAddress getPeerAddress() const