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

@ -1,44 +0,0 @@
#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);
}

View file

@ -1,15 +0,0 @@
#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

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