Move password hashing to server_base.

This is in preparation for adding the `forum_auth` option when uploading an add-on.
This commit is contained in:
Pentarctagon 2021-05-23 11:54:55 -05:00 committed by Pentarctagon
parent ac39ffdcd3
commit 08bfe41b9e
10 changed files with 159 additions and 166 deletions

View file

@ -1,6 +1,5 @@
addon/validation.cpp
server/common/dbconn.cpp
server/common/user_handler.cpp
server/common/forum_user_handler.cpp
server/common/server_base.cpp
server/common/simple_wml.cpp

View file

@ -1,5 +1,4 @@
server/common/dbconn.cpp
server/common/user_handler.cpp
server/common/forum_user_handler.cpp
server/common/resultsets/ban_check.cpp
server/common/resultsets/tournaments.cpp

View file

@ -49,12 +49,12 @@ fuh::fuh(const config& c)
}
}
bool fuh::login(const std::string& name, const std::string& password, const std::string& seed) {
bool fuh::login(const std::string& name, const std::string& password, const std::string& nonce) {
// Retrieve users' password as hash
std::string hash;
try {
hash = get_hash(name);
hash = get_hashed_password_from_db(name);
} catch (const error& e) {
ERR_UH << "Could not retrieve hash for user '" << name << "' :" << e.message << std::endl;
return false;
@ -63,9 +63,9 @@ bool fuh::login(const std::string& name, const std::string& password, const std:
std::string valid_hash;
if(utils::md5::is_valid_hash(hash)) { // md5 hash
valid_hash = utils::md5(hash.substr(12,34), seed).base64_digest();
valid_hash = utils::md5(hash.substr(12,34), nonce).base64_digest();
} else if(utils::bcrypt::is_valid_prefix(hash)) { // bcrypt hash
valid_hash = utils::md5(hash, seed).base64_digest();
valid_hash = utils::md5(hash, nonce).base64_digest();
} else {
ERR_UH << "Invalid hash for user '" << name << "'" << std::endl;
return false;
@ -86,7 +86,7 @@ std::string fuh::extract_salt(const std::string& name) {
std::string hash;
try {
hash = get_hash(name);
hash = get_hashed_password_from_db(name);
} catch (const error& e) {
ERR_UH << "Could not retrieve hash for user '" << name << "' :" << e.message << std::endl;
return "";
@ -188,7 +188,7 @@ std::string fuh::user_info(const std::string& name) {
return info.str();
}
std::string fuh::get_hash(const std::string& user) {
std::string fuh::get_hashed_password_from_db(const std::string& user) {
return conn_.get_user_string(db_users_table_, "user_password", user);
}

View file

@ -37,11 +37,11 @@ public:
*
* @param name The username used to login.
* @param password The hashed password sent by the client.
* @param seed The nonce created for this login attempt.
* @param nonce The nonce created for this login attempt.
* @see server::send_password_request().
* @return Whether the hashed password sent by the client matches the hash retrieved from the phpbb database.
*/
bool login(const std::string& name, const std::string& password, const std::string& seed);
bool login(const std::string& name, const std::string& password, const std::string& nonce);
/**
* Needed because the hashing algorithm used by phpbb requires some info
@ -270,7 +270,7 @@ private:
* @param user The player's username.
* @return The player's hashed password from the phpbb forum database.
*/
std::string get_hash(const std::string& user);
std::string get_hashed_password_from_db(const std::string& user);
/**
* @param user The player's username.

View file

@ -14,9 +14,13 @@
#include "server/common/server_base.hpp"
#include "config.hpp"
#include "hash.hpp"
#include "log.hpp"
#include "serialization/parser.hpp"
#include "serialization/base64.hpp"
#include "filesystem.hpp"
#include "random.hpp"
#ifdef HAVE_CONFIG_H
#include "config.h"
@ -38,8 +42,19 @@
#endif
#include <boost/asio/write.hpp>
#include <array>
#include <ctime>
#include <functional>
#include <queue>
#include <sstream>
#include <string>
#ifndef __APPLE__
#include <openssl/rand.h>
#else
#include <cstdlib>
#endif
static lg::log_domain log_server("server");
#define ERR_SERVER LOG_STREAM(err, log_server)
@ -571,6 +586,106 @@ void server_base::load_tls_config(const config& cfg)
if(!cfg["tls_dh"].str().empty()) tls_context_.use_tmp_dh_file(cfg["tls_dh"].str());
}
std::string server_base::create_unsecure_nonce(int length)
{
srand(static_cast<unsigned>(std::time(nullptr)));
std::stringstream ss;
for(int i = 0; i < length; i++) {
ss << randomness::rng::default_instance().get_random_int(0, 9);
}
return ss.str();
}
#ifndef __APPLE__
namespace
{
class RAND_bytes_exception : public std::exception
{
};
} // namespace
#endif
std::string server_base::create_secure_nonce()
{
// Must be full base64 encodings (3 bytes = 4 chars) else we skew the PRNG results
std::array<unsigned char, (3 * 32) / 4> buf;
#ifndef __APPLE__
if(!RAND_bytes(buf.data(), buf.size())) {
throw RAND_bytes_exception();
}
#else
arc4random_buf(buf.data(), buf.size());
#endif
return base64::encode({buf.data(), buf.size()});
}
std::pair<std::string, std::string> server_base::hash_password(const std::string& pw, const std::string& plain_salt, const std::string& username)
{
std::string password = pw;
// if using crypt_blowfish hashing, create a properly secure nonce
// otherwise if using MD5 hashing, create an insecure nonce
std::string nonce{(plain_salt.length() > 1 && plain_salt[1] == '2')
? create_secure_nonce()
: create_unsecure_nonce()};
std::string salt = plain_salt + nonce;
// Apparently HTML key-characters are passed to the hashing functions of phpbb in this escaped form.
// I will do closer investigations on this, for now let's just hope these are all of them.
// Note: we must obviously replace '&' first, I wasted some time before I figured that out... :)
for(std::string::size_type pos = 0; (pos = password.find('&', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&amp;");
}
for(std::string::size_type pos = 0; (pos = password.find('\"', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&quot;");
}
for(std::string::size_type pos = 0; (pos = password.find('<', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&lt;");
}
for(std::string::size_type pos = 0; (pos = password.find('>', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&gt;");
}
if(salt.length() < 12) {
ERR_SERVER << "Bad salt found for user: " << username << std::endl;
return std::make_pair("", "");
}
if(utils::md5::is_valid_prefix(salt)) {
std::string md5_1 = utils::md5(password, utils::md5::get_salt(salt), utils::md5::get_iteration_count(salt)).base64_digest();
std::string md5_2 = utils::md5(md5_1, salt.substr(12, 8)).base64_digest();
return std::make_pair(md5_2, nonce);
} else if(utils::bcrypt::is_valid_prefix(salt)) {
try {
auto bcrypt_salt = utils::bcrypt::from_salted_salt(salt);
auto hash = utils::bcrypt::hash_pw(password, bcrypt_salt);
const std::string outer_salt = salt.substr(bcrypt_salt.iteration_count_delim_pos + 23);
if(outer_salt.size() != 32) {
throw utils::hash_error("salt wrong size");
}
return std::make_pair(
utils::md5(hash.base64_digest(), outer_salt).base64_digest(),
nonce
);
} catch(const utils::hash_error& err) {
ERR_SERVER << "bcrypt hash failed for user " << username << ": " << err.what() << std::endl;
return std::make_pair("", "");
}
} else {
ERR_SERVER << "Unable to determine how to hash the password for user: " << username << std::endl;
return std::make_pair("", "");
}
}
// This is just here to get it to build without the deprecation_message function
#include "game_version.hpp"
#include "deprecation.hpp"

View file

@ -117,6 +117,31 @@ public:
template<class SocketPtr> void async_send_error(SocketPtr socket, const std::string& msg, const char* error_code = "", const info_table& info = {});
template<class SocketPtr> void async_send_warning(SocketPtr socket, const std::string& msg, const char* warning_code = "", const info_table& info = {});
/**
* Create the poor security nonce for use with passwords still hashed with MD5.
* Uses 8 random integer digits, 29.8 bits entropy.
*
* @param length How many random numbers to generate.
* @return The nonce to use.
*/
std::string create_unsecure_nonce(int length = 8);
/**
* Create a good security nonce for use with bcrypt/crypt_blowfish hashing.
* Uses 32 random Base64 characters, cryptographic-strength, 192 bits entropy
*
* @return The nonce to use.
*/
std::string create_secure_nonce();
/**
* Handles hashing the password provided by the player before comparing it to the hashed password in the forum database.
*
* @param pw The plaintext password.
* @param plain_salt The salt as retrieved from the forum database.
* @param username The player attempting to log in.
* @return The hashed password, or empty if the password couldn't be hashed.
*/
std::pair<std::string, std::string> hash_password(const std::string& pw, const std::string& plain_salt, const std::string& username);
protected:
unsigned short port_;
bool keep_alive_;

View file

@ -1,66 +0,0 @@
/*
Copyright (C) 2008 - 2018 by Thomas Baumhauer <thomas.baumhauer@NOSPAMgmail.com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "server/common/user_handler.hpp"
#include "config.hpp"
#include "random.hpp"
#include "serialization/base64.hpp"
#ifndef __APPLE__
#include <openssl/rand.h>
#else
#include <cstdlib>
#endif
#include <array>
#include <ctime>
#include <sstream>
std::string user_handler::create_unsecure_nonce(int length)
{
srand(static_cast<unsigned>(std::time(nullptr)));
std::stringstream ss;
for(int i = 0; i < length; i++) {
ss << randomness::rng::default_instance().get_random_int(0, 9);
}
return ss.str();
}
#ifndef __APPLE__
namespace
{
class RAND_bytes_exception : public std::exception
{
};
} // namespace
#endif
std::string user_handler::create_secure_nonce()
{
// Must be full base64 encodings (3 bytes = 4 chars) else we skew the PRNG results
std::array<unsigned char, (3 * 32) / 4> buf;
#ifndef __APPLE__
if(!RAND_bytes(buf.data(), buf.size())) {
throw RAND_bytes_exception();
}
#else
arc4random_buf(buf.data(), buf.size());
#endif
return base64::encode({buf.data(), buf.size()});
}

View file

@ -128,14 +128,8 @@ public:
error(const std::string& message) : game::error(message) {}
};
/** Create a random string of digits for password encryption. */
std::string create_unsecure_nonce(int length = 8);
std::string create_secure_nonce();
/**
* Create custom salt.
*
* If not needed let it return and empty string or whatever you feel like.
*/
virtual std::string extract_salt(const std::string& username) = 0;

View file

@ -22,7 +22,6 @@
#include "config.hpp"
#include "filesystem.hpp"
#include "game_config.hpp"
#include "hash.hpp"
#include "log.hpp"
#include "multiplayer_error_codes.hpp"
#include "serialization/parser.hpp"
@ -60,7 +59,6 @@
#include <queue>
#include <set>
#include <sstream>
#include <string>
#include <vector>
static lg::log_domain log_server("server");
@ -889,7 +887,16 @@ template<class SocketPtr> bool server::authenticate(
registered = false;
if(user_handler_) {
const auto [hashed_password, nonce] = hash_password(password, username, socket);
const std::string salt = user_handler_->extract_salt(username);
if(salt.empty()) {
async_send_error(socket,
"Even though your nickname is registered on this server you "
"cannot log in due to an error in the hashing algorithm. "
"Logging into your forum account on https://forums.wesnoth.org "
"may fix this problem.");
return false;
}
const auto [hashed_password, nonce] = hash_password(password, salt, username);
const bool exists = user_handler_->user_exists(username);
// This name is registered but the account is not active
@ -980,77 +987,6 @@ template<class SocketPtr> bool server::authenticate(
return true;
}
template<class SocketPtr> std::pair<std::string, std::string> server::hash_password(const std::string& pw, const std::string& user, SocketPtr socket)
{
std::string plain_salt = user_handler_->extract_salt(user);
std::string password = pw;
// If using crypt_blowfish, use 32 random Base64 characters, cryptographic-strength, 192 bits entropy
// else (phppass, MD5, $H$), use 8 random integer digits, not secure, do not use, this is crap, 29.8 bits entropy
std::string nonce{(plain_salt[1] == '2')
? user_handler_->create_secure_nonce()
: user_handler_->create_unsecure_nonce()};
std::string salt = plain_salt + nonce;
if(salt.empty()) {
async_send_error(socket,
"Even though your nickname is registered on this server you "
"cannot log in due to an error in the hashing algorithm. "
"Logging into your forum account on https://forums.wesnoth.org "
"may fix this problem.");
return std::make_pair("", "");
}
// Apparently HTML key-characters are passed to the hashing functions of phpbb in this escaped form.
// I will do closer investigations on this, for now let's just hope these are all of them.
// Note: we must obviously replace '&' first, I wasted some time before I figured that out... :)
for(std::string::size_type pos = 0; (pos = password.find('&', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&amp;");
}
for(std::string::size_type pos = 0; (pos = password.find('\"', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&quot;");
}
for(std::string::size_type pos = 0; (pos = password.find('<', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&lt;");
}
for(std::string::size_type pos = 0; (pos = password.find('>', pos)) != std::string::npos; ++pos) {
password.replace(pos, 1, "&gt;");
}
if(salt.length() < 12) {
ERR_SERVER << "Bad salt found for user: " << user << std::endl;
return std::make_pair("", "");
}
if(utils::md5::is_valid_prefix(salt)) {
std::string md5_1 = utils::md5(password, utils::md5::get_salt(salt), utils::md5::get_iteration_count(salt)).base64_digest();
std::string md5_2 = utils::md5(md5_1, salt.substr(12, 8)).base64_digest();
return std::make_pair(md5_2, nonce);
} else if(utils::bcrypt::is_valid_prefix(salt)) {
try {
auto bcrypt_salt = utils::bcrypt::from_salted_salt(salt);
auto hash = utils::bcrypt::hash_pw(password, bcrypt_salt);
const std::string outer_salt = salt.substr(bcrypt_salt.iteration_count_delim_pos + 23);
if(outer_salt.size() != 32) {
throw utils::hash_error("salt wrong size");
}
return std::make_pair(
utils::md5(hash.base64_digest(), outer_salt).base64_digest(),
nonce
);
} catch(const utils::hash_error& err) {
ERR_SERVER << "bcrypt hash failed: " << err.what() << std::endl;
return std::make_pair("", "");
}
} else {
ERR_SERVER << "Unable to determine how to hash the password for user: " << user << std::endl;
return std::make_pair("", "");
}
}
template<class SocketPtr> void server::send_password_request(SocketPtr socket,
const std::string& msg,
const char* error_code,

View file

@ -58,15 +58,6 @@ private:
void handle_join_game(player_iterator player, simple_wml::node& join);
void disconnect_player(player_iterator player);
void remove_player(player_iterator player);
/**
* Handles hashing the password provided by the player before comparing it to the hashed password in the forum database.
*
* @param pw The plaintext password.
* @param user The player attempting to log in.
* @param socket The socket the player is connected with.
* @return The hashed password, or empty if the password couldn't be hashed.
*/
template<class SocketPtr> std::pair<std::string, std::string> hash_password(const std::string& pw, const std::string& user, SocketPtr socket);
public:
template<class SocketPtr> void send_server_message(SocketPtr socket, const std::string& message, const std::string& type);