mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-24 18:33:03 -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
d6fbfb32a1
commit
7a3067d4a8
33 changed files with 834 additions and 30 deletions
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
#include <QCryptographicHash>
|
||||
#include <libcockatrice/utility/cryptoutil.h>
|
||||
#include <openssl/crypto.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/hmac.h>
|
||||
|
||||
QString PasswordHasher::computeHash(const QString &password, const QString &salt)
|
||||
{
|
||||
|
|
@ -52,3 +55,114 @@ QString PasswordHasher::generateActivationToken()
|
|||
{
|
||||
return QString(CryptoUtil::randomBytes(16).toBase64().left(16));
|
||||
}
|
||||
|
||||
QByteArray PasswordHasher::deriveKey(const QString &password, const QByteArray &salt, int n, int r, int p)
|
||||
{
|
||||
QByteArray key(SCRYPT_VERIFIER_LENGTH, '\0');
|
||||
const QByteArray passwordUtf8 = password.toUtf8();
|
||||
// EVP_PBE_scrypt aborts unless maxmem covers the required working memory,
|
||||
// which is roughly 128 * n * r bytes (plus the small Salsa20/8 block array).
|
||||
const auto maxmem = static_cast<quint64>(128) * n * r + static_cast<quint64>(128) * r * p + 4096;
|
||||
if (EVP_PBE_scrypt(passwordUtf8.constData(), passwordUtf8.size(),
|
||||
reinterpret_cast<const unsigned char *>(salt.constData()), salt.size(), n, r, p, maxmem,
|
||||
reinterpret_cast<unsigned char *>(key.data()), key.size()) != 1) {
|
||||
return QByteArray();
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
bool PasswordHasher::costParamsAreSane(int n, int r, int p)
|
||||
{
|
||||
// Bounds adopted during review: n in [1024, 2**20] and a power of two, r in [1, 32],
|
||||
// p in [1, 16]. Anything else is rejected before we allocate or derive for it.
|
||||
return n >= 1024 && n <= (1 << 20) && (n & (n - 1)) == 0 && r >= 1 && r <= 32 && p >= 1 && p <= 16;
|
||||
}
|
||||
|
||||
QString PasswordHasher::generatePasswordVerifier(const QString &password)
|
||||
{
|
||||
const QByteArray salt = CryptoUtil::randomBytes(SCRYPT_SALT_LENGTH);
|
||||
const QByteArray verifier = deriveKey(password, salt, SCRYPT_N, SCRYPT_R, SCRYPT_P);
|
||||
return QString("$scrypt$%1$%2$%3$%4$%5")
|
||||
.arg(SCRYPT_N)
|
||||
.arg(SCRYPT_R)
|
||||
.arg(SCRYPT_P)
|
||||
.arg(QString(salt.toBase64()))
|
||||
.arg(QString(verifier.toBase64()));
|
||||
}
|
||||
|
||||
PasswordVerifier PasswordHasher::parsePasswordVerifier(const QString &stored)
|
||||
{
|
||||
PasswordVerifier result;
|
||||
const QStringList parts = stored.split("$");
|
||||
if (parts.size() != 7 || parts.at(1) != "scrypt") {
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
const int n = parts.at(2).toInt(&ok);
|
||||
if (!ok) {
|
||||
return result;
|
||||
}
|
||||
const int r = parts.at(3).toInt(&ok);
|
||||
if (!ok) {
|
||||
return result;
|
||||
}
|
||||
const int p = parts.at(4).toInt(&ok);
|
||||
if (!ok || !costParamsAreSane(n, r, p)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const QByteArray salt = QByteArray::fromBase64(parts.at(5).toUtf8());
|
||||
const QByteArray verifier = QByteArray::fromBase64(parts.at(6).toUtf8());
|
||||
if (salt.isEmpty() || verifier.size() != SCRYPT_VERIFIER_LENGTH) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.format = PasswordFormat::Scrypt;
|
||||
result.n = n;
|
||||
result.r = r;
|
||||
result.p = p;
|
||||
result.salt = salt;
|
||||
result.verifier = verifier;
|
||||
result.isValid = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool PasswordHasher::isLegacyFormat(const QString &stored)
|
||||
{
|
||||
return !stored.startsWith("$");
|
||||
}
|
||||
|
||||
bool PasswordHasher::verifyPassword(const QString &password, const QString &storedPasswordData)
|
||||
{
|
||||
if (isLegacyFormat(storedPasswordData)) {
|
||||
return storedPasswordData == computeHash(password, storedPasswordData.left(16));
|
||||
}
|
||||
|
||||
const PasswordVerifier verifier = parsePasswordVerifier(storedPasswordData);
|
||||
if (!verifier.isValid) {
|
||||
return false;
|
||||
}
|
||||
const QByteArray derived = deriveKey(password, verifier.salt, verifier.n, verifier.r, verifier.p);
|
||||
return !derived.isEmpty() && constantTimeEquals(derived, verifier.verifier);
|
||||
}
|
||||
|
||||
QByteArray PasswordHasher::computeResponse(const QByteArray &key, const QByteArray &nonce)
|
||||
{
|
||||
QByteArray response(EVP_MAX_MD_SIZE, '\0');
|
||||
unsigned int responseLength = 0;
|
||||
if (HMAC(EVP_sha256(), key.constData(), key.size(), reinterpret_cast<const unsigned char *>(nonce.constData()),
|
||||
nonce.size(), reinterpret_cast<unsigned char *>(response.data()), &responseLength) == nullptr) {
|
||||
qFatal("PasswordHasher::computeResponse: HMAC failed");
|
||||
}
|
||||
response.resize(responseLength);
|
||||
return response;
|
||||
}
|
||||
|
||||
bool PasswordHasher::constantTimeEquals(const QByteArray &a, const QByteArray &b)
|
||||
{
|
||||
if (a.size() != b.size()) {
|
||||
return false;
|
||||
}
|
||||
return CRYPTO_memcmp(a.constData(), b.constData(), a.size()) == 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,57 @@
|
|||
#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. Empty on failure. */
|
||||
static QByteArray deriveKey(const QString &password, const QByteArray &salt, int n, int r, int p);
|
||||
/** @brief True if the scrypt cost parameters are acceptable for server and client use. */
|
||||
static bool costParamsAreSane(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 True if the password matches the stored credential, whether legacy salt+hash or scrypt. */
|
||||
static bool verifyPassword(const QString &password, const QString &storedPasswordData);
|
||||
/** @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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue