use hashed passwords in all commands (#4493)

* protocol changes

* server changes

* client changes for password reset and registration

* add hashed password to change password in client

* always use hashed password to log in

* add warning to client when using plain text password

* require real password for changing email on server

this is backwards compatible as users logged in with a real password on
older clients will not need this, only users logged in with a hashed
password

* implement password dialog when changing email

* require min password length

* use qstringlist to build query instead

* use clear instead of = ""

* add max to password dialog

* use proper const ness in abstractclient

* reject too long passwords instead of trimming
This commit is contained in:
ebbit1q 2022-01-17 02:32:30 +01:00 committed by GitHub
parent fcafcb340a
commit 2fc85e0c08
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 330 additions and 96 deletions

View file

@ -22,7 +22,8 @@
#include <google/protobuf/descriptor.h>
AbstractClient::AbstractClient(QObject *parent) : QObject(parent), nextCmdId(0), status(StatusDisconnected)
AbstractClient::AbstractClient(QObject *parent)
: QObject(parent), nextCmdId(0), status(StatusDisconnected), serverSupportsPasswordHash(false)
{
qRegisterMetaType<QVariant>("QVariant");
qRegisterMetaType<CommandContainer>("CommandContainer");

View file

@ -88,6 +88,7 @@ protected slots:
protected:
QMap<int, PendingCommand *> pendingCommands;
QString userName, password, email, country, realName, token;
bool serverSupportsPasswordHash;
void setStatus(ClientStatus _status);
int getNewCmdId()
{
@ -107,7 +108,11 @@ public:
void sendCommand(const CommandContainer &cont);
void sendCommand(PendingCommand *pend);
const QString getUserName()
bool getServerSupportsPasswordHash() const
{
return serverSupportsPasswordHash;
}
const QString &getUserName() const
{
return userName;
}

View file

@ -59,7 +59,11 @@ DlgEditPassword::DlgEditPassword(QWidget *parent) : QDialog(parent)
void DlgEditPassword::actOk()
{
if (newPasswordEdit->text() != newPasswordEdit2->text()) {
// TODO this stuff should be using qvalidators
if (newPasswordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
return;
} else if (newPasswordEdit->text() != newPasswordEdit2->text()) {
QMessageBox::warning(this, tr("Error"), tr("The new passwords don't match."));
return;
}

View file

@ -123,7 +123,11 @@ void DlgForgotPasswordReset::actOk()
return;
}
if (newpasswordEdit->text() != newpasswordverifyEdit->text()) {
// TODO this stuff should be using qvalidators
if (newpasswordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Error"), tr("Your password is too short."));
return;
} else if (newpasswordEdit->text() != newpasswordverifyEdit->text()) {
QMessageBox::critical(this, tr("Reset Password Error"), tr("The passwords do not match."));
return;
}

View file

@ -356,7 +356,11 @@ DlgRegister::DlgRegister(QWidget *parent) : QDialog(parent)
void DlgRegister::actOk()
{
if (passwordEdit->text() != passwordConfirmationEdit->text()) {
// TODO this stuff should be using qvalidators
if (passwordEdit->text().length() < 8) {
QMessageBox::critical(this, tr("Registration Warning"), tr("Your password is too short."));
return;
} else if (passwordEdit->text() != passwordConfirmationEdit->text()) {
QMessageBox::critical(this, tr("Registration Warning"), tr("Your passwords do not match, please try again."));
return;
} else if (emailConfirmationEdit->text() != emailEdit->text()) {

View file

@ -27,7 +27,7 @@ static const unsigned int protocolVersion = 14;
RemoteClient::RemoteClient(QObject *parent)
: AbstractClient(parent), timeRunning(0), lastDataReceived(0), messageInProgress(false), handshakeStarted(false),
usingWebSocket(false), messageLength(0)
usingWebSocket(false), messageLength(0), hashedPassword()
{
clearNewClientFeatures();
@ -110,6 +110,7 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
setStatus(StatusDisconnecting);
return;
}
serverSupportsPasswordHash = event.server_options() & Event_ServerIdentification::SupportsPasswordHash;
if (getStatus() == StatusRequestingForgotPassword) {
Command_ForgotPasswordRequest cmdForgotPasswordRequest;
@ -127,7 +128,14 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
cmdForgotPasswordReset.set_user_name(userName.toStdString());
cmdForgotPasswordReset.set_clientid(getSrvClientID(lastHostname).toStdString());
cmdForgotPasswordReset.set_token(token.toStdString());
cmdForgotPasswordReset.set_new_password(password.toStdString());
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
} else if (!password.isEmpty()) {
qWarning() << "using plain text password to reset password";
cmdForgotPasswordReset.set_new_password(password.toStdString());
}
PendingCommand *pend = prepareSessionCommand(cmdForgotPasswordReset);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
SLOT(submitForgotPasswordResetResponse(Response)));
@ -150,7 +158,14 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
if (getStatus() == StatusRegistering) {
Command_Register cmdRegister;
cmdRegister.set_user_name(userName.toStdString());
cmdRegister.set_password(password.toStdString());
if (!password.isEmpty() && serverSupportsPasswordHash) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdRegister.set_hashed_password(hashedPassword.toStdString());
} else if (!password.isEmpty()) {
qWarning() << "using plain text password to register";
cmdRegister.set_password(password.toStdString());
}
cmdRegister.set_email(email.toStdString());
cmdRegister.set_country(country.toStdString());
cmdRegister.set_real_name(realName.toStdString());
@ -175,13 +190,7 @@ 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();
}
doLogin();
}
void RemoteClient::doRequestPasswordSalt()
@ -213,21 +222,33 @@ Command_Login RemoteClient::generateCommandLogin()
void RemoteClient::doLogin()
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
cmdLogin.set_password(password.toStdString());
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
} else {
doHashedLogin(); // log in using hashed password instead
}
} else {
// TODO add setting for client to reject unhashed logins
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
if (!password.isEmpty()) {
qWarning() << "using plain text password to log in";
cmdLogin.set_password(password.toStdString());
}
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
PendingCommand *pend = prepareSessionCommand(cmdLogin);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
sendCommand(pend);
}
}
void RemoteClient::doLogin(const QString &passwordSalt)
void RemoteClient::doHashedLogin()
{
setStatus(StatusLoggingIn);
Command_Login cmdLogin = generateCommandLogin();
const auto hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
cmdLogin.set_hashed_password(hashedPassword.toStdString());
PendingCommand *pend = prepareSessionCommand(cmdLogin);
@ -244,12 +265,13 @@ 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
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
doLogin();
} else {
doLogin(salt);
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
doHashedLogin();
}
} else if (response.response_code() != Response::RespNotConnected) {
emit loginError(response.response_code(), {}, 0, {});
@ -438,6 +460,7 @@ void RemoteClient::doConnectToServer(const QString &hostname,
password = _password;
lastHostname = hostname;
lastPort = port;
hashedPassword.clear();
connectToHost(hostname, port);
setStatus(StatusConnecting);
@ -460,6 +483,7 @@ void RemoteClient::doRegisterToServer(const QString &hostname,
realName = _realname;
lastHostname = hostname;
lastPort = port;
hashedPassword.clear();
connectToHost(hostname, port);
setStatus(StatusRegistering);
@ -561,7 +585,7 @@ QString RemoteClient::getSrvClientID(const QString &_hostname)
QHostAddress hostAddress = hostInfo.addresses().first();
srvClientID += hostAddress.toString();
} else {
qDebug() << "Warning: ClientID generation host lookup failure [" << hostInfo.errorString() << "]";
qWarning() << "ClientID generation host lookup failure [" << hostInfo.errorString() << "]";
srvClientID += _hostname;
}
QString uniqueServerClientID =
@ -648,6 +672,7 @@ void RemoteClient::doSubmitForgotPasswordResetToServer(const QString &hostname,
lastPort = port;
token = _token.trimmed();
password = _newpassword;
hashedPassword.clear();
connectToHost(lastHostname, lastPort);
setStatus(StatusSubmitForgotPasswordReset);

View file

@ -70,7 +70,7 @@ private slots:
const QString &_realname);
void doRequestPasswordSalt();
void doLogin();
void doLogin(const QString &passwordSalt);
void doHashedLogin();
Command_Login generateCommandLogin();
void doDisconnectFromServer();
void doActivateToServer(const QString &_token);
@ -101,6 +101,7 @@ private:
QWebSocket *websocket;
QString lastHostname;
unsigned int lastPort;
QString hashedPassword;
QString getSrvClientID(const QString &_hostname);
bool newMissingFeatureFound(const QString &_serversMissingFeatures);

View file

@ -4,6 +4,8 @@
#include "dlg_edit_avatar.h"
#include "dlg_edit_password.h"
#include "dlg_edit_user.h"
#include "gettextwithmax.h"
#include "passwordhasher.h"
#include "pb/response_get_user_info.pb.h"
#include "pb/session_commands.pb.h"
#include "pending_command.h"
@ -11,6 +13,8 @@
#include <QDateTime>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QLabel>
#include <QMessageBox>
@ -200,7 +204,22 @@ void UserInfoBox::actEditInternal(const Response &r)
Command_AccountEdit cmd;
cmd.set_real_name(dlg.getRealName().toStdString());
cmd.set_email(dlg.getEmail().toStdString());
if (client->getServerSupportsPasswordHash()) {
if (email != dlg.getEmail()) {
// real password is required to change email
bool ok = false;
QString password =
getTextWithMax(this, tr("Enter Password"),
tr("Password verification is required in order to change your email address"),
QLineEdit::Password, "", &ok);
if (!ok)
return;
cmd.set_password_check(password.toStdString());
cmd.set_email(dlg.getEmail().toStdString());
} // servers that support password hash do not require all fields to be filled anymore
} else {
cmd.set_email(dlg.getEmail().toStdString());
}
cmd.set_country(dlg.getCountry().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
@ -216,9 +235,42 @@ void UserInfoBox::actPassword()
if (!dlg.exec())
return;
auto oldPassword = dlg.getOldPassword();
auto newPassword = dlg.getNewPassword();
if (client->getServerSupportsPasswordHash()) {
Command_RequestPasswordSalt cmd;
cmd.set_user_name(client->getUserName().toStdString());
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend,
// we need qoverload here in order to select the right version of this function
QOverload<const Response &, const CommandContainer &, const QVariant &>::of(&PendingCommand::finished),
this, [=](const Response &response, const CommandContainer &, const QVariant &) {
if (response.response_code() == Response::RespOk) {
changePassword(oldPassword, newPassword);
} else {
QMessageBox::critical(this, tr("Error"),
tr("An error occurred while trying to update your user information."));
}
});
client->sendCommand(pend);
} else {
changePassword(oldPassword, newPassword);
}
}
void UserInfoBox::changePassword(const QString &oldPassword, const QString &newPassword)
{
Command_AccountPassword cmd;
cmd.set_old_password(dlg.getOldPassword().toStdString());
cmd.set_new_password(dlg.getNewPassword().toStdString());
cmd.set_old_password(oldPassword.toStdString());
if (client->getServerSupportsPasswordHash()) {
auto passwordSalt = PasswordHasher::generateRandomSalt();
QString hashedPassword = PasswordHasher::computeHash(newPassword, passwordSalt);
cmd.set_hashed_new_password(hashedPassword.toStdString());
} else {
cmd.set_new_password(newPassword.toStdString());
}
PendingCommand *pend = client->prepareSessionCommand(cmd);
connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this,
@ -254,6 +306,9 @@ void UserInfoBox::processEditResponse(const Response &r)
QMessageBox::critical(this, tr("Error"),
tr("This server does not permit you to update your user informations."));
break;
case Response::RespWrongPassword:
QMessageBox::critical(this, tr("Error"), tr("The entered password does not match your account."));
break;
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
@ -280,7 +335,7 @@ void UserInfoBox::processPasswordResponse(const Response &r)
case Response::RespInternalError:
default:
QMessageBox::critical(this, tr("Error"),
tr("An error occured while trying to update your user informations."));
tr("An error occurred while trying to update your user information."));
break;
}
}

View file

@ -49,6 +49,7 @@ public slots:
private:
void resizeEvent(QResizeEvent *event) override;
void changePassword(const QString &oldPassword, const QString &newPassword);
};
#endif