diff --git a/Server/LoggerMessages/LoginHandlerLoggerMessages.cs b/Server/LoggerMessages/LoginHandlerLoggerMessages.cs new file mode 100644 index 0000000..adefe33 --- /dev/null +++ b/Server/LoggerMessages/LoginHandlerLoggerMessages.cs @@ -0,0 +1,15 @@ +using Microsoft.Extensions.Logging; +using Server.PacketHandlers; + +namespace Server.LoggerMessages; + +public static partial class LoginHandlerLoggerMessages +{ + [LoggerMessage(EventId = 0, Level = LogLevel.Information, + Message = "Login data: Username {Username} & Password {Password}")] + public static partial void LoginData(this ILogger logger, string username, string password); + + [LoggerMessage(EventId = 1, Level = LogLevel.Information, + Message = "Requested account for user: {Username} does not exist")] + public static partial void RequestedAccountDoesNotExist(this ILogger logger, string username); +} diff --git a/Server/PacketHandlers/LoginHandler.cs b/Server/PacketHandlers/LoginHandler.cs index e1ec019..c56c977 100644 --- a/Server/PacketHandlers/LoginHandler.cs +++ b/Server/PacketHandlers/LoginHandler.cs @@ -1,5 +1,6 @@ using System.Security.Cryptography; using System.Text; +using Server.LoggerMessages; using Wonderking.Packets.Incoming; using Wonderking.Packets.Outgoing; @@ -25,63 +26,79 @@ public class LoginHandler : IPacketHandler this._configuration = configuration; } - public async Task HandleAsync(LoginInfoPacket packet, TcpSession session) + private static Task GetPasswordHashAsync(string password, byte[] salt, Guid userId) { - LoginResponseReason loginResponseReason; - this._logger.LogInformation("Login data: Username {Username} & Password {Password}", packet.Username, - packet.Password); - var account = this._wonderkingContext.Accounts.FirstOrDefault(a => a.Username == packet.Username); - // https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Chea1t_Sheet.html#argon2id // "Use Argon2id with a minimum configuration of 19 MiB of memory, an iteration count of 2, and 1 degree of parallelism." - var argon2Id = new Argon2id(Encoding.ASCII.GetBytes(packet.Password)) + var argon2Id = new Argon2id(Encoding.ASCII.GetBytes(password)) { MemorySize = 1024 * 19, Iterations = 2, - DegreeOfParallelism = 1 + DegreeOfParallelism = 1, + Salt = salt, + AssociatedData = userId.ToByteArray() }; + return argon2Id.GetBytesAsync(16); + } + + private async Task CreateAccountOnLoginAsync(string username, string password) + { + LoginResponseReason loginResponseReason; + var transaction = + await _wonderkingContext.Database.BeginTransactionAsync().ConfigureAwait(true); + await using (transaction.ConfigureAwait(false)) + { + try + { + var salt = RandomNumberGenerator.GetBytes(16); + var finalAccount = + await this._wonderkingContext.Accounts.AddAsync(new Account(username, + Array.Empty(), "", + 0, salt)).ConfigureAwait(true); + await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true); + finalAccount.Entity.Password = + await LoginHandler.GetPasswordHashAsync(password, salt, finalAccount.Entity.Id) + .ConfigureAwait(true); + this._wonderkingContext.Accounts.Update(finalAccount.Entity); + loginResponseReason = LoginResponseReason.Ok; + await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true); + + await transaction.CommitAsync().ConfigureAwait(true); + } + catch (Exception) + { + await transaction.RollbackAsync().ConfigureAwait(true); // Rollback the transaction on error + throw; + } + } + + return loginResponseReason; + } + + public async Task HandleAsync(LoginInfoPacket packet, TcpSession session) + { + LoginResponseReason loginResponseReason; + this._logger.LoginData(packet.Username, packet.Password); + var account = this._wonderkingContext.Accounts.FirstOrDefault(a => a.Username == packet.Username); + if (account == null) { if (this._configuration.GetSection("Testing").GetValue("CreateAccountOnLogin")) { - var transaction = - await _wonderkingContext.Database.BeginTransactionAsync().ConfigureAwait(true); - await using (transaction.ConfigureAwait(true)) - { - try - { - argon2Id.Salt = RandomNumberGenerator.GetBytes(16); - var finalAccount = - await this._wonderkingContext.Accounts.AddAsync(new Account(packet.Username, - Array.Empty(), "", - 0, argon2Id.Salt)).ConfigureAwait(true); - await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true); - argon2Id.AssociatedData = finalAccount.Entity.Id.ToByteArray(); - finalAccount.Entity.Password = await argon2Id.GetBytesAsync(16).ConfigureAwait(true); - this._wonderkingContext.Accounts.Update(finalAccount.Entity); - loginResponseReason = LoginResponseReason.Ok; - await this._wonderkingContext.SaveChangesAsync().ConfigureAwait(true); - - await transaction.CommitAsync().ConfigureAwait(true); - } - catch (Exception) - { - await transaction.RollbackAsync().ConfigureAwait(true); // Rollback the transaction on error - throw; - } - } + loginResponseReason = await CreateAccountOnLoginAsync(packet.Username, packet.Password) + .ConfigureAwait(true); } else { - this._logger.LogInformation("Requested account for user: {Username} does not exist", packet.Username); + this._logger.RequestedAccountDoesNotExist(packet.Username); loginResponseReason = LoginResponseReason.AccountDoesNotExit; } } else { - argon2Id.Salt = account.Salt; - argon2Id.AssociatedData = account.Id.ToByteArray(); - var tempPasswordBytes = await argon2Id.GetBytesAsync(16).ConfigureAwait(true); + var salt = account.Salt; + var tempPasswordBytes = await LoginHandler.GetPasswordHashAsync(packet.Password, salt, account.Id) + .ConfigureAwait(false); loginResponseReason = tempPasswordBytes.SequenceEqual(account.Password) ? LoginResponseReason.Ok : LoginResponseReason.WrongPassword; @@ -100,6 +117,7 @@ public class LoginHandler : IPacketHandler sess.AccountId = account.Id; } + _logger.LogInformation("LoginResponsePacket: {@LoginResponsePacket}", loginResponsePacket); sess?.Send(loginResponsePacket); } }