Improve warnings and tests

This commit is contained in:
Jorge Burgos 2025-01-30 22:00:43 -05:00
parent 7b0ed744c7
commit 4da8a0bd04
4 changed files with 240 additions and 104 deletions

View File

@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
using Strata.Base.Internal.Encryptors; using Strata.Base.Internal.Encryptors;
using System; using System;
using System.Configuration; using System.Configuration;
using System.Text;
namespace Strata.Base.Internal.Tests.Security namespace Strata.Base.Internal.Tests.Security
{ {
@ -116,5 +117,139 @@ namespace Strata.Base.Internal.Tests.Security
// Assert // Assert
Assert.AreEqual(24, decodedBytes.Length, "Output should be 24 bytes (192 bits)"); 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<ArgumentNullException>(() =>
encryptor.Encode(TestUsername, TestOrgPin, TestPassword, TestUserGuid, null));
}
[TestMethod]
public void Encode_WithNullPassword_ThrowsArgumentNullException()
{
// Arrange
var encryptor = new UserSaltEncryptionMethod();
// Assert
Assert.ThrowsException<ArgumentNullException>(() =>
encryptor.Encode(TestUsername, TestOrgPin, null, TestUserGuid, TestSalt));
}
[TestMethod]
public void Encode_WithNullUsername_ThrowsArgumentNullException()
{
// Arrange
var encryptor = new UserSaltEncryptionMethod();
// Assert
Assert.ThrowsException<ArgumentNullException>(() =>
encryptor.Encode(null, TestOrgPin, TestPassword, TestUserGuid, TestSalt));
}
[TestMethod]
public void Encode_WithNullOrgPin_ThrowsArgumentNullException()
{
// Arrange
var encryptor = new UserSaltEncryptionMethod();
// Assert
Assert.ThrowsException<ArgumentNullException>(() =>
encryptor.Encode(TestUsername, null, TestPassword, TestUserGuid, TestSalt));
}
} }
} }

View File

@ -17,16 +17,38 @@ Namespace Encryptors
#Region " Methods " #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 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) Using deriveBytes As Rfc2898DeriveBytes = New Rfc2898DeriveBytes(aNewPassword, Encoding.UTF8.GetBytes(saltAndPepper), NUMBER_ITERATIONS, HashAlgorithmName.SHA256)
Dim password As Byte() = deriveBytes.GetBytes(24) Dim password As Byte() = deriveBytes.GetBytes(24)
Return Convert.ToBase64String(password) Return Convert.ToBase64String(password)
End Using End Using
End Function End Function
#End Region #End Region
End Class End Class

View File

