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 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<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 "
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

View File

@ -19,10 +19,6 @@ Namespace EncryptionUtils
''' Type of hash; some are security oriented, others are fast and simple
''' </summary>
Friend Enum Provider
''' <summary>
''' Secure Hashing Algorithm provider, SHA-1 variant, 160-bit
''' </summary>
SHA1
''' <summary>
''' Secure Hashing Algorithm provider, SHA-2 variant, 256-bit
''' </summary>
@ -35,10 +31,6 @@ Namespace EncryptionUtils
''' Secure Hashing Algorithm provider, SHA-2 variant, 512-bit
''' </summary>
SHA512
''' <summary>
''' Message Digest algorithm 5, 128-bit
''' </summary>
MD5
End Enum
Private _Hash As HashAlgorithm
@ -52,10 +44,6 @@ Namespace EncryptionUtils
''' </summary>
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
<Obsolete("MD5 is cryptographically broken and unsuitable for further use. Use SHA256 or stronger.")>
MD5
<Obsolete("SHA1 is cryptographically broken and unsuitable for further use. Use SHA256 or stronger.")>
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
''' <summary>
''' Advanced Encryption Standard (AES) provider
''' </summary>
AES
End Enum
@ -158,18 +134,7 @@ Namespace EncryptionUtils
''' Instantiates a new symmetric encryption object using the specified provider.
''' </summary>
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
''' </summary>
Friend Class Data
Private _b As Byte() = Nothing
@ -425,20 +389,10 @@ Namespace EncryptionUtils
Private _MinBytes 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>
''' Determines the default text encoding for this Data instance
''' </summary>
Friend Encoding As Text.Encoding = DefaultEncoding
Private _encoding As System.Text.Encoding = System.Text.Encoding.UTF8
''' <summary>
''' Creates new, empty encryption data
@ -455,9 +409,12 @@ Namespace EncryptionUtils
''' <summary>
''' 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>
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.
''' </summary>
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
''' </summary>
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
''' <summary>
''' Sets or returns text representation of bytes using the default text encoding
''' Sets or returns text representation of bytes using UTF8 encoding
''' </summary>
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
''' <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 Region

View File

@ -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()