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

@ -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);

44
common/passwordhasher.cpp Normal file
View file

@ -0,0 +1,44 @@
#include "passwordhasher.h"
#include "rng_sfmt.h"
#include <QCryptographicHash>
void PasswordHasher::initialize()
{
// dummy
}
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
{
QCryptographicHash::Algorithm algo = QCryptographicHash::Sha512;
const int rounds = 1000;
QByteArray hash = (salt + password).toUtf8();
for (int i = 0; i < rounds; ++i) {
hash = QCryptographicHash::hash(hash, algo);
}
QString hashedPass = salt + QString(hash.toBase64());
return hashedPass;
}
QString PasswordHasher::generateRandomSalt(const int len)
{
static const char alphanum[] = "0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
QString ret;
int size = sizeof(alphanum) - 1;
for (int i = 0; i < len; ++i) {
ret.append(alphanum[rng->rand(0, size)]);
}
return ret;
}
QString PasswordHasher::generateActivationToken()
{
return QCryptographicHash::hash(generateRandomSalt().toUtf8(), QCryptographicHash::Md5).toBase64().left(16);
}

15
common/passwordhasher.h Normal file
View file

@ -0,0 +1,15 @@
#ifndef PASSWORDHASHER_H
#define PASSWORDHASHER_H
#include <QObject>
class PasswordHasher
{
public:
static void initialize();
static QString computeHash(const QString &password, const QString &salt);
static QString generateRandomSalt(const int len = 16);
static QString generateActivationToken();
};
#endif

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;