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());
+ }
+ }
}
}