diff --git a/scripts/murmur.ini b/scripts/murmur.ini index 98ccc43a311..d103a0aa8d9 100644 --- a/scripts/murmur.ini +++ b/scripts/murmur.ini @@ -172,6 +172,15 @@ users=100 # system. #sendversion=True +# This sets password hash storage to legacy mode (1.2.4 and before) +# (Note that setting this to true is insecure and should not be used unless absolutely necessary) +#legacyPasswordHash=false + +# By default a strong amount of PBKDF2 iterations are chosen automatically. If >0 this setting +# overrides the automatic benchmark and forces a specific number of iterations. +# (Note that you should only change this value if you know what you are doing) +#kdfIterations=-1 + # You can configure any of the configuration options for Ice here. We recommend # leave the defaults as they are. # Please note that this section has to be last in the configuration file. diff --git a/src/murmur/Meta.cpp b/src/murmur/Meta.cpp index 4bd0bbbe4af..fbc25d2e449 100644 --- a/src/murmur/Meta.cpp +++ b/src/murmur/Meta.cpp @@ -54,6 +54,8 @@ MetaParams::MetaParams() { iMaxUsersPerChannel = 0; iMaxTextMessageLength = 5000; iMaxImageMessageLength = 131072; + legacyPasswordHash = false; + kdfIterations = -1; bAllowHTML = true; iDefaultChan = 0; bRememberChan = true; @@ -262,6 +264,8 @@ void MetaParams::read(QString fname) { iTimeout = typeCheckedFromSettings("timeout", iTimeout); iMaxTextMessageLength = typeCheckedFromSettings("textmessagelength", iMaxTextMessageLength); iMaxImageMessageLength = typeCheckedFromSettings("imagemessagelength", iMaxImageMessageLength); + legacyPasswordHash = typeCheckedFromSettings("legacypasswordhash", legacyPasswordHash); + kdfIterations = typeCheckedFromSettings("kdfiterations", -1); bAllowHTML = typeCheckedFromSettings("allowhtml", bAllowHTML); iMaxBandwidth = typeCheckedFromSettings("bandwidth", iMaxBandwidth); iDefaultChan = typeCheckedFromSettings("defaultchannel", iDefaultChan); @@ -458,6 +462,8 @@ void MetaParams::read(QString fname) { qmConfig.insert(QLatin1String("port"),QString::number(usPort)); qmConfig.insert(QLatin1String("timeout"),QString::number(iTimeout)); qmConfig.insert(QLatin1String("textmessagelength"), QString::number(iMaxTextMessageLength)); + qmConfig.insert(QLatin1String("legacypasswordhash"), legacyPasswordHash ? QLatin1String("true") : QLatin1String("false")); + qmConfig.insert(QLatin1String("kdfiterations"), QString::number(kdfIterations)); qmConfig.insert(QLatin1String("allowhtml"), bAllowHTML ? QLatin1String("true") : QLatin1String("false")); qmConfig.insert(QLatin1String("bandwidth"),QString::number(iMaxBandwidth)); qmConfig.insert(QLatin1String("users"),QString::number(iMaxUsers)); diff --git a/src/murmur/Meta.h b/src/murmur/Meta.h index 19bcf2029a3..08816cd4237 100644 --- a/src/murmur/Meta.h +++ b/src/murmur/Meta.h @@ -63,6 +63,13 @@ class MetaParams { int iMaxImageMessageLength; int iOpusThreshold; int iChannelNestingLimit; + /// If true the old SHA1 password hashing is used instead of PBKDF2 + bool legacyPasswordHash; + /// Contains the default number of PBKDF2 iterations to use + /// when hashing passwords. If the value loaded from config + /// is <= 0 the value is loaded from the database and if not + /// available there yet found by a benchmark. + int kdfIterations; bool bAllowHTML; QString qsPassword; QString qsWelcomeText; diff --git a/src/murmur/PBKDF2.cpp b/src/murmur/PBKDF2.cpp new file mode 100644 index 00000000000..3e7b1922527 --- /dev/null +++ b/src/murmur/PBKDF2.cpp @@ -0,0 +1,95 @@ +/* Copyright (C) 2013, Morris Moraes + Copyright (C) 2014, Stefan Hacker + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + - Neither the name of the Mumble Developers nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#include "murmur_pch.h" + +#include "PBKDF2.h" + +int PBKDF2::benchmark() { + const QString pseudopass(QLatin1String("aboutAvg")); + const QString hexSalt = getSalt(); // Could tolerate not getting a salt here, will likely only make it harder. + + int maxIterations = -1; + + QElapsedTimer timer; + timer.start(); + + for (size_t i = 0; i < BENCHMARK_N; ++i) { + int iterations = BENCHMARK_MINIMUM_ITERATION_COUNT / 2; + + timer.restart(); + do { + iterations *= 2; + + // Store return value in a volatile to prevent optimizer + // from ever removing these side-effect-free calls. I don't + // think the compiler can prove they have no side-effects but + // better safe than sorry. + volatile QString result = getHash(hexSalt, pseudopass, iterations); + Q_UNUSED(result); + + } while (timer.restart() < BENCHMARK_DURATION_TARGET_IN_MS && (iterations / 2) < std::numeric_limits::max()); + + if (iterations > maxIterations) { + maxIterations = iterations; + } + } + return maxIterations; +} + +QString PBKDF2::getHash(const QString &hexSalt, const QString &password, int iterationCount) { + QByteArray hash(DERIVED_KEY_LENGTH, 0); + + const QByteArray utf8Password = password.toUtf8(); + const QByteArray salt = QByteArray::fromHex(hexSalt.toLatin1()); + + if (PKCS5_PBKDF2_HMAC(utf8Password.constData(), utf8Password.size(), + reinterpret_cast(salt.constData()), salt.size(), + iterationCount, + EVP_sha384(), + DERIVED_KEY_LENGTH, reinterpret_cast(hash.data())) == 0) { + qFatal("PBKDF2: PKCS5_PBKDF2_HMAC failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return QString(); + } + + return hash.toHex(); +} + + +QString PBKDF2::getSalt() { + QByteArray salt(SALT_LENGTH, 0); + + if (RAND_bytes(reinterpret_cast(salt.data()), salt.size()) != 1) { + qFatal("PBKDF2: RAND_bytes for salt failed: %s", ERR_error_string(ERR_get_error(), NULL)); + return QString(); + } + + return salt.toHex(); +} diff --git a/src/murmur/PBKDF2.h b/src/murmur/PBKDF2.h new file mode 100644 index 00000000000..6e8d0bc4445 --- /dev/null +++ b/src/murmur/PBKDF2.h @@ -0,0 +1,85 @@ +/* Copyright (C) 2013, Morris Moraes + Copyright (C) 2014, Stefan Hacker + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + - Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + - Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + - Neither the name of the Mumble Developers nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ +#ifndef MUMBLE_MURMUR_PBKDF2_H_ +#define MUMBLE_MURMUR_PBKDF2_H_ + +#include + +class QString; + +/// +/// Fully static wrapper class for PBKF2 password hashing functionality used in Murmur. +/// +/// @note Using int all over the place because OpenSSL uses them in its C interface +/// and we want to make sure not to parameterize it in unexpected ways. +/// @warning Operations in this class that experience internal failure will abort +/// program execution using qFatal. +/// +class PBKDF2 { + public: + /// + /// @return Upper bound on iterations possible in + /// BENCHMARK_DURATION_TARGET_IN_MS on this machine. + /// + static int benchmark(); + + /// Performs a PBKDF2 hash operation using EVP_sha384. + /// + /// @param hexSalt Hex encoded salt to use in operation. + /// @param password Password to hash. + /// @param iterationCount Number of PBKDF2 iterations to apply. + /// @return Hex encoded password hash of DERIVED_KEY_LENGTH octets. + /// + static QString getHash(const QString &hexSalt, + const QString &password, + int iterationCount); + + /// + /// @return SALT_LENGTH octets of hex encoded random salt. + /// + static QString getSalt(); + + /// Length of hash in octets + static const int DERIVED_KEY_LENGTH = 48; + /// Length salt in octests + static const int SALT_LENGTH = 8; + + /// Duration for hash operation the benchmark function should target + static const int BENCHMARK_DURATION_TARGET_IN_MS = 10; + /// Benchmark returns highest iteration number of N benchmark attemps + static const size_t BENCHMARK_N = 40; + /// Lower bound of iteration count returned by benchmark + /// regardless of duration (should be divisible by 2) + static const int BENCHMARK_MINIMUM_ITERATION_COUNT = 1000; + +}; + +#endif // MUMBLE_MURMUR_PBKDF2_H_ diff --git a/src/murmur/ServerDB.cpp b/src/murmur/ServerDB.cpp index 67720d3ae29..6411fcc9c79 100644 --- a/src/murmur/ServerDB.cpp +++ b/src/murmur/ServerDB.cpp @@ -41,6 +41,7 @@ #include "Server.h" #include "ServerUser.h" #include "User.h" +#include "PBKDF2.h" #define SQLDO(x) ServerDB::exec(query, QLatin1String(x), true) #define SQLMAY(x) ServerDB::exec(query, QLatin1String(x), false, false) @@ -49,6 +50,7 @@ #define SQLEXECBATCH() ServerDB::execBatch(query) #define SOFTEXEC() ServerDB::exec(query, QString(), false) + class TransactionHolder { public: QSqlQuery *qsqQuery; @@ -72,6 +74,34 @@ QSqlDatabase *ServerDB::db = NULL; Timer ServerDB::tLogClean; QString ServerDB::qsUpgradeSuffix; +void ServerDB::loadOrSetupMetaPKBDF2IterationsCount(QSqlQuery &query) { + if (!Meta::mp.legacyPasswordHash) { + if (Meta::mp.kdfIterations <= 0) { + // Configuration doesn't specify an override, load from db + + SQLDO("SELECT `value` FROM `%1meta` WHERE `keystring` = 'pbkdf2_iterations'"); + if (query.next()) { + Meta::mp.kdfIterations = query.value(0).toInt(); + } + + if (Meta::mp.kdfIterations <= 0) { + // Didn't get a valid iteration count from DB, overwrite + Meta::mp.kdfIterations = PBKDF2::benchmark(); + + qWarning() << "Performed initial PBKDF2 benchmark. Will use " << Meta::mp.kdfIterations << " iterations as default"; + + SQLPREP("INSERT INTO `%1meta` (`keystring`, `value`) VALUES('pbkdf2_iterations',?)"); + query.addBindValue(Meta::mp.kdfIterations); + SQLEXEC(); + } + } + + if (Meta::mp.kdfIterations < PBKDF2::BENCHMARK_MINIMUM_ITERATION_COUNT) { + qWarning() << "Configured default PBKDF2 iteration count of " << Meta::mp.kdfIterations << " is below minimum recommended value of " << PBKDF2::BENCHMARK_MINIMUM_ITERATION_COUNT << " and could be insecure."; + } + } +} + ServerDB::ServerDB() { if (! QSqlDatabase::isDriverAvailable(Meta::mp.qsDBDriver)) { qFatal("ServerDB: Database driver %s not available", qPrintable(Meta::mp.qsDBDriver)); @@ -152,10 +182,13 @@ ServerDB::ServerDB() { SQLDO("CREATE TABLE IF NOT EXISTS `%1meta`(`keystring` varchar(255) PRIMARY KEY, `value` varchar(255)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin"); SQLDO("SELECT `value` FROM `%1meta` WHERE `keystring` = 'version'"); + if (query.next()) version = query.value(0).toInt(); + + loadOrSetupMetaPKBDF2IterationsCount(query); - if (version < 5) { + if (version < 6) { if (version > 0) { qWarning("Renaming old tables..."); SQLDO("ALTER TABLE `%1servers` RENAME TO `%1servers%2`"); @@ -240,7 +273,7 @@ ServerDB::ServerDB() { SQLDO("CREATE UNIQUE INDEX `%1channel_info_id` ON `%1channel_info`(`server_id`, `channel_id`, `key`)"); SQLDO("CREATE TRIGGER `%1channel_info_del_channel` AFTER DELETE on `%1channels` FOR EACH ROW BEGIN DELETE FROM `%1channel_info` WHERE `channel_id` = old.`channel_id` AND `server_id` = old.`server_id`; END;"); - SQLDO("CREATE TABLE `%1users` (`server_id` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `pw` TEXT, `lastchannel` INTEGER, `texture` BLOB, `last_active` DATE)"); + SQLDO("CREATE TABLE `%1users` (`server_id` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `name` TEXT NOT NULL, `pw` TEXT, `salt` TEXT, `kdfiterations` INTEGER, `lastchannel` INTEGER, `texture` BLOB, `last_active` DATE)"); SQLDO("CREATE UNIQUE INDEX `%1users_name` ON `%1users` (`server_id`,`name`)"); SQLDO("CREATE UNIQUE INDEX `%1users_id` ON `%1users` (`server_id`, `user_id`)"); SQLDO("CREATE TRIGGER `%1users_server_del` AFTER DELETE ON `%1servers` FOR EACH ROW BEGIN DELETE FROM `%1users` WHERE `server_id` = old.`server_id`; END;"); @@ -330,7 +363,7 @@ ServerDB::ServerDB() { SQLDO("CREATE UNIQUE INDEX `%1channel_info_id` ON `%1channel_info`(`server_id`, `channel_id`, `key`)"); SQLDO("ALTER TABLE `%1channel_info` ADD CONSTRAINT `%1channel_info_del_channel` FOREIGN KEY (`server_id`, `channel_id`) REFERENCES `%1channels`(`server_id`,`channel_id`) ON DELETE CASCADE"); - SQLDO("CREATE TABLE `%1users` (`server_id` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `name` varchar(255), `pw` varchar(128), `lastchannel` INTEGER, `texture` LONGBLOB, `last_active` TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin"); + SQLDO("CREATE TABLE `%1users` (`server_id` INTEGER NOT NULL, `user_id` INTEGER NOT NULL, `name` varchar(255), `pw` varchar(128), `salt` varchar(128), `kdfiterations` INTEGER, `lastchannel` INTEGER, `texture` LONGBLOB, `last_active` TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin"); SQLDO("CREATE INDEX `%1users_channel` ON `%1users`(`server_id`, `lastchannel`)"); SQLDO("CREATE UNIQUE INDEX `%1users_name` ON `%1users` (`server_id`,`name`)"); SQLDO("CREATE UNIQUE INDEX `%1users_id` ON `%1users` (`server_id`, `user_id`)"); @@ -364,7 +397,7 @@ ServerDB::ServerDB() { } if (version == 0) { SQLDO("INSERT INTO `%1servers` (`server_id`) VALUES(1)"); - SQLDO("INSERT INTO `%1meta` (`keystring`, `value`) VALUES('version','5')"); + SQLDO("INSERT INTO `%1meta` (`keystring`, `value`) VALUES('version','6')"); } else { qWarning("Importing old data..."); @@ -462,7 +495,7 @@ ServerDB::ServerDB() { SQLDO("DROP TABLE IF EXISTS `%1bans%2`"); SQLDO("DROP TABLE IF EXISTS `%1servers%2`"); - SQLDO("UPDATE `%1meta` SET `value` = '5' WHERE `keystring` = 'version'"); + SQLDO("UPDATE `%1meta` SET `value` = '6' WHERE `keystring` = 'version'"); } } query.clear(); @@ -842,10 +875,10 @@ QMap Server::getRegistration(int id) { /// @return UserID of authenticated user, -1 for authentication failures, -2 for unknown user (fallthrough), /// -3 for authentication failures where the data could (temporarily) not be verified. -int Server::authenticate(QString &name, const QString &pw, int sessionId, const QStringList &emails, const QString &certhash, bool bStrongCert, const QList &certs) { +int Server::authenticate(QString &name, const QString &password, int sessionId, const QStringList &emails, const QString &certhash, bool bStrongCert, const QList &certs) { int res = bForceExternalAuth ? -3 : -2; - emit authenticateSig(res, name, sessionId, certs, certhash, bStrongCert, pw); + emit authenticateSig(res, name, sessionId, certs, certhash, bStrongCert, password); if (res != -2) { // External authentication handled it. Ignore certificate completely. @@ -874,19 +907,72 @@ int Server::authenticate(QString &name, const QString &pw, int sessionId, const TransactionHolder th; QSqlQuery &query = *th.qsqQuery; - SQLPREP("SELECT `user_id`,`name`,`pw` FROM `%1users` WHERE `server_id` = ? AND LOWER(`name`) = LOWER(?)"); + SQLPREP("SELECT `user_id`,`name`,`pw`, `salt`, `kdfiterations` FROM `%1users` WHERE `server_id` = ? AND LOWER(`name`) = LOWER(?)"); query.addBindValue(iServerNum); query.addBindValue(name); SQLEXEC(); if (query.next()) { + const int userId = query.value(0).toInt(); + const QString storedPasswordHash = query.value(2).toString(); + const QString storedSalt = query.value(3).toString(); + const int storedKdfIterations = query.value(4).toInt(); res = -1; - QString storedpw = query.value(2).toString(); - QString hashedpw = QString::fromLatin1(sha1(pw).toHex()); - if (! storedpw.isEmpty() && (storedpw == hashedpw)) { - name = query.value(1).toString(); - res = query.value(0).toInt(); - } else if (query.value(0).toInt() == 0) { + if (!storedPasswordHash.isEmpty()) { + // A user has password authentication enabled if there is a password hash. + + if (storedKdfIterations <= 0) { + // If storedKdfIterations is <=0 this means this is an old-style SHA1 hash + // that hasn't been converted yet. Or we are operating in legacy mode. + if (ServerDB::getLegacySHA1Hash(password) == storedPasswordHash) { + name = query.value(1).toString(); + res = query.value(0).toInt(); + + if (! Meta::mp.legacyPasswordHash) { + // Unless disabled upgrade the user password hash + QMap info; + info.insert(ServerDB::User_Password, password); + info.insert(ServerDB::User_KDFIterations, QString::number(Meta::mp.kdfIterations)); + + if (!setInfo(userId, info)) { + qWarning("ServerDB: Failed to upgrade user account to PBKDF2 hash, rejecting login."); + return -1; + } + } + } + } else { + if (PBKDF2::getHash(storedSalt, password, storedKdfIterations) == storedPasswordHash) { + name = query.value(1).toString(); + res = query.value(0).toInt(); + + if (Meta::mp.legacyPasswordHash) { + // Downgrade the password to the legacy hash + QMap info; + info.insert(ServerDB::User_Password, password); + + if (!setInfo(userId, info)) { + qWarning("ServerDB: Failed to downgrade user account to legacy hash, rejecting login."); + return -1; + } + } else if (storedKdfIterations != Meta::mp.kdfIterations) { + // User kdfiterations not equal to the global one. Update it. + QMap info; + info.insert(ServerDB::User_Password, password); + info.insert(ServerDB::User_KDFIterations, QString::number(Meta::mp.kdfIterations)); + + if (!setInfo(userId, info)) { + qWarning() << "ServerDB: Failed to update user PBKDF2 to new iteration count" << Meta::mp.kdfIterations << ", rejecting login."; + return -1; + } + } + } + } + } + + if (userId == 0 && res < 0) { + // For SuperUser only password based authentication is allowed. + // If we couldn't verify the password don't proceed to cert auth + // and instead reject the login attempt. return -1; } } @@ -976,12 +1062,29 @@ bool Server::setInfo(int id, const QMap &setinfo) { info.remove(ServerDB::User_LastActive); } if (info.contains(ServerDB::User_Password)) { - const QString &pw = info.value(ServerDB::User_Password); - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(pw.toUtf8()); + const QString password = info.value(ServerDB::User_Password); + QString passwordHash, salt; + int kdfIterations = -1; + + if (Meta::mp.legacyPasswordHash) { + passwordHash = ServerDB::getLegacySHA1Hash(password); + } else { + kdfIterations = Meta::mp.kdfIterations; + if (info.contains(ServerDB::User_KDFIterations)) { + const int targetIterations = info.value(ServerDB::User_KDFIterations).toInt(); + if (targetIterations > 0) { + kdfIterations = targetIterations; + } + } - SQLPREP("UPDATE `%1users` SET `pw`=? WHERE `server_id` = ? AND `user_id`=?"); - query.addBindValue(pw.isEmpty() ? QVariant() : QString::fromLatin1(hash.result().toHex())); + salt = PBKDF2::getSalt(); + passwordHash = PBKDF2::getHash(salt, password, kdfIterations); + } + + SQLPREP("UPDATE `%1users` SET `pw`=?, `salt`=?, `kdfiterations`=? WHERE `server_id` = ? AND `user_id`=?"); + query.addBindValue(passwordHash); + query.addBindValue(salt); + query.addBindValue(kdfIterations); query.addBindValue(iServerNum); query.addBindValue(id); SQLEXEC(); @@ -1052,9 +1155,14 @@ bool Server::setTexture(int id, const QByteArray &texture) { void ServerDB::setSUPW(int srvnum, const QString &pw) { TransactionHolder th; + QString pwHash, saltHash; - QCryptographicHash hash(QCryptographicHash::Sha1); - hash.addData(pw.toUtf8()); + if (!Meta::mp.legacyPasswordHash) { + saltHash = PBKDF2::getSalt(); + pwHash = PBKDF2::getHash(saltHash, pw, Meta::mp.kdfIterations); + } else { + pwHash = getLegacySHA1Hash(pw); + } QSqlQuery &query = *th.qsqQuery; @@ -1070,13 +1178,20 @@ void ServerDB::setSUPW(int srvnum, const QString &pw) { SQLEXEC(); } - SQLPREP("UPDATE `%1users` SET `pw`=? WHERE `server_id` = ? AND `user_id`=?"); - query.addBindValue(QString::fromLatin1(hash.result().toHex())); + SQLPREP("UPDATE `%1users` SET `pw`=?, `salt`=?, `kdfiterations`=? WHERE `server_id` = ? AND `user_id`=?"); + query.addBindValue(pwHash); + query.addBindValue(saltHash); + query.addBindValue(Meta::mp.kdfIterations); query.addBindValue(srvnum); query.addBindValue(0); SQLEXEC(); } +QString ServerDB::getLegacySHA1Hash(const QString &password) { + QByteArray hash = QCryptographicHash::hash(password.toUtf8(), QCryptographicHash::Sha1); + return QString::fromLatin1(hash.toHex()); +} + QString Server::getUserName(int id) { if (qhUserNameCache.contains(id)) return qhUserNameCache.value(id); diff --git a/src/murmur/ServerDB.h b/src/murmur/ServerDB.h index 77e80643196..d9c789064be 100644 --- a/src/murmur/ServerDB.h +++ b/src/murmur/ServerDB.h @@ -44,7 +44,7 @@ class QSqlQuery; class ServerDB { public: enum ChannelInfo { Channel_Description, Channel_Position }; - enum UserInfo { User_Name, User_Email, User_Comment, User_Hash, User_Password, User_LastActive }; + enum UserInfo { User_Name, User_Email, User_Comment, User_Hash, User_Password, User_LastActive, User_KDFIterations }; ServerDB(); ~ServerDB(); typedef QPair LogRecord; @@ -61,6 +61,7 @@ class ServerDB { static QVariant getConf(int server_id, const QString &key, QVariant def = QVariant()); static void setConf(int server_id, const QString &key, const QVariant &value = QVariant()); static QList getLog(int server_id, unsigned int offs_min, unsigned int offs_max); + static QString getLegacySHA1Hash(const QString &password); static int getLogLen(int server_id); static void wipeLogs(); static bool prepare(QSqlQuery &, const QString &, bool fatal = true, bool warn = true); @@ -68,6 +69,9 @@ class ServerDB { static bool execBatch(QSqlQuery &, const QString &str = QString(), bool fatal= true); // No copy; private declaration without implementation ServerDB(const ServerDB &); + + private: + static void loadOrSetupMetaPKBDF2IterationsCount(QSqlQuery &query); }; #endif diff --git a/src/murmur/murmur.pro b/src/murmur/murmur.pro index 91f0c6968f1..194ae31ce16 100644 --- a/src/murmur/murmur.pro +++ b/src/murmur/murmur.pro @@ -16,8 +16,8 @@ TARGET = murmur DBFILE = murmur.db LANGUAGE = C++ FORMS = -HEADERS *= Server.h ServerUser.h Meta.h -SOURCES *= main.cpp Server.cpp ServerUser.cpp ServerDB.cpp Register.cpp Cert.cpp Messages.cpp Meta.cpp RPC.cpp +HEADERS *= Server.h ServerUser.h Meta.h PBKDF2.h +SOURCES *= main.cpp Server.cpp ServerUser.cpp ServerDB.cpp Register.cpp Cert.cpp Messages.cpp Meta.cpp RPC.cpp PBKDF2.cpp DIST = DBus.h ServerDB.h ../../icons/murmur.ico Murmur.ice MurmurI.h MurmurIceWrapper.cpp murmur.plist PRECOMPILED_HEADER = murmur_pch.h diff --git a/src/murmur/murmur_pch.h b/src/murmur/murmur_pch.h index 0eeaee20516..642a42df093 100644 --- a/src/murmur/murmur_pch.h +++ b/src/murmur/murmur_pch.h @@ -45,6 +45,7 @@ extern "C" { #include #include #include +#include /* OpenSSL defines set_key. This breaks our protobuf-generated setters. */ #undef set_key