Imports System.Text
Imports System.Security.Cryptography

Namespace Security.Cryptography

    ''' <summary>
    ''' Contains a set of hashing functions to construct hashed values.
    ''' </summary>
    ''' <remarks></remarks>
    Public Class HashUtil

#Region "Utilities"

        Private Shared Function GenerateRandomSaltBytes(ByVal saltSize As Integer) As Byte()
            ' Allocate array for salt values
            Dim saltBytes() As Byte = New Byte(saltSize - 1) {}

            ' Get a cryptographic random number generator
            Dim rng As RNGCryptoServiceProvider = New RNGCryptoServiceProvider()
            ' Fill the salt with cryptographically string byte values
            rng.GetNonZeroBytes(saltBytes)

            Return saltBytes
        End Function

        Private Shared Function GeneratePostFixHash(ByVal algorithm As HashAlgorithm, ByVal encoding As Encoding, _
                                                    ByVal text As String, ByVal salt As Long, ByVal returnHex As Boolean, _
                                                    ByVal byteOffset As Integer) As String
            ' Get byte array from text
            Dim credentialBytes() As Byte = encoding.GetBytes(text)
            ' Get byte array from salt
            Dim saltBytes() As Byte = BitConverter.GetBytes(salt)
            If IsValueInt32(salt) Then _
                saltBytes = BitConverter.GetBytes(CType(salt, Integer))

            ' Concatenate hash and salt
            Dim toHash As Byte() = New Byte((credentialBytes.Length + saltBytes.Length - 1) + byteOffset) {}
            Array.Copy(credentialBytes, 0, toHash, 0, credentialBytes.Length)
            Array.Copy(saltBytes, 0, toHash, credentialBytes.Length, saltBytes.Length)

            ' Get hash value
            Dim computedHash() As Byte = algorithm.ComputeHash(toHash)

            ' Return
            If returnHex Then
                Dim hex As String = BitConverter.ToString(computedHash)
                Return Replace(hex, "-", String.Empty)
            Else
                Return Convert.ToBase64String(computedHash)
            End If
        End Function

        Private Shared Function IsValueInt32(ByVal value As Long) As Boolean
            If value < Int32.MinValue OrElse value > Int32.MaxValue Then _
                Return False
            Return True
        End Function

#End Region

#Region "CreateRandomSalt"

        ''' <summary>
        ''' Returns a cryptographically strong random number (32-bit).  
        ''' </summary>
        ''' <returns>A cryptographically strong random number.</returns>
        ''' <remarks>The salt size is a randomly generated number between 4 and 8</remarks>
        Public Shared Function CreateRandomSalt() As Integer
            ' Get pseudo random number generator for salt size
            Dim random As Random = New Random()

            ' Create random salt size between 4 and 8 bytes for Int32
            Return CreateRandomSalt(random.Next(4, 8))
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong random number (32-bit).  
        ''' </summary>
        ''' <param name="saltSize">The size the byte array for the salt value.</param>
        ''' <returns>A cryptographically strong random number.</returns>
        ''' <remarks></remarks>
        Public Shared Function CreateRandomSalt(ByVal saltSize As Integer) As Integer
            ' Generate random salt from the specified salt size
            Dim saltBytes() As Byte = GenerateRandomSaltBytes(saltSize)

            ' Convert to 32 bit integer
            Return BitConverter.ToInt32(saltBytes, 0)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong random number (64-bit).  
        ''' </summary>
        ''' <returns>A cryptographically strong random number.</returns>
        ''' <remarks></remarks>
        Public Shared Function CreateRandomSalt64() As Long
            ' Get pseudo random number generator for salt size
            Dim random As Random = New Random()

            ' Create random salt size between 8 and 16 bytes for Int64
            Return CreateRandomSalt64(random.Next(8, 16))
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong random number (64-bit).  
        ''' </summary>
        ''' <param name="saltSize">The size the byte array for the salt value.</param>
        ''' <returns>A cryptographically strong random number.</returns>
        ''' <remarks></remarks>
        Public Shared Function CreateRandomSalt64(ByVal saltSize As Integer) As Long
            ' Generate random salt from the specified salt size
            Dim saltBytes() As Byte = GenerateRandomSaltBytes(saltSize)

            ' Convert to 64 bit integer
            Return BitConverter.ToInt64(saltBytes, 0)
        End Function

#End Region

