Auth cookie support

This commit is contained in:
Gent Semaj 2024-09-05 11:00:16 -04:00
parent 52833f7fb3
commit a38b14b79a
Signed by untrusted user: ycc
GPG Key ID: 2D76C57BF6BEADC4
6 changed files with 128 additions and 32 deletions

19
sql/migration4.sql Normal file
View File

@ -0,0 +1,19 @@
/*
It is recommended in the SQLite manual to turn off
foreign keys when making schema changes that involve them
*/
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
-- New table to store auth cookies
CREATE TABLE Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Valid INTEGER NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);
-- Update DB Version
UPDATE Meta SET Value = 5 WHERE Key = 'DatabaseVersion';
UPDATE Meta SET Value = strftime('%s', 'now') WHERE Key = 'LastMigration';
COMMIT;
PRAGMA foreign_keys=ON;

View File

@ -143,7 +143,7 @@ CREATE TABLE IF NOT EXISTS EmailItems (
UNIQUE (PlayerID, MsgIndex, Slot) UNIQUE (PlayerID, MsgIndex, Slot)
); );
CREATE TABLE IF NOT EXISTS RaceResults( CREATE TABLE IF NOT EXISTS RaceResults (
EPID INTEGER NOT NULL, EPID INTEGER NOT NULL,
PlayerID INTEGER NOT NULL, PlayerID INTEGER NOT NULL,
Score INTEGER NOT NULL, Score INTEGER NOT NULL,
@ -153,9 +153,17 @@ CREATE TABLE IF NOT EXISTS RaceResults(
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE
); );
CREATE TABLE IF NOT EXISTS RedeemedCodes( CREATE TABLE IF NOT EXISTS RedeemedCodes (
PlayerID INTEGER NOT NULL, PlayerID INTEGER NOT NULL,
Code TEXT NOT NULL, Code TEXT NOT NULL,
FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE, FOREIGN KEY(PlayerID) REFERENCES Players(PlayerID) ON DELETE CASCADE,
UNIQUE (PlayerID, Code) UNIQUE (PlayerID, Code)
) );
CREATE TABLE IF NOT EXISTS Auth (
AccountID INTEGER NOT NULL,
Cookie TEXT NOT NULL,
Valid INTEGER DEFAULT 0 NOT NULL,
FOREIGN KEY(AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE,
UNIQUE (AccountID)
);

View File

@ -5,7 +5,7 @@
#include <string> #include <string>
#include <vector> #include <vector>
#define DATABASE_VERSION 4 #define DATABASE_VERSION 5
namespace Database { namespace Database {
@ -53,6 +53,10 @@ namespace Database {
void updateAccountLevel(int accountId, int accountLevel); void updateAccountLevel(int accountId, int accountLevel);
// return true iff cookie is valid for the account.
// invalidates the stored cookie afterwards
bool checkCookie(int accountId, const char *cookie);
// interface for the /ban command // interface for the /ban command
bool banPlayer(int playerId, std::string& reason); bool banPlayer(int playerId, std::string& reason);
bool unbanPlayer(int playerId); bool unbanPlayer(int playerId);

View File

@ -98,6 +98,53 @@ void Database::updateAccountLevel(int accountId, int accountLevel) {
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
bool Database::checkCookie(int accountId, const char *tryCookie) {
std::lock_guard<std::mutex> lock(dbCrit);
const char* sql_get = R"(
SELECT Cookie
FROM Auth
WHERE AccountID = ? AND Valid = 1;
)";
const char* sql_invalidate = R"(
UPDATE Auth
SET Valid = 0
WHERE AccountID = ?;
)";
sqlite3_stmt* stmt;
sqlite3_prepare_v2(db, sql_get, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
int rc = sqlite3_step(stmt);
if (rc != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
const char *cookie = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
if (strlen(cookie) != strlen(tryCookie)) {
sqlite3_finalize(stmt);
return false;
}
/* since cookies are immediately invalidated, we don't need to be concerned about
* timing-related side channel attacks, so strcmp is fine here
*/
bool match = (strcmp(cookie, tryCookie) == 0);
sqlite3_finalize(stmt);
sqlite3_prepare_v2(db, sql_invalidate, -1, &stmt, NULL);
sqlite3_bind_int(stmt, 1, accountId);
rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc != SQLITE_DONE)
std::cout << "[WARN] Database fail on consumeCookie(): " << sqlite3_errmsg(db) << std::endl;
return match;
}
void Database::updateSelected(int accountId, int slot) { void Database::updateSelected(int accountId, int slot) {
std::lock_guard<std::mutex> lock(dbCrit); std::lock_guard<std::mutex> lock(dbCrit);

View File

@ -105,15 +105,14 @@ void loginFail(LoginError errorCode, std::string userLogin, CNSocket* sock) {
void CNLoginServer::login(CNSocket* sock, CNPacketData* data) { void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
auto login = (sP_CL2LS_REQ_LOGIN*)data->buf; auto login = (sP_CL2LS_REQ_LOGIN*)data->buf;
// TODO: implement better way of sending credentials bool isCookieAuth = login->iLoginType == 2;
std::string userLogin((char*)login->szCookie_TEGid);
std::string userPassword((char*)login->szCookie_authid);
/* std::string userLogin;
* Sometimes the client sends garbage cookie data. std::string userPassword;
* Validate it as normal credentials instead of using a length check before falling back. if (isCookieAuth) {
*/ // username encoded in TEGid raw
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) { userLogin = std::string((char*)login->szCookie_TEGid);
} else {
/* /*
* The std::string -> char* -> std::string maneuver should remove any * The std::string -> char* -> std::string maneuver should remove any
* trailing garbage after the null terminator. * trailing garbage after the null terminator.
@ -122,40 +121,55 @@ void CNLoginServer::login(CNSocket* sock, CNPacketData* data) {
userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str()); userPassword = std::string(AUTOU16TOU8(login->szPassword).c_str());
} }
// the client inserts a "\n" in the password if you press enter key in the middle of the password // check username regex
// (not at the start or the end of the password field) if (!CNLoginServer::isUsernameGood(userLogin)) {
if (int(userPassword.find("\n")) > 0)
userPassword.erase(userPassword.find("\n"), 1);
// check regex
if (!CNLoginServer::isLoginDataGood(userLogin, userPassword)) {
// send a custom error message // send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg); INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid login or password\n"; std::string text = "Invalid login\n";
text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore\n"; text += "Login has to be 4 - 32 characters long and can't contain special characters other than dash and underscore";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg)); U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 15; msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
}
// check password regex if not cookie auth
if (!isCookieAuth && !CNLoginServer::isPasswordGood(userPassword)) {
// send a custom error message
INITSTRUCT(sP_FE2CL_GM_REP_PC_ANNOUNCE, msg);
std::string text = "Invalid password\n";
text += "Password has to be 8 - 32 characters long";
U8toU16(text, msg.szAnnounceMsg, sizeof(msg.szAnnounceMsg));
msg.iDuringTime = 10;
sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE); sock->sendPacket(msg, P_FE2CL_GM_REP_PC_ANNOUNCE);
// we still have to send login fail to prevent softlock // we still have to send login fail to prevent softlock
return loginFail(LoginError::LOGIN_ERROR, userLogin, sock); return loginFail(LoginError::LOGIN_ERROR, userLogin, sock);
} }
Database::Account findUser = {}; Database::Account findUser = {};
Database::findAccount(&findUser, userLogin); Database::findAccount(&findUser, userLogin);
// account was not found // account was not found
if (findUser.AccountID == 0) { if (findUser.AccountID == 0) {
if (settings::AUTOCREATEACCOUNTS) // don't auto-create an account if it's a cookie auth for whatever reason
if (settings::AUTOCREATEACCOUNTS && !isCookieAuth)
return newAccount(sock, userLogin, userPassword, login->iClientVerC); return newAccount(sock, userLogin, userPassword, login->iClientVerC);
return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock); return loginFail(LoginError::ID_DOESNT_EXIST, userLogin, sock);
} }
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword)) if (isCookieAuth) {
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock); const char *cookie = reinterpret_cast<const char*>(login->szCookie_authid);
if (!Database::checkCookie(findUser.AccountID, cookie))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
} else {
// simple password check
if (!CNLoginServer::isPasswordCorrect(findUser.Password, userPassword))
return loginFail(LoginError::ID_AND_PASSWORD_DO_NOT_MATCH, userLogin, sock);
}
// is the account banned // is the account banned
if (findUser.BannedUntil > getTimestamp()) { if (findUser.BannedUntil > getTimestamp()) {
@ -621,11 +635,14 @@ bool CNLoginServer::exitDuplicate(int accountId) {
return false; return false;
} }
bool CNLoginServer::isLoginDataGood(std::string login, std::string password) { bool CNLoginServer::isUsernameGood(std::string login) {
std::regex loginRegex("[a-zA-Z0-9_-]{4,32}"); const std::regex loginRegex("[a-zA-Z0-9_-]{4,32}");
std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}"); return (std::regex_match(login, loginRegex));
}
return (std::regex_match(login, loginRegex) && std::regex_match(password, passwordRegex)); bool CNLoginServer::isPasswordGood(std::string password) {
const std::regex passwordRegex("[a-zA-Z0-9!@#$%^&*()_+]{8,32}");
return (std::regex_match(password, passwordRegex));
} }
bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) { bool CNLoginServer::isPasswordCorrect(std::string actualPassword, std::string tryPassword) {

View File

@ -39,7 +39,8 @@ private:
static void changeName(CNSocket* sock, CNPacketData* data); static void changeName(CNSocket* sock, CNPacketData* data);
static void duplicateExit(CNSocket* sock, CNPacketData* data); static void duplicateExit(CNSocket* sock, CNPacketData* data);
static bool isLoginDataGood(std::string login, std::string password); static bool isUsernameGood(std::string login);
static bool isPasswordGood(std::string password);
static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword); static bool isPasswordCorrect(std::string actualPassword, std::string tryPassword);
static bool isAccountInUse(int accountId); static bool isAccountInUse(int accountId);
static bool isCharacterNameGood(std::string Firstname, std::string Lastname); static bool isCharacterNameGood(std::string Firstname, std::string Lastname);