mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-25 02:43:02 -07:00
[Security] Add challenge-response auth with scrypt verifiers and stop storing plaintext passwords
Challenge-response authentication: the client derives a scrypt verifier (RFC 7914, EVP_PBE_scrypt, N=32768, r=8, p=1) and authenticates with HMAC-SHA256(key, nonce), so neither the password nor its hash is transmitted. The stored format becomes "$scrypt$<n>$<r>$<p>$<salt>$<verifier>" and Response_PasswordSalt now carries the cost parameters. Strict servers only accept scrypt verifiers; legacy accounts are migrated after a successful login. Fix #344 for challenge-response servers: a saved profile stores the derived verifier under the password key instead of the plaintext password. The connect dialog loads it without revealing it, autoconnect passes it through, the change-password dialog no longer prefills the old password field with it, and the client only persists the verifier when "Save password" is checked. Took 3 minutes Took 1 minute Took 10 seconds Took 7 minutes
This commit is contained in:
parent
45d97cb8d2
commit
fb7f7dde55
33 changed files with 676 additions and 23 deletions
|
|
@ -27,7 +27,8 @@ 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()
|
||||
messageInProgress(false), handshakeStarted(false), usingWebSocket(false), messageLength(0), hashedPassword(),
|
||||
passwordNeedsMigration(false)
|
||||
{
|
||||
|
||||
clearNewClientFeatures();
|
||||
|
|
@ -114,6 +115,8 @@ 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;
|
||||
|
|
@ -130,7 +133,10 @@ 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() && serverSupportsPasswordHash) {
|
||||
if (!password.isEmpty() && serverSupportsChallengeResponse) {
|
||||
hashedPassword = PasswordHasher::generatePasswordVerifier(password);
|
||||
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
|
||||
} else if (!password.isEmpty() && serverSupportsPasswordHash) {
|
||||
auto passwordSalt = PasswordHasher::generateRandomSalt();
|
||||
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
|
||||
cmdForgotPasswordReset.set_hashed_new_password(hashedPassword.toStdString());
|
||||
|
|
@ -158,7 +164,10 @@ void RemoteClient::processServerIdentificationEvent(const Event_ServerIdentifica
|
|||
if (getStatus() == StatusRegistering) {
|
||||
Command_Register cmdRegister;
|
||||
cmdRegister.set_user_name(userName.toStdString());
|
||||
if (!password.isEmpty() && serverSupportsPasswordHash) {
|
||||
if (!password.isEmpty() && serverSupportsChallengeResponse) {
|
||||
hashedPassword = PasswordHasher::generatePasswordVerifier(password);
|
||||
cmdRegister.set_hashed_password(hashedPassword.toStdString());
|
||||
} else if (!password.isEmpty() && serverSupportsPasswordHash) {
|
||||
auto passwordSalt = PasswordHasher::generateRandomSalt();
|
||||
hashedPassword = PasswordHasher::computeHash(password, passwordSalt);
|
||||
cmdRegister.set_hashed_password(hashedPassword.toStdString());
|
||||
|
|
@ -223,7 +232,9 @@ Command_Login RemoteClient::generateCommandLogin()
|
|||
|
||||
void RemoteClient::doLogin()
|
||||
{
|
||||
if (!password.isEmpty() && serverSupportsPasswordHash) {
|
||||
if ((!password.isEmpty() || !storedVerifier.isEmpty()) && serverSupportsChallengeResponse) {
|
||||
doRequestPasswordSalt(); // ask salt + nonce to build the challenge response
|
||||
} else 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
|
||||
|
|
@ -257,6 +268,30 @@ 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();
|
||||
|
|
@ -269,7 +304,47 @@ 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();
|
||||
|
|
@ -294,6 +369,14 @@ 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));
|
||||
|
|
@ -550,6 +633,8 @@ void RemoteClient::doDisconnectFromServer()
|
|||
websocket->close();
|
||||
}
|
||||
socket->close();
|
||||
derivedVerifier.clear();
|
||||
pendingVerifier.clear();
|
||||
}
|
||||
|
||||
void RemoteClient::ping()
|
||||
|
|
@ -712,6 +797,9 @@ 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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ 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();
|
||||
|
|
@ -80,6 +83,8 @@ 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);
|
||||
|
|
@ -111,6 +116,14 @@ 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);
|
||||
|
|
@ -149,6 +162,12 @@ 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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue