diff options
| author | fusion32 <marcopuzziello@gmail.com> | 2025-07-15 23:37:52 -0300 |
|---|---|---|
| committer | fusion32 <marcopuzziello@gmail.com> | 2025-07-15 23:37:52 -0300 |
| commit | 5463c34177066b9860c9d77c6d7d59c265052399 (patch) | |
| tree | 828956ca1a0f04673d55e00554f909dd66ac4c36 /src | |
| parent | 8bc672f7111287e6e0b66e5897e80795652dc1c7 (diff) | |
| download | querymanager-5463c34177066b9860c9d77c6d7d59c265052399.tar.gz querymanager-5463c34177066b9860c9d77c6d7d59c265052399.zip | |
finish game queries
Diffstat (limited to 'src')
| -rw-r--r-- | src/connections.cc | 237 | ||||
| -rw-r--r-- | src/database.cc | 436 | ||||
| -rw-r--r-- | src/querymanager.cc | 4 | ||||
| -rw-r--r-- | src/querymanager.hh | 106 | ||||
| -rw-r--r-- | src/sha256.cc | 247 |
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; +} |
