diff --git a/TShockAPI/ConfigFile.cs b/TShockAPI/ConfigFile.cs index 24c1b575..43f4a6f3 100755 --- a/TShockAPI/ConfigFile.cs +++ b/TShockAPI/ConfigFile.cs @@ -398,6 +398,9 @@ namespace TShockAPI [Description("Determines if the server should save the world if the last player exits.")] public bool SaveWorldOnLastPlayerExit = true; + [Description("Determines the BCrypt work factor to use. If increased, all passwords will be upgraded to new work-factor on verify. Range: 5-31.")] + public int WorkFactor = 10; + /// /// Reads a configuration file from a given path /// diff --git a/TShockAPI/DB/UserManager.cs b/TShockAPI/DB/UserManager.cs index b58bca11..5b920ba1 100755 --- a/TShockAPI/DB/UserManager.cs +++ b/TShockAPI/DB/UserManager.cs @@ -23,6 +23,7 @@ using System.Collections.Generic; using System.Linq; using MySql.Data.MySqlClient; using System.Text.RegularExpressions; +using BCrypt.Net; namespace TShockAPI.DB { @@ -309,32 +310,101 @@ namespace TShockAPI.DB public int ID { get; set; } public string Name { get; set; } public string Password { get; set; } - public string UUID { get; set; } + public string UUID { get; set; } public string Group { get; set; } public string Registered { get; set; } - public string LastAccessed { get; set; } - public string KnownIps { get; set; } + public string LastAccessed { get; set; } + public string KnownIps { get; set; } public User(string name, string pass, string uuid, string group, string registered, string last, string known) { Name = name; Password = pass; - UUID = uuid; + UUID = uuid; Group = group; Registered = registered; - LastAccessed = last; - KnownIps = known; + LastAccessed = last; + KnownIps = known; } public User() { Name = ""; Password = ""; - UUID = ""; + UUID = ""; Group = ""; Registered = ""; - LastAccessed = ""; - KnownIps = ""; + LastAccessed = ""; + KnownIps = ""; + } + + /// + /// Verifies if a password matches the one stored in the database. + /// If the password is stored in an unsafe hashing algorithm, it will be converted to BCrypt. + /// If the password is stored using BCrypt, it will be re-saved if the work factor in the config + /// is greater than the existing work factor with the new work factor. + /// + /// string password - The password to check against the user object. + /// bool true, if the password matched, or false, if it didn't. + public bool VerifyPassword(string password) { + try { + if (BCrypt.Net.BCrypt.Verify(password, this.Password)) { + // If necessary, perform an upgrade to the highest work factor. + upgradePasswordWorkFactor(password); + return true; + } + } catch (SaltParseException) { + if (TShock.Utils.HashPassword(password).ToUpper() == this.Password.ToUpper()) { + // The password is not stored using BCrypt; upgrade it to BCrypt immediately + upgradePasswordToBCrypt(password); + return true; + } + return false; + } + return false; + } + + /// Upgrades a password to BCrypt, from an insecure hashing algorithm. + /// string password - the raw user password (unhashed) to upgrade + internal void upgradePasswordToBCrypt(string password) { + // Save the old password, in the event that we have to revert changes. + string oldpassword = this.Password; + + // Convert the password to BCrypt, and save it. + try { + this.Password = BCrypt.Net.BCrypt.HashPassword(password, TShock.Config.WorkFactor); + } catch (ArgumentOutOfRangeException) { + TShock.Log.ConsoleError("Invalid BCrypt work factor! Upgrading user password to BCrypt using default work factor."); + this.Password = BCrypt.Net.BCrypt.HashPassword(password); + } + + try { + TShock.Users.SetUserPassword(this, this.Password); + } catch (UserManagerException e) { + TShock.Log.ConsoleError(e.ToString()); + this.Password = oldpassword; // Revert changes + } + } + + /// Upgrades a password to the highest work factor available in the config. + /// string password - the raw user password (unhashed) to upgrade + internal void upgradePasswordWorkFactor(string password) { + // If the destination work factor is not greater, we won't upgrade it or re-hash it + int currentWorkFactor = Convert.ToInt32(this.Password.Split(new Char[] {'$'})[1]); + + if (currentWorkFactor < TShock.Config.WorkFactor) { + try { + this.Password = BCrypt.Net.BCrypt.HashPassword(password, TShock.Config.WorkFactor); + } catch (ArgumentOutOfRangeException) { + TShock.Log.ConsoleError("Invalid BCrypt work factor! Refusing to change work-factor on exsting password."); + } + + try { + TShock.Users.SetUserPassword(this, this.Password); + } catch (UserManagerException e) { + TShock.Log.ConsoleError(e.ToString()); + } + } } }