@ -19,10 +19,6 @@ Namespace EncryptionUtils
''' Type of hash; some are security oriented, others are fast and simple ''' Type of hash; some are security oriented, others are fast and simple
''' </summary> ''' </summary>
Friend Enum Provider Friend Enum Provider
''' <summary>
''' Secure Hashing Algorithm provider, SHA-1 variant, 160-bit
''' </summary>
SHA1
''' <summary> ''' <summary>
''' Secure Hashing Algorithm provider, SHA-2 variant, 256-bit ''' Secure Hashing Algorithm provider, SHA-2 variant, 256-bit
''' </summary> ''' </summary>
@ -35,10 +31,6 @@ Namespace EncryptionUtils
''' Secure Hashing Algorithm provider, SHA-2 variant, 512-bit ''' Secure Hashing Algorithm provider, SHA-2 variant, 512-bit
''' </summary> ''' </summary>
SHA512 SHA512
''' <summary>
''' Message Digest algorithm 5, 128-bit
''' </summary>
MD5
End Enum End Enum
Private _Hash As HashAlgorithm Private _Hash As HashAlgorithm
@ -52,10 +44,6 @@ Namespace EncryptionUtils
''' </summary> ''' </summary>
Friend Sub New(ByVal p As Provider) Friend Sub New(ByVal p As Provider)
Select Case p Select Case p
Case Provider.MD5
_Hash = MD5.Create()
Case Provider.SHA1
_Hash = SHA1.Create()
Case Provider.SHA256 Case Provider.SHA256
_Hash = SHA256.Create() _Hash = SHA256.Create()
Case Provider.SHA384 Case Provider.SHA384
@ -126,21 +114,9 @@ Namespace EncryptionUtils
Private Const _BufferSize As Integer = 2048 Private Const _BufferSize As Integer = 2048
Friend Enum Provider Friend Enum Provider
<Obsolete("MD5 is cryptographically broken and unsuitable for further use. Use SHA256 or stronger.")> ''' <summary>
MD5 ''' Advanced Encryption Standard (AES) provider
<Obsolete("SHA1 is cryptographically broken and unsuitable for further use. Use SHA256 or stronger.")> ''' </summary>
SHA1
SHA256
SHA384
SHA512
<Obsolete("DES is cryptographically broken and unsuitable for further use. Use AES instead.")>
DES
<Obsolete("RC2 is cryptographically broken and unsuitable for further use. Use AES instead.")>
RC2
<Obsolete("Use AES instead. This enum value will be removed in a future version.")>
Rijndael
<Obsolete("TripleDES is not recommended for new applications. Use AES instead.")>
TripleDES
AES AES
End Enum End Enum
@ -158,18 +134,7 @@ Namespace EncryptionUtils
''' Instantiates a new symmetric encryption object using the specified provider. ''' Instantiates a new symmetric encryption object using the specified provider.
''' </summary> ''' </summary>
Friend Sub New(ByVal provider As Provider, Optional ByVal useDefaultInitializationVector As Boolean = True) Friend Sub New(ByVal provider As Provider, Optional ByVal useDefaultInitializationVector As Boolean = True)
Select Case provider _crypto = Aes.Create() ' Always use AES as it's the most secure option
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
'-- make sure key and IV are always set, no matter what '-- make sure key and IV are always set, no matter what
Me.Key = RandomKey() Me.Key = RandomKey()
@ -417,7 +382,6 @@ Namespace EncryptionUtils
''' represents Hex, Byte, Base64, or String data to encrypt/decrypt; ''' represents Hex, Byte, Base64, or String data to encrypt/decrypt;
''' use the .Text property to set/get a string representation ''' use the .Text property to set/get a string representation
''' use the .Hex property to set/get a string-based Hexadecimal representation ''' use the .Hex property to set/get a string-based Hexadecimal representation
''' use the .Base64 to set/get a string-based Base64 representation
''' </summary> ''' </summary>
Friend Class Data Friend Class Data
Private _b As Byte() = Nothing Private _b As Byte() = Nothing
@ -425,20 +389,10 @@ Namespace EncryptionUtils
Private _MinBytes As Integer = 0 Private _MinBytes As Integer = 0
Private _StepBytes As Integer = 0 Private _StepBytes As Integer = 0
''' <summary>
''' Determines the default text encoding across ALL Data instances
''' </summary>
Friend Shared DefaultEncoding As Text.Encoding
Shared Sub New()
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
DefaultEncoding = System.Text.Encoding.GetEncoding("Windows-1252")
End Sub
''' <summary> ''' <summary>
''' Determines the default text encoding for this Data instance ''' Determines the default text encoding for this Data instance
''' </summary> ''' </summary>
Friend Encoding As Text.Encoding = DefaultEncoding Private _encoding As System.Text.Encoding = System.Text.Encoding.UTF8
''' <summary> ''' <summary>
''' Creates new, empty encryption data ''' Creates new, empty encryption data
@ -455,9 +409,12 @@ Namespace EncryptionUtils
''' <summary> ''' <summary>
''' Creates new encryption data with the specified string; ''' 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
''' </summary> ''' </summary>
Friend Sub New(ByVal s As String) Friend Sub New(ByVal s As String)
If s Is Nothing Then
Throw New ArgumentNullException(NameOf(s))
End If
Me.Text = s Me.Text = s
End Sub End Sub
@ -466,7 +423,13 @@ Namespace EncryptionUtils
''' specified encoding to convert the string to a byte array. ''' specified encoding to convert the string to a byte array.
''' </summary> ''' </summary>
Friend Sub New(ByVal s As String, ByVal encoding As System.Text.Encoding) 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 Me.Text = s
End Sub End Sub
@ -563,20 +526,22 @@ Namespace EncryptionUtils
''' </summary> ''' </summary>
Friend Property Bytes() As Byte() Friend Property Bytes() As Byte()
Get Get
If _MaxBytes > 0 Then If _b Is Nothing Then
If _b.Length > _MaxBytes Then Return Array.Empty(Of Byte)()
Dim b(_MaxBytes - 1) As Byte
Array.Copy(_b, b, b.Length)
_b = b
End If
End If End If
If _MinBytes > 0 Then
If _b.Length < _MinBytes Then If _MaxBytes > 0 AndAlso _b.Length > _MaxBytes Then
Dim b(_MinBytes - 1) As Byte Dim b(_MaxBytes - 1) As Byte
Array.Copy(_b, b, _b.Length) Array.Copy(_b, b, b.Length)
_b = b _b = b
End If
End If 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 Return _b
End Get End Get
Set(ByVal Value As Byte()) Set(ByVal Value As Byte())
@ -585,26 +550,31 @@ Namespace EncryptionUtils
End Property End Property
''' <summary> ''' <summary>
''' Sets or returns text representation of bytes using the default text encoding ''' Sets or returns text representation of bytes using UTF8 encoding
''' </summary> ''' </summary>
Friend Property Text() As String Friend Property Text() As String
Get Get
If _b Is Nothing Then If _b Is Nothing OrElse _b.Length = 0 Then
Return "" Return String.Empty
Else End If
'-- need to handle nulls here; oddly, C# will happily convert
'-- nulls into the string whereas VB stops converting at the Try
'-- first null! 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)) Dim i As Integer = Array.IndexOf(_b, CType(0, Byte))
If i >= 0 Then If i >= 0 Then
Return Me.Encoding.GetString(_b, 0, i) Return _encoding.GetString(_b, 0, i)
Else
Return Me.Encoding.GetString(_b)
End If End If
End If Throw
End Try
End Get End Get
Set(ByVal Value As String) 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 Set
End Property End Property
@ -632,27 +602,6 @@ Namespace EncryptionUtils
End Set End Set
End Property End Property
''' <summary>
''' Returns text representation of bytes using the default text encoding
''' </summary>
Friend Shadows Function ToString() As String
Return Me.Text
End Function
''' <summary>
''' returns Base64 string representation of this data
''' </summary>
Friend Function ToBase64() As String
Return Me.Base64
End Function
''' <summary>
''' returns Hex string representation of this data
''' </summary>
Friend Function ToHex() As String
Return Me.Hex
End Function
End Class End Class
#End Region #End Region

View File

@ -8,6 +8,10 @@ Public Class SecurityUtils
#Region " Methods " #Region " Methods "
Private Shared Function PadKey(key As String) As String 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 Dim paddedKey As String = key & ENCRYPTION_KEY_SUFFIX
If paddedKey.Length < KEY_SIZE_BYTES Then If paddedKey.Length < KEY_SIZE_BYTES Then
paddedKey = paddedKey.PadRight(KEY_SIZE_BYTES, "X"c) paddedKey = paddedKey.PadRight(KEY_SIZE_BYTES, "X"c)
@ -18,13 +22,39 @@ Public Class SecurityUtils
End Function End Function
Public Shared Function EncryptValue(value As String, key As String) As String 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 End Function
Public Shared Function DecryptValue(encryptedValue As String, key As String) As String 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 ' note EncryptValue returns Base64 string so we need to initialized encryptedData as Base64
Dim encryptedData As EncryptionUtils.Data = New EncryptionUtils.Data() Dim encryptedData As EncryptionUtils.Data = New EncryptionUtils.Data()