Imports System.IO
Imports System.Security.Cryptography
Imports System.Text

Namespace EncryptionUtils

#Region " Hash "

    ''' <summary>
    ''' Hash functions are fundamental to modern cryptography. These functions map binary 
    ''' strings of an arbitrary length to small binary strings of a fixed length, known as 
    ''' hash values. A cryptographic hash function has the property that it is computationally
    ''' infeasible to find two distinct inputs that hash to the same value. Hash functions 
    ''' are commonly used with digital signatures and for data integrity.
    ''' </summary>
    Public Class Hasher

        ''' <summary>
        ''' Type of hash; some are security oriented, others are fast and simple
        ''' </summary>
        Friend Enum Provider
            ''' <summary>
            ''' Secure Hashing Algorithm provider, SHA-2 variant, 256-bit
            ''' </summary>
            SHA256
            ''' <summary>
            ''' Secure Hashing Algorithm provider, SHA-2 variant, 384-bit
            ''' </summary>
            SHA384
            ''' <summary>
            ''' Secure Hashing Algorithm provider, SHA-2 variant, 512-bit
            ''' </summary>
            SHA512
        End Enum

        Private _Hash As HashAlgorithm
        Private _HashValue As New Data

        Friend Sub New()
        End Sub

        ''' <summary>
        ''' Instantiate a new hash of the specified type
        ''' </summary>
        Friend Sub New(ByVal p As Provider)
            Select Case p
                Case Provider.SHA256
                    _Hash = SHA256.Create()
                Case Provider.SHA384
                    _Hash = SHA384.Create()
                Case Provider.SHA512
                    _Hash = SHA512.Create()
                Case Else
                    _Hash = SHA256.Create()  ' Default to SHA256 for unknown providers
            End Select
        End Sub

        ''' <summary>
        ''' Returns the previously calculated hash
        ''' </summary>
        Friend ReadOnly Property Value() As Data
            Get
                Return _HashValue
            End Get
        End Property

        ''' <summary>
        ''' Calculates hash on a stream of arbitrary length
        ''' </summary>
        Friend Function Calculate(ByRef s As System.IO.Stream) As Data
            _HashValue.Bytes = _Hash.ComputeHash(s)
            Return _HashValue
        End Function

        ''' <summary>
        ''' Calculates hash for fixed length <see cref="Data"/>
        ''' </summary>
        Friend Function Calculate(ByVal d As Data) As Data
            Return CalculatePrivate(d.Bytes)
        End Function

        ''' <summary>
        ''' Calculates hash for a string with a prefixed salt value. 
        ''' A "salt" is random data prefixed to every hashed value to prevent 
        ''' common dictionary attacks.
        ''' </summary>
        Friend Function Calculate(ByVal d As Data, ByVal salt As Data) As Data
            Dim nb(d.Bytes.Length + salt.Bytes.Length - 1) As Byte
            salt.Bytes.CopyTo(nb, 0)
            d.Bytes.CopyTo(nb, salt.Bytes.Length)
            Return CalculatePrivate(nb)
        End Function

        ''' <summary>
        ''' Calculates hash for an array of bytes
        ''' </summary>
        Private Function CalculatePrivate(ByVal b() As Byte) As Data
            _HashValue.Bytes = _Hash.ComputeHash(b)
            Return _HashValue
        End Function

    End Class
#End Region

#Region " Symmetric "

    ''' <summary>
    ''' Symmetric encryption uses a single key to encrypt and decrypt. 
    ''' Both parties (encryptor and decryptor) must share the same secret key.
    ''' </summary>
    Friend Class SymmetricEncryptor

        Private Const _DefaultIntializationVector As String = "%1Az=-@qT"
        Private Const _BufferSize As Integer = 2048

        Friend Enum Provider
            ''' <summary>
            ''' Advanced Encryption Standard (AES) provider
            ''' </summary>
            AES
        End Enum

        Private _data As Data
        Private _key As Data
        Private _iv As Data
        Private _crypto As SymmetricAlgorithm
        Private _EncryptedBytes As Byte()
        Private _UseDefaultInitializationVector As Boolean

        Private Sub New()
        End Sub

        ''' <summary>
        ''' Instantiates a new symmetric encryption object using the specified provider.
        ''' </summary>
        Friend Sub New(ByVal provider As Provider, Optional ByVal useDefaultInitializationVector As Boolean = True)
            _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()
            If useDefaultInitializationVector Then
                Me.IntializationVector = New Data(_DefaultIntializationVector)
            Else
                Me.IntializationVector = RandomInitializationVector()
            End If
        End Sub

        ''' <summary>
        ''' Key size in bytes. We use the default key size for any given provider; if you 
        ''' want to force a specific key size, set this property
        ''' </summary>
        Friend Property KeySizeBytes() As Integer
            Get
                Return _crypto.KeySize \ 8
            End Get
            Set(ByVal Value As Integer)
                _crypto.KeySize = Value * 8
                _key.MaxBytes = Value
            End Set
        End Property

        ''' <summary>
        ''' Key size in bits. We use the default key size for any given provider; if you 
        ''' want to force a specific key size, set this property
        ''' </summary>
        Friend Property KeySizeBits() As Integer
            Get
                Return _crypto.KeySize
            End Get
            Set(ByVal Value As Integer)
                _crypto.KeySize = Value
                _key.MaxBits = Value
            End Set
        End Property

        ''' <summary>
        ''' The key used to encrypt/decrypt data
        ''' </summary>
        Friend Property Key() As Data
            Get
                Return _key
            End Get
            Set(ByVal Value As Data)
                _key = Value
                _key.MaxBytes = _crypto.LegalKeySizes(0).MaxSize \ 8
                _key.MinBytes = _crypto.LegalKeySizes(0).MinSize \ 8
                _key.StepBytes = _crypto.LegalKeySizes(0).SkipSize \ 8
            End Set
        End Property

        ''' <summary>
        ''' Using the default Cipher Block Chaining (CBC) mode, all data blocks are processed using
        ''' the value derived from the previous block; the first data block has no previous data block
        ''' to use, so it needs an InitializationVector to feed the first block
        ''' </summary>
        Friend Property IntializationVector() As Data
            Get
                Return _iv
            End Get
            Set(ByVal Value As Data)
                _iv = Value
                _iv.MaxBytes = _crypto.BlockSize \ 8
                _iv.MinBytes = _crypto.BlockSize \ 8
            End Set
        End Property

        ''' <summary>
        ''' generates a random Initialization Vector, if one was not provided
        ''' </summary>
        Friend Function RandomInitializationVector() As Data
            _crypto.GenerateIV()
            Dim d As New Data(_crypto.IV)
            Return d
        End Function

        ''' <summary>
        ''' generates a random Key, if one was not provided
        ''' </summary>
        Friend Function RandomKey() As Data
            _crypto.GenerateKey()
            Dim d As New Data(_crypto.Key)
            Return d
        End Function

        ''' <summary>
        ''' Ensures that _crypto object has valid Key and IV
        ''' prior to any attempt to encrypt/decrypt anything
        ''' </summary>
        Private Sub ValidateKeyAndIv(ByVal isEncrypting As Boolean)
            If _key.IsEmpty Then
                If isEncrypting Then
                    _key = RandomKey()
                Else
                    Throw New CryptographicException("No key was provided for the decryption operation!")
                End If
            End If
            If _iv.IsEmpty Then
                If isEncrypting Then
                    _iv = RandomInitializationVector()
                Else
                    Throw New CryptographicException("No initialization vector was provided for the decryption operation!")
                End If
            End If
            Try
                _crypto.Key = _key.Bytes
                _crypto.IV = _iv.Bytes
            Catch ex As CryptographicException
                Throw New CryptographicException("Invalid key or initialization vector.", ex)
            End Try
        End Sub

        ''' <summary>
        ''' Encrypts the specified Data using provided key
        ''' </summary>
        Friend Function Encrypt(ByVal d As Data, ByVal key As Data) As Data
            Me.Key = key
            Return Encrypt(d)
        End Function

        ''' <summary>
        ''' Encrypts the specified Data using preset key and preset initialization vector
        ''' </summary>
        Friend Function Encrypt(ByVal d As Data) As Data
            Dim ms As New IO.MemoryStream

            ValidateKeyAndIv(True)

            Dim cs As New CryptoStream(ms, _crypto.CreateEncryptor(), CryptoStreamMode.Write)
            cs.Write(d.Bytes, 0, d.Bytes.Length)
            cs.Close()
            ms.Close()

            Return New Data(ms.ToArray)
        End Function

        ''' <summary>
        ''' Encrypts the stream to memory using provided key and provided initialization vector
        ''' </summary>
        Friend Function Encrypt(ByVal s As Stream, ByVal key As Data, ByVal iv As Data) As Data
            Me.IntializationVector = iv
            Me.Key = key
            Return Encrypt(s)
        End Function

        ''' <summary>
        ''' Encrypts the stream to memory using specified key
        ''' </summary>
        Friend Function Encrypt(ByVal s As Stream, ByVal key As Data) As Data
            Me.Key = key
            Return Encrypt(s)
        End Function

        ''' <summary>
        ''' Encrypts the specified stream to memory using preset key and preset initialization vector
        ''' </summary>
        Friend Function Encrypt(ByVal s As Stream) As Data
            Dim ms As New IO.MemoryStream
            Dim b(_BufferSize) As Byte
            Dim i As Integer

            ValidateKeyAndIv(True)

            Dim cs As New CryptoStream(ms, _crypto.CreateEncryptor(), CryptoStreamMode.Write)
            i = s.Read(b, 0, _BufferSize)
            Do While i > 0
                cs.Write(b, 0, i)
                i = s.Read(b, 0, _BufferSize)
            Loop

            cs.Close()
            ms.Close()

            Return New Data(ms.ToArray)
        End Function

        ''' <summary>
        ''' Decrypts the specified data using provided key and preset initialization vector
        ''' </summary>
        Friend Function Decrypt(ByVal encryptedData As Data, ByVal key As Data) As Data
            Me.Key = key
            Return Decrypt(encryptedData)
        End Function

        ''' <summary>
        ''' Decrypts the specified stream using provided key and preset initialization vector
        ''' </summary>
        Friend Function Decrypt(ByVal encryptedStream As Stream, ByVal key As Data) As Data
            Me.Key = key
            Return Decrypt(encryptedStream)
        End Function

        ''' <summary>
        ''' Decrypts the specified stream using preset key and preset initialization vector
        ''' </summary>
        Friend Function Decrypt(ByVal encryptedStream As Stream) As Data
            Dim ms As New System.IO.MemoryStream
            Dim b(_BufferSize) As Byte

            ValidateKeyAndIv(False)
            Dim cs As New CryptoStream(encryptedStream,
                _crypto.CreateDecryptor(), CryptoStreamMode.Read)

            Dim i As Integer
            i = cs.Read(b, 0, _BufferSize)

            Do While i > 0
                ms.Write(b, 0, i)
                i = cs.Read(b, 0, _BufferSize)
            Loop
            cs.Close()
            ms.Close()

            Return New Data(ms.ToArray)
        End Function

        ''' <summary>
        ''' Decrypts the specified data using preset key and preset initialization vector
        ''' </summary>
        Friend Function Decrypt(ByVal encryptedData As Data) As Data
            Using ms As New System.IO.MemoryStream(encryptedData.Bytes, 0, encryptedData.Bytes.Length)
                ValidateKeyAndIv(False)
                Using cs As New CryptoStream(ms, _crypto.CreateDecryptor(), CryptoStreamMode.Read)
                    Using outputMs As New MemoryStream()
                        Try
                            cs.CopyTo(outputMs)
                            Return New Data(outputMs.ToArray())
                        Catch ex As CryptographicException
                            Throw New CryptographicException("Unable to decrypt data. The provided key may be invalid.", ex)
                        End Try
                    End Using
                End Using
            End Using
        End Function

    End Class

