diff --git a/SacsStandardBase/Base/Dbas/ISacsDatabase.cs b/SacsStandardBase/Base/Dbas/ISacsDatabase.cs index 2ebba41..405b0fe 100644 --- a/SacsStandardBase/Base/Dbas/ISacsDatabase.cs +++ b/SacsStandardBase/Base/Dbas/ISacsDatabase.cs @@ -77,14 +77,6 @@ namespace Sarand.Net.Standard.Base.Dbas // Database info - schema summary. IDatabaseInfo CreateDbInfo(); - //------------------------------------------------------------------- - // Password utilities. - public enum PasswordHashMethod { MsSql } - /// Generate password hash using the specified method. - String EncodePassword_Hex(String password, PasswordHashMethod method = PasswordHashMethod.MsSql); - /// Compare password to hashed value using the specified method. - bool ComparePassword_Hex(String password, String passwordHashHex, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql); - //------------------------------------------------------------------- // Query records. String GetSelectQueryPrefix(); diff --git a/SacsStandardBase/Base/Security/SaSecurityConsole.cs b/SacsStandardBase/Base/Security/SaSecurityConsole.cs index f2a45f5..bc98aca 100644 --- a/SacsStandardBase/Base/Security/SaSecurityConsole.cs +++ b/SacsStandardBase/Base/Security/SaSecurityConsole.cs @@ -160,12 +160,12 @@ namespace Sarand.Net.Standard.Base.Security /// Store the password against against the user. public abstract bool SetPassword(SaSecurityItem securityItemUser, String password); - /// Does the password match that currently defined against the user. - public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password); - /// Is the password currently defined against the user blank. public abstract bool PasswordIsBlank(SaSecurityItem securityItemUser); + /// Does the password match that currently defined against the user. + public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password, DbUpdateInfo dbUpdateInfo); + /// Show form to change password for currently logged in user. /// Flag to indicate password changed public abstract bool ShowChangePasswordForm(DbUpdateInfo dbUpdateInfo); diff --git a/SacsStandardBase/Base/Security/SaSecurityItem.cs b/SacsStandardBase/Base/Security/SaSecurityItem.cs index 1248b74..c4b71a7 100644 --- a/SacsStandardBase/Base/Security/SaSecurityItem.cs +++ b/SacsStandardBase/Base/Security/SaSecurityItem.cs @@ -305,32 +305,32 @@ namespace Sarand.Net.Standard.Base.Security } // Security. - [Obsolete("Avoid using the original C++ password encryption - switch to hashed password instead.")] - public String PasswordEncrypted - { - get { return data.PasswordEncrypted; } - set { data.PasswordEncrypted = value; } - } public String PasswordSalt { get { return data.PasswordSalt; } - set { data.PasswordSalt = value; } + protected set { data.PasswordSalt = value; } } public String PasswordHash { get { return data.PasswordHash; } - set { data.PasswordHash = value; } + protected set { data.PasswordHash = value; } } - public List PasswordHistory + [Obsolete("Avoid using the original C++ password encryption - switch to hashed password instead.")] + public String PasswordEncrypted { - get { return data.PasswordHistory; } - set { data.PasswordHistory = value; } + get { return data.PasswordEncrypted; } + protected set { data.PasswordEncrypted = value; } } public String PasswordToken { get { return data.PasswordToken; } set { data.PasswordToken = value; } } + public List PasswordHistory + { + get { return data.PasswordHistory; } + set { data.PasswordHistory = value; } + } public DateTime PasswordChangedAt { get { return data.PasswordChangedAt; } @@ -368,6 +368,8 @@ namespace Sarand.Net.Standard.Base.Security /// Extract password storage method from data. public SaSecurity.PasswordMethod GetPasswordMethod() { + if (PasswordSalt == PasswordSalt_MsSql && !StringUtil.IsBlank(PasswordHash)) + return SaSecurity.PasswordMethod.MsSql; return SaSecurity.PasswordMethod.Legacy; } diff --git a/SacsStandardBase/Base/Security/SaSecurityMgr.cs b/SacsStandardBase/Base/Security/SaSecurityMgr.cs index 34e91f7..a6e7b00 100644 --- a/SacsStandardBase/Base/Security/SaSecurityMgr.cs +++ b/SacsStandardBase/Base/Security/SaSecurityMgr.cs @@ -35,16 +35,23 @@ namespace Sarand.Net.Standard.Base.Security //------------------------------------------------------------------- // Password utilities. + public enum PasswordHashMethod { MsSql } /// Store the password against against the user. public abstract bool SetPassword(SaSecurityItem securityItemUser, String password); /// Does the password match that currently defined against the user. - public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password); + public abstract bool PasswordMatches(SaSecurityItem securityItemUser, String password, DbUpdateInfo dbUpdateInfo); /// Is the password currently defined against the user blank. public abstract bool PasswordIsBlank(SaSecurityItem securityItemUser); + /// Generate password hash using the specified method. + public abstract String EncodePassword_Hex(String password, PasswordHashMethod method = PasswordHashMethod.MsSql); + + /// Compare password to hashed value using the specified method. + public abstract bool ComparePassword_Hex(String password, String passwordHashHex, PasswordHashMethod method = PasswordHashMethod.MsSql); + //------------------------------------------------------------------- } //======================================================================= diff --git a/SacsStandardDbas/Dbas/Base/SacsDatabase.cs b/SacsStandardDbas/Dbas/Base/SacsDatabase.cs index 59620c1..06ee490 100644 --- a/SacsStandardDbas/Dbas/Base/SacsDatabase.cs +++ b/SacsStandardDbas/Dbas/Base/SacsDatabase.cs @@ -61,83 +61,6 @@ namespace Sarand.Net.Standard.Dbas.Base public abstract int GetNextIdFromTable(string objectName, string idColumn = "Id"); public abstract bool SetMinNextId(String objectName, SysId id, DbUpdateInfo dbUpdateInfo, bool databaseTable = true, String idColumn = "Id", bool force = false); - //------------------------------------------------------------------- - // Password utilities. - //------------------------------------------------------------------- - /// - /// Generate password hash using the specified method. - /// - Returns hex notation as a string. - /// - /// - /// String of hex digits corresponding to encrypted password. - public String EncodePassword_Hex(String password, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql) - { - ClearErrorState(); - if (method == ISacsDatabase.PasswordHashMethod.MsSql) - { - if (DbmsType == DbmsType.MsSql) - { - try - { - String colAlias = "DataValue"; - String query = $"Select {colAlias}=Convert(VarChar(2000),PwdEncrypt('{DbQuery.FormatForCondition(password)}'),2);"; - using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query)) - { - if (dbResultSet.NextRecord()) - return dbResultSet.GetRecordValues().GetValue(colAlias).ToString(); - else if (dbResultSet.HasErrorState()) - SetErrorState(dbResultSet.ErrorStateMessage); - } - } - catch (Exception exc) - { - SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "hashing password")}", true); - SetErrorState(exc.Message); - } - } - else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password by method 'SQL'. Database is '{DbmsType}'."); - } - else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password. Unrecognised method: '{method}'."); - return ""; - } - - /// - /// Compare the password to the encrypted hash using the specified method. - /// - /// - /// - /// True if the password matches the encrypted hash value. - public bool ComparePassword_Hex(String password, String passwordHashHex, ISacsDatabase.PasswordHashMethod method = ISacsDatabase.PasswordHashMethod.MsSql) - { - ClearErrorState(); - if (method == ISacsDatabase.PasswordHashMethod.MsSql) - { - if (DbmsType == DbmsType.MsSql) - { - try - { - String colAlias = "PwdCompare"; - String query = $"Select {colAlias}=PwdCompare('{DbQuery.FormatForCondition(password)}', Convert(VarBinary(1000),'{DbQuery.FormatForCondition(passwordHashHex)}',2));"; - using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(this, query)) - { - if (dbResultSet.NextRecord()) - return StringUtil.ToBool(dbResultSet.GetRecordValues().GetValue(colAlias).ToString()); - else if (dbResultSet.HasErrorState()) - SetErrorState(dbResultSet.ErrorStateMessage); - } - } - catch (Exception exc) - { - SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "comparing password")}", true); - SetErrorState(exc.Message); - } - } - else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password by method 'SQL'. Database is '{DbmsType}'."); - } - else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password. Unrecognised method: '{method}'."); - return false; - } - //------------------------------------------------------------------- // O D B C D r i v e r s //------------------------------------------------------------------- diff --git a/SacsStandardDbas/Dbas/Security/SaSecurityConsole_Dbas.cs b/SacsStandardDbas/Dbas/Security/SaSecurityConsole_Dbas.cs index e25b3ae..45962a9 100644 --- a/SacsStandardDbas/Dbas/Security/SaSecurityConsole_Dbas.cs +++ b/SacsStandardDbas/Dbas/Security/SaSecurityConsole_Dbas.cs @@ -7,6 +7,7 @@ using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Security; using Sarand.Net.Standard.Dbas.Utils; using Sarand.Net.Standard.Base.Utils; +using static Sarand.Net.Standard.Base.Security.SaSecurityMgr; namespace Sarand.Net.Standard.Dbas.Security { @@ -83,9 +84,9 @@ namespace Sarand.Net.Standard.Dbas.Security } /// Does the password match that currently defined against the user. - public override bool PasswordMatches(SaSecurityItem securityItemUser, String password) + public override bool PasswordMatches(SaSecurityItem securityItemUser, String password, DbUpdateInfo dbUpdateInfo) { - return securityMgr.PasswordMatches(securityItemUser, password); + return securityMgr.PasswordMatches(securityItemUser, password, dbUpdateInfo); } /// Is the password currently defined against the user blank. @@ -101,6 +102,18 @@ namespace Sarand.Net.Standard.Dbas.Security throw new NotImplementedException(); } + /// Generate password hash using the specified method. + public String EncodePassword_Hex(String password, PasswordHashMethod method = PasswordHashMethod.MsSql) + { + return securityMgr.EncodePassword_Hex(password, method); + } + + /// Compare password to hashed value using the specified method. + public bool ComparePassword_Hex(String password, String passwordHashHex, PasswordHashMethod method = PasswordHashMethod.MsSql) + { + return securityMgr.ComparePassword_Hex(password, passwordHashHex, method); + } + //------------------------------------------------------------------- // A p p l i c a t i o n S e c u r i t y R i g h t s //------------------------------------------------------------------- diff --git a/SacsStandardDbas/Dbas/Security/SaSecurityItem_Dbas.cs b/SacsStandardDbas/Dbas/Security/SaSecurityItem_Dbas.cs index 75909f4..2f1fe04 100644 --- a/SacsStandardDbas/Dbas/Security/SaSecurityItem_Dbas.cs +++ b/SacsStandardDbas/Dbas/Security/SaSecurityItem_Dbas.cs @@ -1,5 +1,6 @@ using Sarand.Net.Standard.Base.Bus; using Sarand.Net.Standard.Base.Security; +using System; namespace Sarand.Net.Standard.Dbas.Security { @@ -14,6 +15,20 @@ namespace Sarand.Net.Standard.Dbas.Security Flags = flags; } + public void SetPasswordSalt(String passwordSalt) + { + base.PasswordSalt = passwordSalt; + } + public void SetPasswordHash(String passwordHash) + { + base.PasswordHash = passwordHash; + } + [Obsolete("Avoid using the original C++ password encryption - switch to hashed password instead.")] + public void SetPasswordEncrypted(String passwordEncrypted) + { + base.PasswordEncrypted = passwordEncrypted; + } + //------------------------------------------------------------------- } //======================================================================= diff --git a/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas-SecurityItem.cs b/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas-SecurityItem.cs index 6edb43b..0ca0d2f 100644 --- a/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas-SecurityItem.cs +++ b/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas-SecurityItem.cs @@ -156,7 +156,7 @@ namespace Sarand.Net.Standard.Dbas.Security public SaSecurityItem CreateSecurityItem(SaSecItem_Db dbSecurityItem) { ClearErrorState(); - SaSecurityItem securityItem = new SaSecurityItem_Dbas(new SysId(dbSecurityItem._Id), (SaSecurity.ItemType)dbSecurityItem._Type, (ushort)dbSecurityItem._Flags); + SaSecurityItem_Dbas securityItem = new SaSecurityItem_Dbas(new SysId(dbSecurityItem._Id), (SaSecurity.ItemType)dbSecurityItem._Type, (ushort)dbSecurityItem._Flags); PopulateFrom(dbSecurityItem, securityItem); return securityItem; } @@ -278,11 +278,11 @@ namespace Sarand.Net.Standard.Dbas.Security dbSecurityItem._Code = securityItem.Code; dbSecurityItem._Description = securityItem.Description; dbSecurityItem._Location = securityItem.Location; + dbSecurityItem._PwordSalt = securityItem.PasswordSalt; + dbSecurityItem._PwordHash = securityItem.PasswordHash; #pragma warning disable CS0618 // Deprecated property dbSecurityItem._EncPword = securityItem.PasswordEncrypted; #pragma warning restore CS0618 // Deprecated property - dbSecurityItem._PwordSalt = securityItem.PasswordSalt; - dbSecurityItem._PwordHash = securityItem.PasswordHash; dbSecurityItem._LastLogin = securityItem.LastLogin; dbSecurityItem._DataItemType = (short)securityItem.DataItemType; // Common properties. @@ -299,18 +299,18 @@ namespace Sarand.Net.Standard.Dbas.Security dbSecurityItem._UpdateBy = dbUpdateInfo.UserId.ToInt(); dbSecurityItem._UpdateHost = dbUpdateInfo.Host; } - private void PopulateFrom(SaSecItem_Db dbSecurityItem, SaSecurityItem securityItem) + private void PopulateFrom(SaSecItem_Db dbSecurityItem, SaSecurityItem_Dbas securityItem) { //securityItem.Id = new SysId(dbSecurityItem._Id); securityItem.Code = dbSecurityItem._Code; securityItem.EmailAddress = dbSecurityItem._EmailAddress; securityItem.Description = dbSecurityItem._Description; securityItem.Location = dbSecurityItem._Location; + securityItem.SetPasswordSalt(dbSecurityItem._PwordSalt); + securityItem.SetPasswordHash(dbSecurityItem._PwordHash); #pragma warning disable CS0618 // Deprecated property - securityItem.PasswordEncrypted = dbSecurityItem._EncPword; + securityItem.SetPasswordEncrypted(dbSecurityItem._EncPword); #pragma warning restore CS0618 // Deprecated property - securityItem.PasswordSalt = dbSecurityItem._PwordSalt; - securityItem.PasswordHash = dbSecurityItem._PwordHash; securityItem.LastLogin = dbSecurityItem._LastLogin; securityItem.DataItemType = dbSecurityItem._DataItemType; // Common properties. diff --git a/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas.cs b/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas.cs index 6ca2266..6d8a1ac 100644 --- a/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas.cs +++ b/SacsStandardDbas/Dbas/Security/SaSecurityMgr_Dbas.cs @@ -5,6 +5,7 @@ using System.Text; using Sarand.Net.Standard.Base.App; using Sarand.Net.Standard.Base.Dbas; using Sarand.Net.Standard.Base.Security; +using Sarand.Net.Standard.Base.Utils; using Sarand.Net.Standard.Dbas.Data; namespace Sarand.Net.Standard.Dbas.Security @@ -44,43 +45,64 @@ namespace Sarand.Net.Standard.Dbas.Security //------------------------------------------------------------------- // Password utilities. - public override bool PasswordIsBlank(SaSecurityItem securityItemUser) - { - if (securityItemUser == null) return false; - SaSecurity.PasswordMethod method = securityItemUser.GetPasswordMethod(); - // MsSql method. - if (method == SaSecurity.PasswordMethod.MsSql) - { - //... - } - // Legacy. -#pragma warning disable CS0618 // Deprecated property - return EncryptPassword_Legacy("").Equals(securityItemUser.PasswordEncrypted); -#pragma warning restore CS0618 // Deprecated property - } - - public override bool PasswordMatches(SaSecurityItem securityItemUser, String password) - { - if (securityItemUser == null) return false; - SaSecurity.PasswordMethod method = securityItemUser.GetPasswordMethod(); - // MsSql method. - if (method == SaSecurity.PasswordMethod.MsSql) - { - //... - } - // Legacy. -#pragma warning disable CS0618 // Deprecated property - return (EncryptPassword_Legacy(password).Equals(securityItemUser.PasswordEncrypted)); -#pragma warning restore CS0618 // Deprecated property - } - /// Store the password against against the user. public override bool SetPassword(SaSecurityItem securityItemUser, String password) { + SaSecurity.PasswordMethod method = SaSecurity.PasswordMethod.Legacy; + + // If the Password Salt/Hash columns exist, attempt to encode password using non-legacy method. + if (ColumnExists(SaSecurityDbDfn.ColumnIdx.SaSecItem_PwordSalt) && ColumnExists(SaSecurityDbDfn.ColumnIdx.SaSecItem_PwordHash)) + { + // NB Do not use Microsoft.AspNetCore.Identity.PasswordHasher.HashPassword() while legacy (C++) desktop application still in use. + // Encode password using MS SQL utilities. + String passwordHashHex_MsSql = EncodePassword_Hex(password, PasswordHashMethod.MsSql); + if (!StringUtil.IsBlank(passwordHashHex_MsSql) && ComparePassword_Hex(password, passwordHashHex_MsSql, PasswordHashMethod.MsSql)) + { + // MS SQL encoded password is valid. + securityItemUser.SetEncodedPassword(SaSecurity.PasswordMethod.MsSql, passwordHashHex_MsSql); + method = SaSecurity.PasswordMethod.MsSql; + } + } + + if (method == SaSecurity.PasswordMethod.Legacy) + { + // Default to legacy method. + securityItemUser.SetEncodedPassword(SaSecurity.PasswordMethod.Legacy, EncodePassword_Legacy(password)); + } return false; } - internal static String EncryptPassword_Legacy(String password) + /// Is the password currently defined against the user blank. + public override bool PasswordIsBlank(SaSecurityItem securityItemUser) + { + return PasswordMatches(securityItemUser, "", null); + } + + /// Does the password match that currently defined against the user. + public override bool PasswordMatches(SaSecurityItem securityItemUser, String password, DbUpdateInfo dbUpdateInfo) + { + if (securityItemUser == null) return false; + SaSecurity.PasswordMethod method = securityItemUser.GetPasswordMethod(); + if (method == SaSecurity.PasswordMethod.MsSql) + { + // MsSql method. + return ComparePassword_Hex(password, securityItemUser.PasswordHash, PasswordHashMethod.MsSql); + } + + // Legacy method. +#pragma warning disable CS0618 // Deprecated property + bool matches = (EncodePassword_Legacy(password).Equals(securityItemUser.PasswordEncrypted)); +#pragma warning restore CS0618 // Deprecated property + if (matches && !StringUtil.IsBlank(password)) + { + SetPassword(securityItemUser, password); + if (securityItemUser.GetPasswordMethod() != SaSecurity.PasswordMethod.Legacy) + UpdateSecurityItem(securityItemUser, dbUpdateInfo); + } + return matches; + } + + internal static String EncodePassword_Legacy(String password) { // We know this is not very water-tight, but it at least // hides the contents of the password from prying eyes. @@ -136,6 +158,83 @@ namespace Sarand.Net.Standard.Dbas.Security return Encoding_Ansi1252.GetString(new[] { value }); } + //------------------------------------------------------------------- + // Password Database utilities. + //------------------------------------------------------------------- + /// + /// Generate password hash using the specified method. + /// - Returns hex notation as a string. + /// + /// + /// String of hex digits corresponding to encrypted password. + public override String EncodePassword_Hex(String password, PasswordHashMethod method = PasswordHashMethod.MsSql) + { + ClearErrorState(); + if (method == PasswordHashMethod.MsSql) + { + if (Database.DbmsType == DbmsType.MsSql) + { + try + { + String colAlias = "DataValue"; + String query = $"Select {colAlias}=Convert(VarChar(2000),PwdEncrypt('{DbQuery.FormatForCondition(password)}'),2);"; + using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(Database, query)) + { + if (dbResultSet.NextRecord()) + return dbResultSet.GetRecordValues().GetValue(colAlias).ToString(); + else if (dbResultSet.HasErrorState()) + SetErrorState(dbResultSet.ErrorStateMessage); + } + } + catch (Exception exc) + { + SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "hashing password")}", true); + SetErrorState(exc.Message); + } + } + else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password by method 'SQL'. Database is '{Database.DbmsType}'."); + } + else SaLogger.Warn($"{SysUtil.Fn} Unable to encode password. Unrecognised method: '{method}'."); + return ""; + } + + /// + /// Compare the password to the encrypted hash using the specified method. + /// + /// + /// + /// True if the password matches the encrypted hash value. + public override bool ComparePassword_Hex(String password, String passwordHashHex, PasswordHashMethod method = PasswordHashMethod.MsSql) + { + ClearErrorState(); + if (method == PasswordHashMethod.MsSql) + { + if (Database.DbmsType == DbmsType.MsSql) + { + try + { + String colAlias = "PwdCompare"; + String query = $"Select {colAlias}=PwdCompare('{DbQuery.FormatForCondition(password)}', Convert(VarBinary(1000),'{DbQuery.FormatForCondition(passwordHashHex)}',2));"; + using (DbResultSet dbResultSet = DbResultSet_DataReader.ExecuteQuery(Database, query)) + { + if (dbResultSet.NextRecord()) + return StringUtil.ToBool(dbResultSet.GetRecordValues().GetValue(colAlias).ToString()); + else if (dbResultSet.HasErrorState()) + SetErrorState(dbResultSet.ErrorStateMessage); + } + } + catch (Exception exc) + { + SaLogger.Error($"{SysUtil.Fn} {ExceptionUtil.GetLogMessage(exc, "comparing password")}", true); + SetErrorState(exc.Message); + } + } + else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password by method 'SQL'. Database is '{Database.DbmsType}'."); + } + else SaLogger.Warn($"{SysUtil.Fn} Unable to compare password. Unrecognised method: '{method}'."); + return false; + } + //------------------------------------------------------------------- } //=======================================================================