diff --git a/vb-migration/Strata.Base.Internal.Tests/Security/UserSaltEncryptionMethodTests.cs b/vb-migration/Strata.Base.Internal.Tests/Security/UserSaltEncryptionMethodTests.cs index 2c2e20a..014482b 100644 --- a/vb-migration/Strata.Base.Internal.Tests/Security/UserSaltEncryptionMethodTests.cs +++ b/vb-migration/Strata.Base.Internal.Tests/Security/UserSaltEncryptionMethodTests.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Strata.Base.Internal.Encryptors; using System; using System.Configuration; +using System.Text; namespace Strata.Base.Internal.Tests.Security { @@ -116,5 +117,139 @@ namespace Strata.Base.Internal.Tests.Security // Assert Assert.AreEqual(24, decodedBytes.Length, "Output should be 24 bytes (192 bits)"); } + + [TestMethod] + public void Encode_WithEmptyPassword_ReturnsValidHash() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, "", TestUserGuid, TestSalt); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Empty password should still produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithEmptySalt_ReturnsValidHash() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, TestPassword, TestUserGuid, ""); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Empty salt should still produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithLongPassword_HandlesCorrectly() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + string longPassword = new string('a', 1000000); // 1MB password + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, longPassword, TestUserGuid, TestSalt); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Long password should produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithLongSalt_HandlesCorrectly() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + string longSalt = new string('a', 1000000); // 1MB salt + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, TestPassword, TestUserGuid, longSalt); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Long salt should produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithSpecialCharacters_HandlesCorrectly() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + string specialCharsPassword = "!@#$%^&*()_+-=[]{}|;:'\",.<>?/~`"; + string specialCharsSalt = "!@#$%^&*()_+-=[]{}|;:'\",.<>?/~`"; + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, specialCharsPassword, TestUserGuid, specialCharsSalt); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Special characters should produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithUnicodeCharacters_HandlesCorrectly() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + string unicodePassword = "Hello 世界! Привет мир! 안녕하세요!"; + string unicodeSalt = "Salt 世界! Соль! 소금!"; + + // Act + string result = encryptor.Encode(TestUsername, TestOrgPin, unicodePassword, TestUserGuid, unicodeSalt); + + // Assert + Assert.IsFalse(string.IsNullOrEmpty(result), "Unicode characters should produce a hash"); + Assert.AreEqual(24, Convert.FromBase64String(result).Length, "Hash should still be 24 bytes"); + } + + [TestMethod] + public void Encode_WithNullSalt_ThrowsArgumentNullException() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Assert + Assert.ThrowsException(() => + encryptor.Encode(TestUsername, TestOrgPin, TestPassword, TestUserGuid, null)); + } + + [TestMethod] + public void Encode_WithNullPassword_ThrowsArgumentNullException() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Assert + Assert.ThrowsException(() => + encryptor.Encode(TestUsername, TestOrgPin, null, TestUserGuid, TestSalt)); + } + + [TestMethod] + public void Encode_WithNullUsername_ThrowsArgumentNullException() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Assert + Assert.ThrowsException(() => + encryptor.Encode(null, TestOrgPin, TestPassword, TestUserGuid, TestSalt)); + } + + [TestMethod] + public void Encode_WithNullOrgPin_ThrowsArgumentNullException() + { + // Arrange + var encryptor = new UserSaltEncryptionMethod(); + + // Assert + Assert.ThrowsException(() => + encryptor.Encode(TestUsername, null, TestPassword, TestUserGuid, TestSalt)); + } } } diff --git a/vb-migration/Strata.Base.Internal/Encryptors/UserSaltEncryptionMethod.vb b/vb-migration/Strata.Base.Internal/Encryptors/UserSaltEncryptionMethod.vb index a40ec84..885c1e3 100644 --- a/vb-migration/Strata.Base.Internal/Encryptors/UserSaltEncryptionMethod.vb +++ b/vb-migration/Strata.Base.Internal/Encryptors/UserSaltEncryptionMethod.vb @@ -17,16 +17,38 @@ Namespace Encryptors #Region " Methods " Public Function Encode(ByVal username As String, ByVal anOrgPin As String, ByVal aNewPassword As String, ByVal aUserGUID As System.Guid, aSalt As String) As String Implements IPasswordEncryptionMethod.Encode - Dim saltAndPepper As String = aSalt & ConfigurationManager.AppSettings(NameOf(StrataJazzOptions.UserSaltEncryptionKey)) + If username Is Nothing Then + Throw New ArgumentNullException(NameOf(username)) + End If + If anOrgPin Is Nothing Then + Throw New ArgumentNullException(NameOf(anOrgPin)) + End If + + If aNewPassword Is Nothing Then + Throw New ArgumentNullException(NameOf(aNewPassword)) + End If + + If aSalt Is Nothing Then + Throw New ArgumentNullException(NameOf(aSalt)) + End If + + ' Get encryption key from config, throw if not found + Dim encryptionKey As String = ConfigurationManager.AppSettings(NameOf(StrataJazzOptions.UserSaltEncryptionKey)) + If String.IsNullOrEmpty(encryptionKey) Then + Throw New ConfigurationErrorsException("UserSaltEncryptionKey not found in configuration") + End If + + ' Combine salt with encryption key + Dim saltAndPepper As String = aSalt & encryptionKey + + ' Use UTF8 encoding to properly handle Unicode characters Using deriveBytes As Rfc2898DeriveBytes = New Rfc2898DeriveBytes(aNewPassword, Encoding.UTF8.GetBytes(saltAndPepper), NUMBER_ITERATIONS, HashAlgorithmName.SHA256) Dim password As Byte() = deriveBytes.GetBytes(24) - Return Convert.ToBase64String(password) End Using End Function - #End Region End Class diff --git a/vb-migration/Strata.Base.Internal/Security/Encryption.vb b/vb-migration/Strata.Base.Internal/Security/Encryption.vb index 1bf72b1..42880cc 100644 --- a/vb-migration/Strata.Base.Internal/Security/Encryption.vb +++ b/vb-migration/Strata.Base.Internal/Security/Encryption.vb @@ -19,10 +19,6 @@ Namespace EncryptionUtils ''' Type of hash; some are security oriented, others are fast and simple ''' Friend Enum Provider - ''' - ''' Secure Hashing Algorithm provider, SHA-1 variant, 160-bit - ''' - SHA1 ''' ''' Secure Hashing Algorithm provider, SHA-2 variant, 256-bit ''' @@ -35,10 +31,6 @@ Namespace EncryptionUtils ''' Secure Hashing Algorithm provider, SHA-2 variant, 512-bit ''' SHA512 - ''' - ''' Message Digest algorithm 5, 128-bit - ''' - MD5 End Enum Private _Hash As HashAlgorithm @@ -52,10 +44,6 @@ Namespace EncryptionUtils ''' Friend Sub New(ByVal p As Provider) Select Case p - Case Provider.MD5 - _Hash = MD5.Create() - Case Provider.SHA1 - _Hash = SHA1.Create() Case Provider.SHA256 _Hash = SHA256.Create() Case Provider.SHA384 @@ -126,21 +114,9 @@ Namespace EncryptionUtils Private Const _BufferSize As Integer = 2048 Friend Enum Provider - - MD5 - - SHA1 - SHA256 - SHA384 - SHA512 - - DES - - RC2 - - Rijndael - - TripleDES + ''' + ''' Advanced Encryption Standard (AES) provider + ''' AES End Enum @@ -158,18 +134,7 @@ Namespace EncryptionUtils ''' Instantiates a new symmetric encryption object using the specified provider. ''' Friend Sub New(ByVal provider As Provider, Optional ByVal useDefaultInitializationVector As Boolean = True) - Select Case provider - Case Provider.DES - _crypto = DES.Create() - Case Provider.RC2 - _crypto = RC2.Create() - Case Provider.Rijndael, Provider.AES - _crypto = Aes.Create() - Case Provider.TripleDES - _crypto = TripleDES.Create() - Case Else - _crypto = Aes.Create() ' Default to AES for unknown providers - End Select + _crypto = Aes.Create() ' Always use AES as it's the most secure option '-- make sure key and IV are always set, no matter what Me.Key = RandomKey() @@ -417,7 +382,6 @@ Namespace EncryptionUtils ''' represents Hex, Byte, Base64, or String data to encrypt/decrypt; ''' use the .Text property to set/get a string representation ''' use the .Hex property to set/get a string-based Hexadecimal representation - ''' use the .Base64 to set/get a string-based Base64 representation ''' Friend Class Data Private _b As Byte() = Nothing @@ -425,20 +389,10 @@ Namespace EncryptionUtils Private _MinBytes As Integer = 0 Private _StepBytes As Integer = 0 - ''' - ''' Determines the default text encoding across ALL Data instances - ''' - Friend Shared DefaultEncoding As Text.Encoding - - Shared Sub New() - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance) - DefaultEncoding = System.Text.Encoding.GetEncoding("Windows-1252") - End Sub - ''' ''' Determines the default text encoding for this Data instance ''' - Friend Encoding As Text.Encoding = DefaultEncoding + Private _encoding As System.Text.Encoding = System.Text.Encoding.UTF8 ''' ''' Creates new, empty encryption data @@ -455,9 +409,12 @@ Namespace EncryptionUtils ''' ''' Creates new encryption data with the specified string; - ''' will be converted to byte array using default encoding + ''' will be converted to byte array using UTF8 encoding ''' Friend Sub New(ByVal s As String) + If s Is Nothing Then + Throw New ArgumentNullException(NameOf(s)) + End If Me.Text = s End Sub @@ -466,7 +423,13 @@ Namespace EncryptionUtils ''' specified encoding to convert the string to a byte array. ''' Friend Sub New(ByVal s As String, ByVal encoding As System.Text.Encoding) - Me.Encoding = encoding + If s Is Nothing Then + Throw New ArgumentNullException(NameOf(s)) + End If + If encoding Is Nothing Then + Throw New ArgumentNullException(NameOf(encoding)) + End If + _encoding = encoding Me.Text = s End Sub @@ -563,20 +526,22 @@ Namespace EncryptionUtils ''' Friend Property Bytes() As Byte() Get - If _MaxBytes > 0 Then - If _b.Length > _MaxBytes Then - Dim b(_MaxBytes - 1) As Byte - Array.Copy(_b, b, b.Length) - _b = b - End If + If _b Is Nothing Then + Return Array.Empty(Of Byte)() End If - If _MinBytes > 0 Then - If _b.Length < _MinBytes Then - Dim b(_MinBytes - 1) As Byte - Array.Copy(_b, b, _b.Length) - _b = b - End If + + If _MaxBytes > 0 AndAlso _b.Length > _MaxBytes Then + Dim b(_MaxBytes - 1) As Byte + Array.Copy(_b, b, b.Length) + _b = b End If + + If _MinBytes > 0 AndAlso _b.Length < _MinBytes Then + Dim b(_MinBytes - 1) As Byte + Array.Copy(_b, b, _b.Length) + _b = b + End If + Return _b End Get Set(ByVal Value As Byte()) @@ -585,26 +550,31 @@ Namespace EncryptionUtils End Property ''' - ''' Sets or returns text representation of bytes using the default text encoding + ''' Sets or returns text representation of bytes using UTF8 encoding ''' Friend Property Text() As String Get - If _b Is Nothing Then - Return "" - Else - '-- need to handle nulls here; oddly, C# will happily convert - '-- nulls into the string whereas VB stops converting at the - '-- first null! + If _b Is Nothing OrElse _b.Length = 0 Then + Return String.Empty + End If + + Try + Return _encoding.GetString(_b) + Catch ex As Exception + ' If there's an encoding error, try to salvage what we can Dim i As Integer = Array.IndexOf(_b, CType(0, Byte)) If i >= 0 Then - Return Me.Encoding.GetString(_b, 0, i) - Else - Return Me.Encoding.GetString(_b) + Return _encoding.GetString(_b, 0, i) End If - End If + Throw + End Try End Get Set(ByVal Value As String) - _b = Me.Encoding.GetBytes(Value) + If Value Is Nothing Then + _b = Array.Empty(Of Byte)() + Else + _b = _encoding.GetBytes(Value) + End If End Set End Property @@ -632,27 +602,6 @@ Namespace EncryptionUtils End Set End Property - ''' - ''' Returns text representation of bytes using the default text encoding - ''' - Friend Shadows Function ToString() As String - Return Me.Text - End Function - - ''' - ''' returns Base64 string representation of this data - ''' - Friend Function ToBase64() As String - Return Me.Base64 - End Function - - ''' - ''' returns Hex string representation of this data - ''' - Friend Function ToHex() As String - Return Me.Hex - End Function - End Class #End Region diff --git a/vb-migration/Strata.Base.Internal/Security/SecurityUtils.vb b/vb-migration/Strata.Base.Internal/Security/SecurityUtils.vb index c2030ac..0127e7b 100644 --- a/vb-migration/Strata.Base.Internal/Security/SecurityUtils.vb +++ b/vb-migration/Strata.Base.Internal/Security/SecurityUtils.vb @@ -8,6 +8,10 @@ Public Class SecurityUtils #Region " Methods " Private Shared Function PadKey(key As String) As String + If key Is Nothing Then + Throw New ArgumentNullException(NameOf(key)) + End If + Dim paddedKey As String = key & ENCRYPTION_KEY_SUFFIX If paddedKey.Length < KEY_SIZE_BYTES Then paddedKey = paddedKey.PadRight(KEY_SIZE_BYTES, "X"c) @@ -18,13 +22,39 @@ Public Class SecurityUtils End Function Public Shared Function EncryptValue(value As String, key As String) As String - Dim encryption As New EncryptionUtils.SymmetricEncryptor(EncryptionUtils.SymmetricEncryptor.Provider.Rijndael) + If value Is Nothing Then + Return Nothing + End If - Return encryption.Encrypt(New EncryptionUtils.Data(value), New EncryptionUtils.Data(PadKey(key))).ToBase64 + If String.IsNullOrEmpty(value) Then + Return String.Empty + End If + + If key Is Nothing Then + Throw New ArgumentNullException(NameOf(key)) + End If + + ' Create encryptor with default IV + Dim encryption As New EncryptionUtils.SymmetricEncryptor(EncryptionUtils.SymmetricEncryptor.Provider.AES) + + Dim result = encryption.Encrypt(New EncryptionUtils.Data(value), New EncryptionUtils.Data(PadKey(key))) + Return result.Base64 End Function Public Shared Function DecryptValue(encryptedValue As String, key As String) As String - Dim encryption As New EncryptionUtils.SymmetricEncryptor(EncryptionUtils.SymmetricEncryptor.Provider.Rijndael) + If encryptedValue Is Nothing Then + Return Nothing + End If + + If String.IsNullOrEmpty(encryptedValue) Then + Return String.Empty + End If + + If key Is Nothing Then + Throw New ArgumentNullException(NameOf(key)) + End If + + Dim encryption As New EncryptionUtils.SymmetricEncryptor(EncryptionUtils.SymmetricEncryptor.Provider.AES) ' note EncryptValue returns Base64 string so we need to initialized encryptedData as Base64 Dim encryptedData As EncryptionUtils.Data = New EncryptionUtils.Data()