#End Region

#Region " Data "

    ''' <summary>
    ''' 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 
    ''' </summary>
    Friend Class Data
        Private _b As Byte() = Nothing
        Private _MaxBytes As Integer = 0
        Private _MinBytes As Integer = 0
        Private _StepBytes As Integer = 0

        ''' <summary>
        ''' Determines the default text encoding for this Data instance
        ''' </summary>
        Private _encoding As System.Text.Encoding = System.Text.Encoding.UTF8

        ''' <summary>
        ''' Creates new, empty encryption data
        ''' </summary>
        Friend Sub New()
        End Sub

        ''' <summary>
        ''' Creates new encryption data with the specified byte array
        ''' </summary>
        Friend Sub New(ByVal b As Byte())
            _b = b
        End Sub

        ''' <summary>
        ''' Creates new encryption data with the specified string; 
        ''' 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

        ''' <summary>
        ''' Creates new encryption data using the specified string and the 
        ''' specified encoding to convert the string to a byte array.
        ''' </summary>
        Friend Sub New(ByVal s As String, ByVal encoding As System.Text.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

        ''' <summary>
        ''' returns true if no data is present
        ''' </summary>
        Friend ReadOnly Property IsEmpty() As Boolean
            Get
                If _b Is Nothing Then
                    Return True
                End If
                If _b.Length = 0 Then
                    Return True
                End If
                Return False
            End Get
        End Property

        ''' <summary>
        ''' allowed step interval, in bytes, for this data; if 0, no limit
        ''' </summary>
        Friend Property StepBytes() As Integer
            Get
                Return _StepBytes
            End Get
            Set(ByVal Value As Integer)
                _StepBytes = Value
            End Set
        End Property

        ''' <summary>
        ''' allowed step interval, in bits, for this data; if 0, no limit
        ''' </summary>
        Friend Property StepBits() As Integer
            Get
                Return _StepBytes * 8
            End Get
            Set(ByVal Value As Integer)
                _StepBytes = Value \ 8
            End Set
        End Property

        ''' <summary>
        ''' minimum number of bytes allowed for this data; if 0, no limit
        ''' </summary>
        Friend Property MinBytes() As Integer
            Get
                Return _MinBytes
            End Get
            Set(ByVal Value As Integer)
                _MinBytes = Value
            End Set
        End Property

        ''' <summary>
        ''' minimum number of bits allowed for this data; if 0, no limit
        ''' </summary>
        Friend Property MinBits() As Integer
            Get
                Return _MinBytes * 8
            End Get
            Set(ByVal Value As Integer)
                _MinBytes = Value \ 8
            End Set
        End Property

        ''' <summary>
        ''' maximum number of bytes allowed for this data; if 0, no limit
        ''' </summary>
        Friend Property MaxBytes() As Integer
            Get
                Return _MaxBytes
            End Get
            Set(ByVal Value As Integer)
                _MaxBytes = Value
            End Set
        End Property

        ''' <summary>
        ''' maximum number of bits allowed for this data; if 0, no limit
        ''' </summary>
        Friend Property MaxBits() As Integer
            Get
                Return _MaxBytes * 8
            End Get
            Set(ByVal Value As Integer)
                _MaxBytes = Value \ 8
            End Set
        End Property

        ''' <summary>
        ''' Returns the byte representation of the data; 
        ''' This will be padded to MinBytes and trimmed to MaxBytes as necessary!
        ''' </summary>
        Friend Property Bytes() As Byte()
            Get
                If _b Is Nothing Then
                    Return Array.Empty(Of Byte)()
                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())
                _b = Value
            End Set
        End Property

        ''' <summary>
        ''' Sets or returns text representation of bytes using UTF8 encoding
        ''' </summary>
        Friend Property Text() As String
            Get
                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 _encoding.GetString(_b, 0, i)
                    End If
                    Throw
                End Try
            End Get
            Set(ByVal Value As String)
                If Value Is Nothing Then
                    _b = Array.Empty(Of Byte)()
                Else
                    _b = _encoding.GetBytes(Value)
                End If
            End Set
        End Property

        ''' <summary>
        ''' Sets or returns Hex string representation of this data
        ''' </summary>
        Friend Property Hex() As String
            Get
                Return Utils.ToHex(_b)
            End Get
            Set(ByVal Value As String)
                _b = Utils.FromHex(Value)
            End Set
        End Property

        ''' <summary>
        ''' Sets or returns Base64 string representation of this data
        ''' </summary>
        Friend Property Base64() As String
            Get
                Return Utils.ToBase64(_b)
            End Get
            Set(ByVal Value As String)
                _b = Utils.FromBase64(Value)
            End Set
        End Property

    End Class

#End Region

#Region " Utils "

    ''' <summary>
    ''' Friend class for shared utility methods used by multiple Encryption classes
    ''' </summary>
    Friend Class Utils

        ''' <summary>
        ''' converts an array of bytes to a string Hex representation
        ''' </summary>
        Friend Shared Function ToHex(ByVal ba() As Byte) As String
            If ba Is Nothing OrElse ba.Length = 0 Then
                Return ""
            End If
            Const HexFormat As String = "{0:X2}"
            Dim sb As New StringBuilder
            For Each b As Byte In ba
                sb.Append(String.Format(HexFormat, b))
            Next
            Return sb.ToString
        End Function

        ''' <summary>
        ''' converts from a string Hex representation to an array of bytes
        ''' </summary>
        Friend Shared Function FromHex(ByVal hexEncoded As String) As Byte()
            If hexEncoded Is Nothing OrElse hexEncoded.Length = 0 Then
                Return Nothing
            End If
            Try
                Dim l As Integer = Convert.ToInt32(hexEncoded.Length / 2)
                Dim b(l - 1) As Byte
                For i As Integer = 0 To l - 1
                    b(i) = Convert.ToByte(hexEncoded.Substring(i * 2, 2), 16)
                Next
                Return b
            Catch ex As Exception
                Throw New System.FormatException("The provided string does not appear to be Hex encoded:" &
                    Environment.NewLine & hexEncoded & Environment.NewLine, ex)
            End Try
        End Function

        ''' <summary>
        ''' converts from a string Base64 representation to an array of bytes
        ''' </summary>
        Friend Shared Function FromBase64(ByVal base64Encoded As String) As Byte()
            If base64Encoded Is Nothing OrElse base64Encoded.Length = 0 Then
                Return Nothing
            End If
            Try
                Return Convert.FromBase64String(base64Encoded)
            Catch ex As System.FormatException
                Throw New System.FormatException("The provided string does not appear to be Base64 encoded:" &
                    Environment.NewLine & base64Encoded & Environment.NewLine, ex)
            End Try
        End Function

        ''' <summary>
        ''' converts from an array of bytes to a string Base64 representation
        ''' </summary>
        Friend Shared Function ToBase64(ByVal b() As Byte) As String
            If b Is Nothing OrElse b.Length = 0 Then
                Return ""
            End If
            Return Convert.ToBase64String(b)
        End Function

    End Class

#End Region

End Namespace
