Cockatrice/libcockatrice_utility/libcockatrice/utility/passwordhasher.h
Lukas Brübach fb7f7dde55
[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
2026-08-29 15:58:57 +02:00

53 lines
1.8 KiB
C++

#ifndef PASSWORDHASHER_H
#define PASSWORDHASHER_H
#include <QByteArray>
#include <QObject>
// scrypt cost parameters used for newly created password verifiers. These match
// the RFC 7914 recommended parameters for interactive use.
constexpr int SCRYPT_N = 32768;
constexpr int SCRYPT_R = 8;
constexpr int SCRYPT_P = 1;
constexpr int SCRYPT_SALT_LENGTH = 16;
constexpr int SCRYPT_VERIFIER_LENGTH = 64;
enum class PasswordFormat
{
None = 0,
Scrypt
};
struct PasswordVerifier
{
PasswordFormat format = PasswordFormat::None;
int n = 0;
int r = 0;
int p = 0;
QByteArray salt;
QByteArray verifier;
bool isValid = false;
};
class PasswordHasher
{
public:
static QString computeHash(const QString &password, const QString &salt);
static QString generateRandomSalt(const int len = 16);
static QString generateActivationToken();
/** @brief Derive the scrypt verifier for the given password, salt and cost parameters. */
static QByteArray deriveKey(const QString &password, const QByteArray &salt, int n, int r, int p);
/** @brief Build a "$scrypt$<n>$<r>$<p>$<salt>$<verifier>" string with a fresh random salt. */
static QString generatePasswordVerifier(const QString &password);
/** @brief Parse a stored "$scrypt$..." string into its components. */
static PasswordVerifier parsePasswordVerifier(const QString &stored);
/** @brief True if the stored value is not in the scrypt format (legacy salt+hash). */
static bool isLegacyFormat(const QString &stored);
/** @brief HMAC-SHA256 of nonce keyed with the password verifier, used for challenge-response logins. */
static QByteArray computeResponse(const QByteArray &key, const QByteArray &nonce);
/** @brief Constant-time byte comparison. */
static bool constantTimeEquals(const QByteArray &a, const QByteArray &b);
};
#endif