#Region "IsEquivalent"

        ''' <summary>
        ''' Returns a value indicating whether the specified hash value matches 
        ''' a specified salt and string hashed with the provided algorithm.
        ''' </summary>
        ''' <param name="compareHash">The hash value to compare.</param>
        ''' <param name="text">The text to be hashed and compared.</param>
        ''' <param name="salt">The salt value to apply to the hashing function.</param>
        ''' <param name="algorithm">A valid HashAlgorithm.</param>
        ''' <returns>True if the specified hash value matches the hashed 
        ''' value of the specified salt and string; otherwise, false.</returns>
        ''' <remarks></remarks>
        Public Shared Function IsEquivalent(ByVal compareHash As String, ByVal text As String, _
                                            ByVal salt As Long, ByVal algorithm As HashAlgorithm) As Boolean
            Return IsEquivalent(compareHash, text, salt, algorithm, Encoding.GetEncoding(Encoding.Unicode.CodePage), False)
        End Function

        Public Shared Function IsEquivalent(ByVal compareHash As String, ByVal text As String, ByVal salt As Long, _
            ByVal algorithm As HashAlgorithm, ByVal encoding As Encoding, ByVal compareHex As Boolean) As Boolean
            Dim hash As String = GetHash(algorithm, encoding, text, salt, compareHex)
            If hash = compareHash Then _
                Return True
            Return False
        End Function

#End Region

#Region "GetHash"

        ''' <summary>
        ''' Returns a cryptographically strong hash string generated 
        ''' from the specified hash info, text, and salt values.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <param name="info">An object containing data for generating a cryptographic hash.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetHash(ByVal text As String, ByVal salt As Long, ByVal info As HashInfo) As String
            If info Is Nothing Then _
                Throw New ArgumentNullException("info")
            Return GeneratePostFixHash(info.Algorithm, info.Encoding, text, salt, info.ReturnHex, info.ByteOffset)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string generated 
        ''' from the specified hash algorithm, text, and salt values.
        ''' </summary>
        ''' <param name="algorithm">The hash algorithm to utilize.</param>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <param name="returnHex">A value indicating whether the return string 
        ''' should be formatted as hexadecimal numbers.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetHash(ByVal algorithm As HashAlgorithm, ByVal encoding As Encoding, _
                                       ByVal text As String, ByVal salt As Long, ByVal returnHex As Boolean) As String
            Return GeneratePostFixHash(algorithm, encoding, text, salt, returnHex, 0)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from 
        ''' the specified text using the SHA256 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA256(ByVal text As String) As String
            Return GetSHA256(text, 0)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from the 
        ''' specified text and salt using the SHA256 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA256(ByVal text As String, ByVal salt As Integer) As String
            Return GetSHA256(text, salt, False)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from the 
        ''' specified text and salt using the SHA256 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <param name="returnHex">A value indicating whether the return string 
        ''' should be formatted as hexadecimal numbers.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA256(ByVal text As String, ByVal salt As Integer, ByVal returnHex As Boolean) As String
#If FULL_FRAME Then
            Using ha As SHA256 = SHA256.Create()
                Return GetHash(ha, Encoding.GetEncoding(Encoding.Unicode.CodePage, _
                                                        New EncoderExceptionFallback(), _
                                                        New DecoderExceptionFallback()), text, salt, returnHex)
            End Using
#Else
            Throw New NotSupportedException()
#End If
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from 
        ''' the specified text using the SHA512 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA512(ByVal text As String) As String
            Return GetSHA512(text, 0)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from the 
        ''' specified text and salt using the SHA512 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA512(ByVal text As String, ByVal salt As Long) As String
            Return GetSHA512(text, salt, False)
        End Function

        ''' <summary>
        ''' Returns a cryptographically strong hash string from the 
        ''' specified text and salt using the SHA512 hash algorithm.
        ''' </summary>
        ''' <param name="text">The text to hash.</param>
        ''' <param name="salt">The salt to apply to the hashed value.</param>
        ''' <param name="returnHex">A value indicating whether the return string 
        ''' should be formatted as hexadecimal numbers.</param>
        ''' <returns>A cryptographically strong hash string.</returns>
        ''' <remarks></remarks>
        Public Shared Function GetSHA512(ByVal text As String, ByVal salt As Long, ByVal returnHex As Boolean) As String
#If FULL_FRAME Then
            Using ha As SHA512 = SHA512.Create()
                Return GetHash(ha, Encoding.GetEncoding(Encoding.Unicode.CodePage, _
                                                        New EncoderExceptionFallback(), _
                                                        New DecoderExceptionFallback()), text, salt, returnHex)
            End Using
#Else
            Throw New NotSupportedException()
#End If
        End Function

#End Region

    End Class

End Namespace