Improve warnings and tests
This commit is contained in:
parent
7b0ed744c7
commit
4da8a0bd04
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user