aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/connections.cc237
-rw-r--r--src/database.cc436
-rw-r--r--src/querymanager.cc4
-rw-r--r--src/querymanager.hh106
-rw-r--r--src/sha256.cc247
5 files changed, 1024 insertions, 6 deletions
diff --git a/src/connections.cc b/src/connections.cc
index e4ac2f2..79a810e 100644
--- a/src/connections.cc
+++ b/src/connections.cc
@@ -526,12 +526,208 @@ void ProcessLoginAdminQuery(TConnection *Connection, TReadBuffer *Buffer){
SendQueryStatusFailed(Connection);
}
+static int LoginGameTransaction(int WorldID, int AccountID, const char *CharacterName,
+ const char *Password, int IPAddress, bool PrivateWorld, bool GamemasterRequired,
+ TCharacterData *Character, DynamicArray<TAccountBuddy> *Buddies,
+ DynamicArray<TCharacterRight> *Rights, bool *PremiumAccountActivated){
+ TransactionScope Tx("LoginGame");
+ if(!Tx.Begin()){
+ return -1;
+ }
+
+ if(!GetCharacterData(CharacterName, Character)){
+ return -1;
+ }
+
+ if(Character->CharacterID == 0){
+ return 1;
+ }
+
+ if(Character->Deleted){
+ return 2;
+ }
+
+ if(Character->WorldID != WorldID){
+ return 3;
+ }
+
+ if(PrivateWorld){
+ if(!GetWorldInvitation(WorldID, Character->CharacterID)){
+ return 4;
+ }
+ }
+
+ TAccountData Account;
+ if(!GetAccountData(AccountID, &Account)){
+ return -1;
+ }
+
+ if(Account.AccountID == 0 || Account.AccountID != Character->AccountID){
+ // NOTE(fusion): This is correct, there is no error code 5.
+ return 15;
+ }
+
+ if(Account.Deleted){
+ return 8;
+ }
+
+ if(GetAccountLoginAttempts(Account.AccountID, 5 * 60) > 10){
+ return 7;
+ }
+
+ if(GetIPAddressLoginAttempts(IPAddress, 30 * 60) > 15){
+ return 9;
+ }
+
+ if(!TestPassword(Account.Auth, sizeof(Account.Auth), Password)){
+ return 6;
+ }
+
+ if(IsAccountBanished(Account.AccountID)){
+ return 10;
+ }
+
+ if(IsCharacterNamelocked(Character->CharacterID)){
+ return 11;
+ }
+
+ if(IsIPBanished(IPAddress)){
+ return 12;
+ }
+
+ if(!GetCharacterRight(Character->CharacterID, "ALLOW_MULTICLIENT")
+ && GetAccountOnlineCharacters(Account.AccountID) > 0){
+ return 13;
+ }
+
+ if(GamemasterRequired){
+ if(!GetCharacterRight(Character->CharacterID, "GAMEMASTER_OUTFIT")){
+ return 14;
+ }
+ }
+
+ if(!GetBuddies(WorldID, Account.AccountID, Buddies)){
+ return -1;
+ }
+
+ if(!GetCharacterRights(Character->CharacterID, Rights)){
+ return -1;
+ }
+
+ if(!Account.Premium && Account.PendingPremiumDays > 0){
+ if(!ActivatePendingPremiumDays(Account.AccountID)){
+ return -1;
+ }
+
+ *PremiumAccountActivated = true;
+ }
+
+ if(!IncrementIsOnline(WorldID, Character->CharacterID)){
+ return -1;
+ }
+
+ if(!Tx.Commit()){
+ return -1;
+ }
+
+ return 0;
+}
+
void ProcessLoginGameQuery(TConnection *Connection, TReadBuffer *Buffer){
- SendQueryStatusFailed(Connection);
+ if(Connection->ApplicationType != APPLICATION_TYPE_GAME){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ char CharacterName[30];
+ char Password[30];
+ char IPString[16];
+ int AccountID = (int)Buffer->Read32();
+ Buffer->ReadString(CharacterName, sizeof(CharacterName));
+ Buffer->ReadString(Password, sizeof(Password));
+ Buffer->ReadString(IPString, sizeof(IPString));
+ bool PrivateWorld = Buffer->ReadFlag();
+ Buffer->ReadFlag(); // "PremiumAccountRequired" unused
+ bool GamemasterRequired = Buffer->ReadFlag();
+
+ int IPAddress = 0;
+ if(IPString[0] != 0 && !ParseIPAddress(IPString, &IPAddress)){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ TCharacterData Character;
+ DynamicArray<TAccountBuddy> Buddies;
+ DynamicArray<TCharacterRight> Rights;
+ bool PremiumAccountActivated = false;
+ int Result = LoginGameTransaction(Connection->WorldID, AccountID,
+ CharacterName, Password, IPAddress, PrivateWorld,
+ GamemasterRequired, &Character, &Buddies, &Rights,
+ &PremiumAccountActivated);
+
+ // IMPORTANT(fusion): We need to insert login attempts outside the login game
+ // transaction or we could end up not having it recorded at all due to rollbacks.
+ // It is also the reason the whole transaction had to be pulled to its own function.
+ // IMPORTANT(fusion): Don't return if we fail to insert the login attempt as the
+ // result of the whole operation was already determined by the transaction function.
+ InsertLoginAttempt(AccountID, IPAddress, (Result != 0));
+
+ if(Result == -1){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ if(Result != 0){
+ SendQueryStatusError(Connection, Result);
+ return;
+ }
+
+ TWriteBuffer WriteBuffer = PrepareResponse(Connection, QUERY_STATUS_OK);
+ WriteBuffer.Write32((uint32)Character.CharacterID);
+ WriteBuffer.WriteString(Character.Name);
+ WriteBuffer.Write8((uint8)Character.Sex);
+ WriteBuffer.WriteString(Character.Guild);
+ WriteBuffer.WriteString(Character.Rank);
+ WriteBuffer.WriteString(Character.Title);
+
+ int NumBuddies = std::min<int>(Buddies.Length(), UINT8_MAX);
+ WriteBuffer.Write8((uint8)NumBuddies);
+ for(int i = 0; i < NumBuddies; i += 1){
+ WriteBuffer.Write32((uint32)Buddies[i].CharacterID);
+ WriteBuffer.WriteString(Buddies[i].Name);
+ }
+
+ int NumRights = std::min<int>(Rights.Length(), UINT8_MAX);
+ WriteBuffer.Write8((uint8)NumRights);
+ for(int i = 0; i < NumRights; i += 1){
+ WriteBuffer.WriteString(Rights[i].Name);
+ }
+
+ SendResponse(Connection, &WriteBuffer);
}
void ProcessLogoutGameQuery(TConnection *Connection, TReadBuffer *Buffer){
- SendQueryStatusFailed(Connection);
+ if(Connection->ApplicationType != APPLICATION_TYPE_GAME){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ char Profession[30];
+ char Residence[30];
+ int CharacterID = (int)Buffer->Read32();
+ int Level = Buffer->Read16();
+ Buffer->ReadString(Profession, sizeof(Profession));
+ Buffer->ReadString(Residence, sizeof(Residence));
+ int LastLoginTime = (int)Buffer->Read32();
+ int TutorActivities = Buffer->Read16();
+
+ if(!LogoutCharacter(Connection->WorldID, CharacterID, Level,
+ Profession, Residence, LastLoginTime, TutorActivities)){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ SendQueryStatusOk(Connection);
}
void ProcessSetNamelockQuery(TConnection *Connection, TReadBuffer *Buffer){
@@ -1261,6 +1457,10 @@ void ProcessCreatePlayerlistQuery(TConnection *Connection, TReadBuffer *Buffer){
return;
}
+ // TODO(fusion): I think `NumCharacters` may be used to signal that the
+ // server is going OFFLINE, in which case we'd have to add an `Online`
+ // column to `Worlds` and update it here.
+
bool NewRecord = false;
int NumCharacters = Buffer->Read16();
if(NumCharacters != 0xFFFF && NumCharacters > 0){
@@ -1293,7 +1493,38 @@ void ProcessCreatePlayerlistQuery(TConnection *Connection, TReadBuffer *Buffer){
}
void ProcessLogKilledCreaturesQuery(TConnection *Connection, TReadBuffer *Buffer){
- SendQueryStatusFailed(Connection);
+ if(Connection->ApplicationType != APPLICATION_TYPE_GAME){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ int NumStats = Buffer->Read16();
+ TKillStatistics *Stats = (TKillStatistics*)alloca(NumStats * sizeof(TKillStatistics));
+ for(int i = 0; i < NumStats; i += 1){
+ Buffer->ReadString(Stats[i].RaceName, sizeof(Stats[i].RaceName));
+ Stats[i].PlayersKilled = (int)Buffer->Read32();
+ Stats[i].TimesKilled = (int)Buffer->Read32();
+ }
+
+ if(NumStats > 0){
+ TransactionScope Tx("LogKilledCreatures");
+ if(!Tx.Begin()){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ if(!MergeKillStatistics(Connection->WorldID, NumStats, Stats)){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+
+ if(!Tx.Commit()){
+ SendQueryStatusFailed(Connection);
+ return;
+ }
+ }
+
+ SendQueryStatusOk(Connection);
}
void ProcessLoadPlayersQuery(TConnection *Connection, TReadBuffer *Buffer){
diff --git a/src/database.cc b/src/database.cc
index eb91dbc..3ba0201 100644
--- a/src/database.cc
+++ b/src/database.cc
@@ -198,6 +198,88 @@ bool GetWorldConfig(int WorldID, TWorldConfig *WorldConfig){
return true;
}
+bool GetAccountData(int AccountID, TAccountData *Account){
+ ASSERT(Account != NULL);
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT AccountID, Email, Auth, (PremiumEnd >= UNIXEPOCH()), PendingPremiumDays"
+ " FROM Accounts WHERE AccountID = ?1");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){
+ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ int ErrorCode = sqlite3_step(Stmt);
+ if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ memset(Account, 0, sizeof(TAccountData));
+ if(ErrorCode == SQLITE_ROW){
+ Account->AccountID = sqlite3_column_int(Stmt, 0);
+ StringCopy(Account->Email, sizeof(Account->Email),
+ (const char*)sqlite3_column_text(Stmt, 1));
+ if(sqlite3_column_bytes(Stmt, 2) == sizeof(Account->Auth)){
+ memcpy(Account->Auth, sqlite3_column_blob(Stmt, 2), sizeof(Account->Auth));
+ }
+ Account->Premium = (sqlite3_column_int(Stmt, 3) != 0);
+ Account->PendingPremiumDays = sqlite3_column_int(Stmt, 4);
+ }
+
+ return true;
+}
+
+int GetAccountOnlineCharacters(int AccountID){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT COUNT(*) FROM Characters"
+ " WHERE AccountID = ?1 AND IsOnline != 0");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return 0;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){
+ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(g_Database));
+ return 0;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_ROW){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return 0;
+ }
+
+ return sqlite3_column_int(Stmt, 0);
+}
+
+bool ActivatePendingPremiumDays(int AccountID){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "UPDATE Accounts"
+ " SET PremiumEnd = MAX(PremiumEnd, UNIXEPOCH()) + PendingPremiumDays * 86400,"
+ " PendingPremiumDays = 0"
+ " WHERE AccountID = ?1 AND PendingPremiumDays > 0");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){
+ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return true;
+}
+
int GetCharacterID(int WorldID, const char *CharacterName){
ASSERT(CharacterName != NULL);
sqlite3_stmt *Stmt = PrepareQuery(
@@ -223,6 +305,48 @@ int GetCharacterID(int WorldID, const char *CharacterName){
return (ErrorCode == SQLITE_ROW ? sqlite3_column_int(Stmt, 0) : 0);
}
+bool GetCharacterData(const char *CharacterName, TCharacterData *Character){
+ ASSERT(CharacterName != NULL && Character != NULL);
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT WorldID, CharacterID, AccountID, Name,"
+ " Sex, Guild, Rank, Title, Deleted"
+ " FROM Characters WHERE Name = ?1");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return 0;
+ }
+
+ if(sqlite3_bind_text(Stmt, 1, CharacterName, -1, NULL) != SQLITE_OK){
+ LOG_ERR("Failed to bind CharacterName: %s", sqlite3_errmsg(g_Database));
+ return 0;
+ }
+
+ int ErrorCode = sqlite3_step(Stmt);
+ if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return 0;
+ }
+
+ memset(Character, 0, sizeof(TCharacterData));
+ if(ErrorCode == SQLITE_ROW){
+ Character->WorldID = sqlite3_column_int(Stmt, 0);
+ Character->CharacterID = sqlite3_column_int(Stmt, 1);
+ Character->AccountID = sqlite3_column_int(Stmt, 2);
+ StringCopy(Character->Name, sizeof(Character->Name),
+ (const char*)sqlite3_column_text(Stmt, 3));
+ Character->Sex = sqlite3_column_int(Stmt, 4);
+ StringCopy(Character->Guild, sizeof(Character->Guild),
+ (const char*)sqlite3_column_text(Stmt, 5));
+ StringCopy(Character->Rank, sizeof(Character->Rank),
+ (const char*)sqlite3_column_text(Stmt, 6));
+ StringCopy(Character->Title, sizeof(Character->Title),
+ (const char*)sqlite3_column_text(Stmt, 7));
+ Character->Deleted = sqlite3_column_int(Stmt, 8);
+ }
+
+ return true;
+}
+
bool GetCharacterRight(int CharacterID, const char *Right){
ASSERT(Right != NULL);
sqlite3_stmt *Stmt = PrepareQuery(
@@ -248,6 +372,35 @@ bool GetCharacterRight(int CharacterID, const char *Right){
return (ErrorCode == SQLITE_ROW);
}
+bool GetCharacterRights(int CharacterID, DynamicArray<TCharacterRight> *Rights){
+ ASSERT(Rights != NULL);
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT Right FROM CharacterRights WHERE CharacterID = ?1");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, CharacterID) != SQLITE_OK){
+ LOG_ERR("Failed to bind CharacterID: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ while(sqlite3_step(Stmt) == SQLITE_ROW){
+ TCharacterRight Right = {};
+ StringCopy(Right.Name, sizeof(Right.Name),
+ (const char*)sqlite3_column_text(Stmt, 0));
+ Rights->Push(Right);
+ }
+
+ if(sqlite3_errcode(g_Database) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return true;
+}
+
bool GetGuildLeaderStatus(int WorldID, int CharacterID){
// NOTE(fusion): Same as `DecrementIsOnline`.
sqlite3_stmt *Stmt = PrepareQuery(
@@ -281,6 +434,30 @@ bool GetGuildLeaderStatus(int WorldID, int CharacterID){
return Result;
}
+bool IncrementIsOnline(int WorldID, int CharacterID){
+ // NOTE(fusion): Same as `DecrementIsOnline`
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "UPDATE Characters SET IsOnline = IsOnline + 1"
+ " WHERE WorldID = ?1 AND CharacterID = ?2");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return sqlite3_changes(g_Database) > 0;
+}
+
bool DecrementIsOnline(int WorldID, int CharacterID){
// NOTE(fusion): A character is uniquely identified by its id. The world id
// check is purely to avoid a world from modifying a character from another
@@ -331,6 +508,43 @@ bool ClearIsOnline(int WorldID, int *NumAffectedCharacters){
return true;
}
+bool LogoutCharacter(int WorldID, int CharacterID, int Level,
+ const char *Profession, const char *Residence, int LastLoginTime,
+ int TutorActivities){
+ ASSERT(Profession != NULL && Residence != NULL);
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "UPDATE Characters"
+ " SET Level = ?3,"
+ " Profession = ?4,"
+ " Residence = ?5,"
+ " LastLoginTime = ?6,"
+ " TutorActivities = ?7"
+ " IsOnline = IsOnline - 1"
+ " WHERE WorldID = ?1 AND CharacterID = ?2");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 3, Level) != SQLITE_OK
+ || sqlite3_bind_text(Stmt, 4, Profession, -1, NULL) != SQLITE_OK
+ || sqlite3_bind_text(Stmt, 5, Residence, -1, NULL) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 6, LastLoginTime) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 7, TutorActivities) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return sqlite3_changes(g_Database) > 0;
+}
+
bool GetCharacterIndexEntries(int WorldID, int MinimumCharacterID,
int MaxEntries, int *NumEntries, TCharacterIndexEntry *Entries){
ASSERT(MaxEntries > 0 && NumEntries != NULL && Entries != NULL);
@@ -357,7 +571,7 @@ bool GetCharacterIndexEntries(int WorldID, int MinimumCharacterID,
Entries[EntryIndex].CharacterID = sqlite3_column_int(Stmt, 0);
StringCopy(Entries[EntryIndex].Name,
sizeof(Entries[EntryIndex].Name),
- (const char *)sqlite3_column_text(Stmt, 1));
+ (const char*)sqlite3_column_text(Stmt, 1));
}
if(sqlite3_errcode(g_Database) != SQLITE_DONE){
@@ -371,6 +585,7 @@ bool GetCharacterIndexEntries(int WorldID, int MinimumCharacterID,
bool InsertCharacterDeath(int WorldID, int CharacterID, int Level,
int OffenderID, const char *Remark, bool Unjustified, int Timestamp){
+ ASSERT(Remark != NULL);
// NOTE(fusion): Same as `DecrementIsOnline`.
sqlite3_stmt *Stmt = PrepareQuery(
"INSERT INTO CharacterDeaths (CharacterID, Level,"
@@ -455,6 +670,135 @@ bool DeleteBuddy(int WorldID, int AccountID, int BuddyID){
return true;
}
+bool GetBuddies(int WorldID, int AccountID, DynamicArray<TAccountBuddy> *Buddies){
+ ASSERT(Buddies != NULL);
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT B.BuddyID, C.Name"
+ " FROM Buddies AS B"
+ " INNER JOIN Characters AS C"
+ " ON C.WorldID = B.WorldID AND C.CharacterID = B.BuddyID"
+ " WHERE B.WorldID = ?1 AND B.AccountID = ?2");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, AccountID) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ while(sqlite3_step(Stmt) == SQLITE_ROW){
+ TAccountBuddy Buddy = {};
+ Buddy.CharacterID = sqlite3_column_int(Stmt, 0);
+ StringCopy(Buddy.Name, sizeof(Buddy.Name),
+ (const char*)sqlite3_column_text(Stmt, 1));
+ Buddies->Push(Buddy);
+ }
+
+ if(sqlite3_errcode(g_Database) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return true;
+}
+
+bool GetWorldInvitation(int WorldID, int CharacterID){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT 1 FROM WorldInvitations"
+ " WHERE WorldID = ?1 AND CharacterID = ?2");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, CharacterID) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ int ErrorCode = sqlite3_step(Stmt);
+ if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return (ErrorCode == SQLITE_ROW);
+}
+
+bool InsertLoginAttempt(int AccountID, int IPAddress, bool Failed){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "INSERT INTO LoginAttempts (AccountID, IPAddress, Timestamp, Failed)"
+ " VALUES (?1, ?2, UNIXEPOCH(), ?4)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, IPAddress) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 3, (Failed ? 1 : 0)) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return true;
+}
+
+int GetAccountLoginAttempts(int AccountID, int TimeWindow){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT COUNT(*) FROM LoginAttempts"
+ " WHERE AccountID = ?1 AND Timestamp >= (UNIXEPOCH() - ?2)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return 0;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, TimeWindow) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_ROW){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return sqlite3_column_int(Stmt, 0);
+}
+
+int GetIPAddressLoginAttempts(int IPAddress, int TimeWindow){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT COUNT(*) FROM LoginAttempts"
+ " WHERE IPAddress = ?1 AND Timestamp >= (UNIXEPOCH() - ?2)");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return 0;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, IPAddress) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 2, TimeWindow) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_ROW){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return sqlite3_column_int(Stmt, 0);
+}
+
// House tables
//==============================================================================
bool FinishHouseAuctions(int WorldID, DynamicArray<THouseAuction> *Auctions){
@@ -848,6 +1192,11 @@ bool ExcludeFromAuctions(int WorldID, int CharacterID, int Duration, int Banishm
// Banishment tables
//==============================================================================
+bool IsCharacterNamelocked(int CharacterID){
+ TNamelockStatus Status = GetNamelockStatus(CharacterID);
+ return Status.Namelocked && !Status.Approved;
+}
+
TNamelockStatus GetNamelockStatus(int CharacterID){
TNamelockStatus Status = {};
sqlite3_stmt *Stmt = PrepareQuery(
@@ -903,6 +1252,30 @@ bool InsertNamelock(int CharacterID, int IPAddress, int GamemasterID,
return true;
}
+bool IsAccountBanished(int AccountID){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT 1 FROM Banishments"
+ " WHERE AccountID = ?1"
+ " AND (Until = Issued OR Until > UNIXEPOCH())");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, AccountID) != SQLITE_OK){
+ LOG_ERR("Failed to bind AccountID: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ int ErrorCode = sqlite3_step(Stmt);
+ if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return (ErrorCode == SQLITE_ROW);
+}
+
TBanishmentStatus GetBanishmentStatus(int CharacterID){
TBanishmentStatus Status = {};
sqlite3_stmt *Stmt = PrepareQuery(
@@ -1025,6 +1398,30 @@ bool InsertNotation(int CharacterID, int IPAddress, int GamemasterID,
return true;
}
+bool IsIPBanished(int IPAddress){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "SELECT 1 FROM IPBanishments"
+ " WHERE IPAddress = ?1"
+ " AND (Until = Issued OR Until > UNIXEPOCH())");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, IPAddress) != SQLITE_OK){
+ LOG_ERR("Failed to bind IPAddress: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ int ErrorCode = sqlite3_step(Stmt);
+ if(ErrorCode != SQLITE_ROW && ErrorCode != SQLITE_DONE){
+ LOG_ERR("Failed to execute query: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ return (ErrorCode == SQLITE_ROW);
+}
+
bool InsertIPBanishment(int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment, int Duration){
ASSERT(Reason != NULL && Comment != NULL);
@@ -1163,6 +1560,43 @@ bool InsertReportedStatement(int WorldID, TStatement *Statement, int BanishmentI
// Info tables
//==============================================================================
+bool MergeKillStatistics(int WorldID, int NumStats, TKillStatistics *Stats){
+ sqlite3_stmt *Stmt = PrepareQuery(
+ "INSERT INTO KillStatistics (WorldID, RaceName, TimesKilled, PlayersKilled)"
+ " VALUES (?1, ?2, ?3, ?4)"
+ " ON CONFLICT DO UPDATE SET TimesKilled = TimesKilled + Excluded.TimesKilled,"
+ " PlayersKilled = PlayersKilled + Excluded.TimesKilled");
+ if(Stmt == NULL){
+ LOG_ERR("Failed to prepare query");
+ return false;
+ }
+
+ if(sqlite3_bind_int(Stmt, 1, WorldID) != SQLITE_OK){
+ LOG_ERR("failed to bind WorldID: %s", sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ for(int i = 0; i < NumStats; i += 1){
+ if(sqlite3_bind_text(Stmt, 2, Stats[i].RaceName, -1, NULL) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 3, Stats[i].TimesKilled) != SQLITE_OK
+ || sqlite3_bind_int(Stmt, 4, Stats[i].PlayersKilled) != SQLITE_OK){
+ LOG_ERR("Failed to bind parameters for \"%s\" stats: %s",
+ Stats[i].RaceName, sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ if(sqlite3_step(Stmt) != SQLITE_DONE){
+ LOG_ERR("Failed to insert \"%s\" stats: %s",
+ Stats[i].RaceName, sqlite3_errmsg(g_Database));
+ return false;
+ }
+
+ sqlite3_reset(Stmt);
+ }
+
+ return true;
+}
+
bool DeleteOnlineCharacters(int WorldID){
sqlite3_stmt *Stmt = PrepareQuery(
"DELETE FROM OnlineCharacters WHERE WorldID = ?1");
diff --git a/src/querymanager.cc b/src/querymanager.cc
index dddb86f..26d2855 100644
--- a/src/querymanager.cc
+++ b/src/querymanager.cc
@@ -364,6 +364,10 @@ int main(int argc, const char **argv){
return EXIT_FAILURE;
}
+ if(!CheckSHA256()){
+ return EXIT_FAILURE;
+ }
+
atexit(ExitDatabase);
atexit(ExitConnections);
if(!InitDatabase() || !InitConnections()){
diff --git a/src/querymanager.hh b/src/querymanager.hh
index 5276d2c..ef37de1 100644
--- a/src/querymanager.hh
+++ b/src/querymanager.hh
@@ -140,6 +140,28 @@ inline uint32 BufferRead32BE(const uint8 *Buffer){
| (uint32)Buffer[3];
}
+inline uint64 BufferRead64LE(const uint8 *Buffer){
+ return (uint64)Buffer[0]
+ | ((uint64)Buffer[1] << 8)
+ | ((uint64)Buffer[2] << 16)
+ | ((uint64)Buffer[3] << 24)
+ | ((uint64)Buffer[4] << 32)
+ | ((uint64)Buffer[5] << 40)
+ | ((uint64)Buffer[6] << 48)
+ | ((uint64)Buffer[7] << 56);
+}
+
+inline uint64 BufferRead64BE(const uint8 *Buffer){
+ return ((uint64)Buffer[0] << 56)
+ | ((uint64)Buffer[1] << 48)
+ | ((uint64)Buffer[2] << 40)
+ | ((uint64)Buffer[3] << 32)
+ | ((uint64)Buffer[4] << 24)
+ | ((uint64)Buffer[5] << 16)
+ | ((uint64)Buffer[6] << 8)
+ | (uint64)Buffer[7];
+}
+
inline void BufferWrite8(uint8 *Buffer, uint8 Value){
Buffer[0] = Value;
}
@@ -168,6 +190,28 @@ inline void BufferWrite32BE(uint8 *Buffer, uint32 Value){
Buffer[3] = (uint8)(Value >> 0);
}
+inline void BufferWrite64LE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 0);
+ Buffer[1] = (uint8)(Value >> 8);
+ Buffer[2] = (uint8)(Value >> 16);
+ Buffer[3] = (uint8)(Value >> 24);
+ Buffer[4] = (uint8)(Value >> 32);
+ Buffer[5] = (uint8)(Value >> 40);
+ Buffer[6] = (uint8)(Value >> 48);
+ Buffer[7] = (uint8)(Value >> 56);
+}
+
+inline void BufferWrite64BE(uint8 *Buffer, uint64 Value){
+ Buffer[0] = (uint8)(Value >> 56);
+ Buffer[1] = (uint8)(Value >> 48);
+ Buffer[2] = (uint8)(Value >> 40);
+ Buffer[3] = (uint8)(Value >> 32);
+ Buffer[4] = (uint8)(Value >> 24);
+ Buffer[5] = (uint8)(Value >> 16);
+ Buffer[6] = (uint8)(Value >> 8);
+ Buffer[7] = (uint8)(Value >> 0);
+}
+
struct TReadBuffer{
uint8 *Buffer;
int Size;
@@ -644,7 +688,6 @@ void ProcessAddPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer);
void ProcessCancelPaymentNewQuery(TConnection *Connection, TReadBuffer *Buffer);
void ProcessConnectionQuery(TConnection *Connection);
-
// database.cc
//==============================================================================
struct TWorldConfig{
@@ -658,6 +701,36 @@ struct TWorldConfig{
int PremiumNewbieBuffer;
};
+struct TAccountData{
+ int AccountID;
+ char Email[100];
+ uint8 Auth[64];
+ bool Premium;
+ int PendingPremiumDays;
+ bool Deleted;
+};
+
+struct TAccountBuddy{
+ int CharacterID;
+ char Name[30];
+};
+
+struct TCharacterData{
+ int WorldID;
+ int CharacterID;
+ int AccountID;
+ char Name[30];
+ int Sex;
+ char Guild[30];
+ char Rank[30];
+ char Title[30];
+ bool Deleted;
+};
+
+struct TCharacterRight{
+ char Name[30];
+};
+
struct TCharacterIndexEntry{
char Name[30];
int CharacterID;
@@ -722,6 +795,12 @@ struct TStatement{
char Text[256];
};
+struct TKillStatistics{
+ char RaceName[30];
+ int TimesKilled;
+ int PlayersKilled;
+};
+
struct TOnlineCharacter{
char Name[30];
int Level;
@@ -744,17 +823,31 @@ public:
// NOTE(fusion): Primary tables.
int GetWorldID(const char *WorldName);
bool GetWorldConfig(int WorldID, TWorldConfig *WorldConfig);
+bool GetAccountData(int AccountID, TAccountData *Account);
+int GetAccountOnlineCharacters(int AccountID);
+bool ActivatePendingPremiumDays(int AccountID);
int GetCharacterID(int WorldID, const char *CharacterName);
+bool GetCharacterData(const char *CharacterName, TCharacterData *Character);
bool GetCharacterRight(int CharacterID, const char *Right);
+bool GetCharacterRights(int CharacterID, DynamicArray<TCharacterRight> *Rights);
bool GetGuildLeaderStatus(int WorldID, int CharacterID);
+bool IncrementIsOnline(int WorldID, int CharacterID);
bool DecrementIsOnline(int WorldID, int CharacterID);
bool ClearIsOnline(int WorldID, int *NumAffectedCharacters);
+bool LogoutCharacter(int WorldID, int CharacterID, int Level,
+ const char *Profession, const char *Residence, int LastLoginTime,
+ int TutorActivities);
bool GetCharacterIndexEntries(int WorldID, int MinimumCharacterID,
int MaxEntries, int *NumEntries, TCharacterIndexEntry *Entries);
bool InsertCharacterDeath(int WorldID, int CharacterID, int Level,
int OffenderID, const char *Remark, bool Unjustified, int Timestamp);
bool InsertBuddy(int WorldID, int AccountID, int BuddyID);
bool DeleteBuddy(int WorldID, int AccountID, int BuddyID);
+bool GetBuddies(int WorldID, int AccountID, DynamicArray<TAccountBuddy> *Buddies);
+bool GetWorldInvitation(int WorldID, int CharacterID);
+bool InsertLoginAttempt(int AccountID, int IPAddress, bool Failed);
+int GetAccountLoginAttempts(int AccountID, int TimeWindow);
+int GetIPAddressLoginAttempts(int IPAddress, int TimeWindow);
// NOTE(fusion): House tables.
bool FinishHouseAuctions(int WorldID, DynamicArray<THouseAuction> *Auctions);
@@ -772,9 +865,11 @@ bool InsertHouses(int WorldID, int NumHouses, THouse *Houses);
bool ExcludeFromAuctions(int WorldID, int CharacterID, int Duration, int BanishmentID);
// NOTE(fusion): Banishment tables.
+bool IsCharacterNamelocked(int CharacterID);
TNamelockStatus GetNamelockStatus(int CharacterID);
bool InsertNamelock(int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment);
+bool IsAccountBanished(int AccountID);
TBanishmentStatus GetBanishmentStatus(int CharacterID);
bool InsertBanishment(int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment, bool FinalWarning,
@@ -782,15 +877,16 @@ bool InsertBanishment(int CharacterID, int IPAddress, int GamemasterID,
int GetNotationCount(int CharacterID);
bool InsertNotation(int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment);
+bool IsIPBanished(int IPAddress);
bool InsertIPBanishment(int CharacterID, int IPAddress, int GamemasterID,
const char *Reason, const char *Comment, int Duration);
-
bool IsStatementReported(int WorldID, TStatement *Statement);
bool InsertStatements(int WorldID, int NumStatements, TStatement *Statements);
bool InsertReportedStatement(int WorldID, TStatement *Statement, int BanishmentID,
int ReporterID, const char *Reason, const char *Comment);
// NOTE(fusion): Info tables.
+bool MergeKillStatistics(int WorldID, int NumStats, TKillStatistics *Stats);
bool DeleteOnlineCharacters(int WorldID);
bool InsertOnlineCharacters(int WorldID, int NumCharacters, TOnlineCharacter *Characters);
bool CheckOnlineRecord(int WorldID, int NumCharacters, bool *NewRecord);
@@ -806,4 +902,10 @@ bool CheckDatabaseSchema(void);
bool InitDatabase(void);
void ExitDatabase(void);
+// sha256.cc
+//==============================================================================
+void SHA256(const uint8 *Input, int InputBytes, uint8 *Digest);
+bool TestPassword(const uint8 *Auth, int AuthSize, const char *Password);
+bool CheckSHA256(void);
+
#endif //TIBIA_QUERYMANAGER_HH_
diff --git a/src/sha256.cc b/src/sha256.cc
new file mode 100644
index 0000000..1070c6b
--- /dev/null
+++ b/src/sha256.cc
@@ -0,0 +1,247 @@
+#include "querymanager.hh"
+
+static const uint32 SHA256IV[8] = {
+ 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
+ 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19,
+};
+
+static const uint32 SHA256K[64] = {
+ 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
+ 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
+ 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
+ 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
+ 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,
+ 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
+ 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,
+ 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
+ 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
+ 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
+ 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,
+ 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
+ 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,
+ 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
+ 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
+ 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
+};
+
+static uint32 RotR32(uint32 Value, int N){
+ ASSERT(N >= 0 && N < 32);
+ return (Value >> N) | (Value << (32 - N));
+}
+
+static void SHA256Compress(uint32 *H, const uint8 *Block){
+ uint32 W[64];
+
+ for(int i = 0; i < 16; i += 1){
+ W[i] = BufferRead32BE(&Block[i * 4]);
+ }
+
+ for(int i = 16; i < 64; i += 1){
+ uint32 S0 = RotR32(W[i - 15], 7) ^ RotR32(W[i - 15], 18) ^ (W[i - 15] >> 3);
+ uint32 S1 = RotR32(W[i - 2], 17) ^ RotR32(W[i - 2], 19) ^ (W[i - 2] >> 10);
+ W[i] = W[i - 16] + S0 + W[i - 7] + S1;
+ }
+
+ uint32 Aux[8];
+ memcpy(Aux, H, sizeof(uint32) * 8);
+ for(int i = 0; i < 64; i += 1){
+ uint32 S1 = RotR32(Aux[4], 6) ^ RotR32(Aux[4], 11) ^ RotR32(Aux[4], 25);
+ uint32 Ch = (Aux[4] & Aux[5]) ^ (~Aux[4] & Aux[6]);
+ uint32 T1 = Aux[7] + S1 + Ch + SHA256K[i] + W[i];
+
+ uint32 S0 = RotR32(Aux[0], 2) ^ RotR32(Aux[0], 13) ^ RotR32(Aux[0], 22);
+ uint32 Maj = (Aux[0] & Aux[1]) ^ (Aux[0] & Aux[2]) ^ (Aux[1] & Aux[2]);
+ uint32 T2 = S0 + Maj;
+
+ Aux[7] = Aux[6];
+ Aux[6] = Aux[5];
+ Aux[5] = Aux[4];
+ Aux[4] = Aux[3] + T1;
+ Aux[3] = Aux[2];
+ Aux[2] = Aux[1];
+ Aux[1] = Aux[0];
+ Aux[0] = T1 + T2;
+ }
+
+ H[0] += Aux[0];
+ H[1] += Aux[1];
+ H[2] += Aux[2];
+ H[3] += Aux[3];
+ H[4] += Aux[4];
+ H[5] += Aux[5];
+ H[6] += Aux[6];
+ H[7] += Aux[7];
+}
+
+void SHA256(const uint8 *Input, int InputBytes, uint8 *Digest){
+ ASSERT(Input != NULL && InputBytes >= 0 && Digest != NULL);
+ uint32 H[8];
+ memcpy(H, SHA256IV, sizeof(uint32) * 8);
+
+ const uint8 *InputPtr = Input;
+ int InputRem = InputBytes;
+ while(InputRem >= 64){
+ SHA256Compress(H, InputPtr);
+ InputPtr += 64;
+ InputRem -= 64;
+ }
+
+ ASSERT(InputRem < 64);
+ uint8 Block[64] = {};
+ memcpy(Block, InputPtr, InputRem);
+ BufferWrite8(&Block[InputRem], 0x80);
+ if(InputRem > 55){
+ SHA256Compress(H, Block);
+ memset(Block, 0, sizeof(Block));
+ }
+ BufferWrite64BE(&Block[56], ((uint64)InputBytes * 8));
+ SHA256Compress(H, Block);
+
+ BufferWrite32BE(&Digest[ 0], H[0]);
+ BufferWrite32BE(&Digest[ 4], H[1]);
+ BufferWrite32BE(&Digest[ 8], H[2]);
+ BufferWrite32BE(&Digest[12], H[3]);
+ BufferWrite32BE(&Digest[16], H[4]);
+ BufferWrite32BE(&Digest[20], H[5]);
+ BufferWrite32BE(&Digest[24], H[6]);
+ BufferWrite32BE(&Digest[28], H[7]);
+}
+
+bool TestPassword(const uint8 *Auth, int AuthSize, const char *Password){
+ if(AuthSize != 64){
+ LOG_ERR("Expected 64 bytes of authentication data (got %d)", AuthSize);
+ return false;
+ }
+
+ // NOTE(fusion): Constant time comparison to check whether the authentication
+ // data is set. I'm considering all zeros to be NOT set.
+ bool IsSet = false;
+ for(int i = 0; i < AuthSize; i += 1){
+ if(Auth[i] != 0){
+ IsSet = true;
+ }
+ }
+
+ if(!IsSet){
+ LOG_ERR("Authentication data not set");
+ return false;
+ }
+
+ const uint8 *Hash = &Auth[ 0];
+ const uint8 *Salt = &Auth[32];
+
+ // TODO(fusion): It's probably not the best way to mix the salt but should
+ // be better than using plaintext or non-salted hashing schemes.
+ uint8 Digest[32];
+ SHA256((const uint8*)Password, (int)strlen(Password), Digest);
+ for(int i = 0; i < 32; i += 1){
+ Digest[i] ^= Salt[i];
+ }
+ SHA256(Digest, 32, Digest);
+
+ // NOTE(fusion): Constant time comparison.
+ uint8 Result = 0;
+ for(int i = 0; i < 32; i += 1){
+ Result |= Digest[i] ^ Hash[i];
+ }
+ return Result == 0;;
+}
+
+// CheckSHA256
+//==============================================================================
+static int HexDigit(int Ch){
+ if(Ch >= '0' && Ch <= '9'){
+ return (Ch - '0');
+ }else if(Ch >= 'A' && Ch <= 'F'){
+ return (Ch - 'A') + 10;
+ }else if(Ch >= 'a' && Ch <= 'f'){
+ return (Ch - 'a') + 10;
+ }else{
+ return -1;
+ }
+}
+
+static int ParseHexString(uint8 *Buffer, int BufferSize, const char *String){
+ int StringLen = (int)strlen(String);
+ if(StringLen % 2 != 0){
+ LOG_ERR("Expected even number of characters");
+ return -1;
+ }
+
+ int NumBytes = (StringLen / 2);
+ if(NumBytes > BufferSize){
+ LOG_ERR("Supplied buffer is too small (Size: %d, Required: %d)",
+ BufferSize, NumBytes);
+ return -1;
+ }
+
+ for(int i = 0; i < NumBytes; i += 1){
+ int Digit0 = HexDigit(String[i * 2 + 0]);
+ int Digit1 = HexDigit(String[i * 2 + 1]);
+ if(Digit0 == -1 || Digit1 == -1){
+ LOG_ERR("Invalid hex digit at offset %d", i * 2);
+ return -1;
+ }
+
+ Buffer[i] = ((uint8)Digit0 << 4) | (uint8)Digit1;
+ }
+
+ return NumBytes;
+}
+
+bool CheckSHA256(void){
+ // NOTE(fusion): We're using only a few NIST test vectors. This is to make
+ // sure there are no blatant implementation errors but we'd ideally run it
+ // against all of them to be sure.
+ struct{
+ const char *Input;
+ const char *Expected;
+ } Tests[] = {
+ {
+ "",
+ "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ },
+ {
+ "5738c929c4f4ccb6",
+ "963bb88f27f512777aab6c8b1a02c70ec0ad651d428f870036e1917120fb48bf",
+ },
+ {
+ "1b503fb9a73b16ada3fcf1042623ae7610",
+ "d5c30315f72ed05fe519a1bf75ab5fd0ffec5ac1acb0daf66b6b769598594509",
+ },
+ {
+ "09fc1accc230a205e4a208e64a8f204291f581a12756392da4b8c0cf5ef02b95",
+ "4f44c1c7fbebb6f9601829f3897bfd650c56fa07844be76489076356ac1886a4",
+ },
+ {
+ "03b264be51e4b941864f9b70b4c958f5355aac294b4b87cb037f11f85f07eb57"
+ "b3f0b89550",
+ "d1f8bd684001ac5a4b67bbf79f87de524d2da99ac014dec3e4187728f4557471",
+ },
+ {
+ "d1be3f13febafefc14414d9fb7f693db16dc1ae270c5b647d80da8583587c1ad"
+ "8cb8cb01824324411ca5ace3ca22e179a4ff4986f3f21190f3d7f3",
+ "02804978eba6e1de65afdbc6a6091ed6b1ecee51e8bff40646a251de6678b7ef",
+ }
+ };
+
+ uint8 Input[64];
+ uint8 Expected[32];
+ uint8 Digest[32];
+ for(int i = 0; i < NARRAY(Tests); i += 1){
+ int InputBytes = ParseHexString(Input, sizeof(Input), Tests[i].Input);
+ int ExpectedBytes = ParseHexString(Expected, sizeof(Expected), Tests[i].Expected);
+ if(InputBytes == -1 || ExpectedBytes != sizeof(Expected)){
+ LOG_ERR("Invalid test vector %d", i);
+ return false;
+ }
+
+ SHA256(Input, InputBytes, Digest);
+ if(memcmp(Expected, Digest, 32) != 0){
+ LOG_ERR("Test vector %d failed", i);
+ return false;
+ }
+ }
+
+ return true;
+}