ProtectStrings.psm1

### --- PUBLIC FUNCTIONS --- ###
#Region - Export-MasterPassword.ps1
Function Export-MasterPassword {
    <#
    .Synopsis
    Export the currently set Master Password to a text file
    .Description
    Function to retrieve the currently set master password and export it to a text file for transportation between systems or backup.
    Similar in end result to generating an AES key and saving it to file.
    .Parameter FilePath
    Destination full path (including file name) for exported AES Key.
    .EXAMPLE
    PS C:\> Export-MasterPassword -FilePath C:\temp\keyfile.txt
 
 
    this will convert the current session AES key from a SecureString object to its raw byte values, encode in Base64
    and export it to a file called keyfile.txt
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [validatescript({
            if( -not ($_.DirectoryName | test-path) ){
                throw "Folder does not exist"
                }
                return $true
        })]
        [Alias('Path')]
        [System.IO.FileInfo]$FilePath
    )

    Begin {
        $SecureAESKey = Try {
            $Global:AESMP
        } Catch {
            # do nothing
        }
    }

    Process {
        if ($SecureAESKey) {
            Write-Verbose "Stored AES key found"
            $ClearTextAESKey = ConvertFrom-SecureStringToPlainText $SecureAESKey
            $AESKey = ConvertTo-Bytes -InputString $ClearTextAESKey -Encoding Unicode
            Write-Verbose "Converting to Base64 before export"
            $EncodedKey = [System.Convert]::ToBase64String($AESKey)
            Write-Verbose "Saving to $Filepath with Encoded key:"
            Write-Debug "$EncodedKey"
            Out-File -FilePath $FilePath -InputObject $EncodedKey -Force
        } else {
            Write-Warning "No key found to export"
        }
    }

}
#Region - Get-MasterPassword.ps1
Function Get-MasterPassword {
    <#
    .Synopsis
    Returns the saved MasterPassword derived key.
    .Description
    This function is mostly used to verify that a MasterPassword is currently stored. It returns a SecureString object with the current stored AES key.
    .EXAMPLE
    PS C:\> Get-MasterPassword
 
    does stuff
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    #>

    [cmdletbinding()]
    Param (
    )
    Write-Verbose "Checking for stored AES key"
    Get-AESMPVariable -Boolean

}
#Region - Import-MasterPassword.ps1
Function Import-MasterPassword {
    <#
    .Synopsis
    Import a previously exported master password from a text file
    .Description
    Function to import a previously exported master password keyfile and save it in the current session as the master password.
    .Parameter FilePath
    Destination full path (including file name) for the file containing the exported AES Key.
    .EXAMPLE
    PS C:\> Import-MasterPassword -FilePath C:\temp\keyfile.txt
 
 
    This will important the key from keyfile.txt and store it in the current Powershell session as the Master Password.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [validatescript({
            if( -not ($_ | test-path) ){
                throw "File does not exist"
                }
            if(-not ( $_ | test-path -PathType Leaf) ){
                throw "The -FilePath argument must be a file"
                }
                return $true
        })]
        [Alias('Path')]
        [System.IO.FileInfo]$FilePath
    )

    Begin {
        Try {
            Write-Verbose "Retreiving file content from: $FilePath"
            $EncodedKey = Get-Content -Path $FilePath -ErrorAction Stop
        } Catch {
            Write-Error $_
        }
    }

    Process {
        If ($EncodedKey) {
            $AESKey = [System.Convert]::FromBase64String($EncodedKey)
            $ClearTextAESKey = ConvertFrom-Bytes -InputBytes $AESKey -Encoding Unicode
            Write-Verbose "Storing AES Key to current session"
            $SecureAESKey = ConvertTo-SecureString -String $ClearTextAESKey -AsPlainText -Force
            Set-AESMPVariable -MPKey $SecureAESKey
        }
    }

}
#Region - Protect-String.ps1
Function Protect-String {
    <#
    .Synopsis
    Encrypt a provided string with DPAPI or AES 256-bit encryption and return the cipher text.
    .Description
    This function will encrypt provided string text with either Microsoft's DPAPI or AES 256-bit encryption. By default it will use DPAPI unless specified.
    Returns a string object of Base64 encoded text.
    .Parameter InputString
    This is the string text you wish to protect with encryption. Can be provided via the pipeline.
    .Parameter Encryption
    Specify either DPAPI or AES encryption. DPAPI is the default if not specified.
    .EXAMPLE
    PS C:\> Protect-String "Secret message"
    eyJFbmNyeXB0aW9uIjoiRFBBUEkiLCJDaXBoZXJUZXh0IjoiMDEwMDAwMDBkMDhjOWRkZjAxMTVkMTExOGM3YTAwYzA0ZmMyOTdlYjAxMDAwMDAwODRkMTVhY2QwZjk5ZDM0NDllNzE5MTkwZGI0YzY2ZWUwMDAwMDAwMDAyMDAwMDAwMDAwMDAzNjYwMDAwYzAwMDAwMDAxMDAwMDAwMGMyNjFhZTY5YThjZjdlMTI0ZTJmZWI3MmVmMTk3YmRlMDAwMDAwMDAwNDgwMDAwMGEwMDAwMDAwMTAwMDAwMDA4NjUxZWJjZWY4MTE4MzEzMzljNDMyNjA5OWUxZWY3ZDIwMDAwMDAwZGQ3MDUyNGFkZGZlMmM5YzQyMDlhZDc2NjYzZTlhMzgxMTBjNDJkMjk3ZDNhOGQ2OGY4MGI1NDU0YTIxNTUyZjE0MDAwMDAwZThmYjFmY2YyMzYyM2U4NjRmMDliMzA1ZmI4ZTM1ZWRkMjBmNzU2NCIsIkRQQVBJSWRlbnRpdHkiOiJMTklQQzIwMzQ3NExcXEJvZGV0dEMifQ==
 
    This command will encrypt the provided string with DPAPI encryption and return the encoded cipher text.
    .EXAMPLE
    PS C:\> Protect-String "Secret message" -Encryption AES
    Enter Master Password: ********
    eyJFbmNyeXB0aW9uIjoiQUVTIiwiQ2lwaGVyVGV4dCI6IktUU2RYVG9tREt0M1N5eFN0OGsveGtxc2xjTjhseUZMQTllMDlWQWdkVTA9IiwiRFBBUElJZGVudGl0eSI6IiJ9
 
    This command will encrypt the provided string with AES 256-bit encryption. If no Master Password is found in the current session (set with Set-MasterPassword) then it will prompt for one to be set.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    Version: 1.1
    Author: C. Bodett
    Creation Date: 5/12/2022
    Purpose/Change: changed to Generic List from ArrayList
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [String]$InputString,
        [Parameter(Mandatory = $false, Position = 1)]
        [ValidateSet("DPAPI","AES")]
        [String]$Encryption = "DPAPI"
    )
    
    Begin {
        Write-Verbose "Encryption Type: $Encryption"
        If ($Encryption -eq "AES") {
            Write-Verbose "Retrieving Master Password key"
            $SecureAESKey = Get-AESMPVariable
            $ClearTextAESKey = ConvertFrom-SecureStringToPlainText $SecureAESKey
            $AESKey = ConvertTo-Bytes -InputString $ClearTextAESKey -Encoding Unicode
        }
        $OutputString = [System.Collections.Generic.List[String]]::New()
    }

    Process {
        Switch ($Encryption) {
            "DPAPI" {
                Try {
                    Write-Verbose "Converting string text to a SecureString object"
                    $ConvertedString = ConvertTo-SecureString $InputString -AsPlainText -Force | ConvertFrom-SecureString
                    $CipherObject = New-CipherObject -Encryption "DPAPI" -CipherText $ConvertedString
                    $CipherObject.DPAPIIdentity = Get-DPAPIIdentity
                    Write-Debug "DPAPI Identity: $($CipherObject.DPAPIIdentity)"
                    $JSONObject = ConvertTo-Json -InputObject $CipherObject -Compress
                    $JSONBytes = ConvertTo-Bytes -InputString $JSONObject -Encoding UTF8
                    $EncodedOutput = [System.Convert]::ToBase64String($JSONBytes)
                    $OutputString.add($EncodedOutput)
                } Catch {
                    Write-Error $_
                }
            }
            "AES" {
                Try {
                    Write-Verbose "Encrypting string text with AES 256-bit"
                    $ConvertedString = ConvertTo-AESCipherText -InputString $InputString -Key $AESKey -ErrorAction Stop
                    $CipherObject = New-CipherObject -Encryption "AES" -CipherText $ConvertedString
                    $JSONObject = ConvertTo-Json -InputObject $CipherObject -Compress
                    $JSONBytes = ConvertTo-Bytes -InputString $JSONObject -Encoding UTF8
                    $EncodedOutput = [System.Convert]::ToBase64String($JSONBytes)
                    $OutputString.add($EncodedOutput)
                } Catch {
                    Write-Error $_
                }
            }
        }
    }

    End {
        Write-Verbose "Protection complete. Returning $($OutputString.count) objects"
        Return $OutputString
    }
}
#Region - Remove-MasterPassword.ps1
Function Remove-MasterPassword {
    <#
    .Synopsis
    Removes the Master Password stored in the current session
    .Description
    The Master Password is stored as a Secure String object in memory for the current session. Should you wish to clear it manually you can do so with this function.
    .EXAMPLE
    PS C:\> Remove-MasterPassword
 
    This will erase the currently saved master password.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    #>

    [cmdletbinding()]
    Param (
    )
    Write-Verbose "Removing master password from current session"
    Clear-AESMPVariable

}
#Region - Set-AESKeyConfig.ps1
Function Set-AESKeyConfig {
    <#
    .Synopsis
    Function to write settings for use with PBKDF2 to create an AES Key
    .Description
    Allows custom configuration of the parameters associated with the PBKDF2 generation. Namely the Salt, number of Iterations, and Hash type.
    .Parameter Salt
    Provide a custom string to be used as the Salt bytes in PBKDF2 generation. Must be at least 8 characters in length.
    .Parameter Iterations
    Specify the number of iterations PBKDF2 should use. 1000 is the default.
    .Parameter Hash
    Specify the Hash type used with PBKDF2. Accetable values are: 'MD5','SHA1','SHA256','SHA384','SHA512'.
    .Parameter Defaults
    Switch parameter that resets the AES Key Config file to default values.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 06/09/2022
    Purpose/Change: initial function development
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $false)]
        [ValidateScript({
            if ($_.Length -lt 8) {
                Throw "Salt must be at least 8 bytes in length"
            } else {
                return $true
            }
        })]
        [String]$Salt,
        [Parameter(Mandatory = $false)]
        [Int32]$Iterations,
        [Parameter(Mandatory = $false)]
        [ValidateSet('MD5','SHA1','SHA256','SHA384','SHA512')]
        [String]$Hash,
        [Switch]$Defaults
    )

    Process {
        $ConfigFileName = "ProtectStringsConfig.psd1"
        $ConfigFilePath = Join-Path -Path $Env:LOCALAPPDATA  -ChildPath $ConfigFileName
        Write-Verbose "Config file path: $($ConfigFilePath)"
        if (-not (Test-Path  $ConfigFilePath) -or $Defaults) {
            Write-verbose "Defaults parameter provided or no config file present. Creating defaults"
            $DefaultConfigData = @"
@{
    Salt = '|½ÁôøwÚ♀å>~I©kâ—„=ýñíî'
    Iterations = 310000
    Hash = 'SHA256'
}
"@

            Try {
                $DefaultConfigData | Out-File -FilePath $ConfigFilePath -ErrorAction Stop
            } Catch {
                Throw $_
            }
        }

        if ($($PSVersionTable.PSVersion.Major) -lt 5) {
            $Settings = Import-LocalizedData -BaseDirectory $ENV:LOCALAPPDATA -FileName $ConfigFileName
        } else {
            $Settings = Import-PowerShellDataFile -Path $ConfigFilePath
        }

        Switch ($PSBoundParameters.Keys) {
            'Salt' {$Settings.Salt = $Salt}
            'Iterations' {$Settings.Iterations = $Iterations}
            'Hash' {$Settings.Hash = $Hash}
        }

        $VMsg = @"
`r`n Saving settings...
        Salt........: $($Settings.Salt)
        Iterations..: $($Settings.Iterations)
        Hash........: $($Settings.Hash)
"@

        Write-Verbose $VMsg

        $OutString = "@{{`n{0}`n}}" -f ($(
                        ForEach ($Key in @($Settings.Keys)) {
                            If ($Settings[$Key] -is [Int32]) {
                                " $Key = " + ($Settings[$Key])
                            } Else {
                                    " $Key = " + "'{0}'" -f ($Settings[$Key])
                                }
                        }) -split "`n" -join "`n")
        $OutString | Out-File -FilePath $ConfigFilePath -ErrorAction Stop
    }
}
#Region - Set-MasterPassword.ps1
Function Set-MasterPassword {
    <#
    .Synopsis
    Securely retrieves from console the desired master password and saves it for the current session.
    .Description
    Takes a user provided master password as a secure string object and creates a unique AES 256 bit key from it and stores that as a SecureString object in memory for the current session.
    .Parameter MasterPassword
    If you already have a password in a variable as a SecureString object you can pass it to this function.
    .EXAMPLE
    PS C:\> Set-MasterPassword
    Enter Master Password: ********
 
    In this example you will be prompted to provide a password. It will then be silently stored in the current session.
    .EXAMPLE
    PS C:\> $Pass = Read-Host -AsSecureString
    *****************
    PS C:\> Set-MasterPassword -MasterPassword $Pass
 
 
    Here the desired master password is saved beforehand in the variable $Pass and then passed to the Set-MasterPassword function.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $false, Position = 0)]
        [SecureString]$MasterPassword 
    )

    If (-not ($MasterPassword)) {
        $MasterPassword = Read-Host -Prompt "Enter Master Password" -AsSecureString
    }

    Try {
        Write-Verbose "Generating a 256-bit AES key from provided password"
        $SecureAESKey = ConvertTo-AESKey $MasterPassword
    } Catch {
        throw $_
    }
    Write-Verbose "Storing key for use within this session. Can be removed with Remove-MasterPassword"
    Set-AESMPVariable -MPKey $SecureAESKey
}
#Region - Unprotect-String.ps1
Function UnProtect-String {
    <#
    .Synopsis
    Decrypt a provided string using either DPAPI or AES encryption
    .Description
    This function will decode the provided protected text, and automatically determine if it was encrypted using DPAPI or AES encryption.
    If no master password has been set it will prompt for one.
    If there is a decryption problem it will notify.
    .Parameter InputString
    This is the protected text previously produced by the ProtectStrings module. Encryption type will be automatically determined.
    .EXAMPLE
    PS C:\> Protect-String "Secret message"
    eyJFbmNyeXB0aW9uIjoiRFBBUEkiLCJDaXBoZXJUZXh0IjoiMDEwMDAwMDBkMDhjOWRkZjAxMTVkMTExOGM3YTAwYzA0ZmMyOTdlYjAxMDAwMDAwODRkMTVhY2QwZjk5ZDM0NDllNzE5MTkwZGI0YzY2ZWUwMDAwMDAwMDAyMDAwMDAwMDAwMDAzNjYwMDAwYzAwMDAwMDAxMDAwMDAwMGMyNjFhZTY5YThjZjdlMTI0ZTJmZWI3MmVmMTk3YmRlMDAwMDAwMDAwNDgwMDAwMGEwMDAwMDAwMTAwMDAwMDA4NjUxZWJjZWY4MTE4MzEzMzljNDMyNjA5OWUxZWY3ZDIwMDAwMDAwZGQ3MDUyNGFkZGZlMmM5YzQyMDlhZDc2NjYzZTlhMzgxMTBjNDJkMjk3ZDNhOGQ2OGY4MGI1NDU0YTIxNTUyZjE0MDAwMDAwZThmYjFmY2YyMzYyM2U4NjRmMDliMzA1ZmI4ZTM1ZWRkMjBmNzU2NCIsIkRQQVBJSWRlbnRpdHkiOiJMTklQQzIwMzQ3NExcXEJvZGV0dEMifQ==
 
    This command will encrypt the provided string with DPAPI encryption and return the encoded cipher text.
 
    PS C:\> Unprotect-String 'eyJFbmNyeXB0aW9uIjoiRFBBUEkiLCJDaXBoZXJUZXh0IjoiMDEwMDAwMDBkMDhjOWRkZjAxMTVkMTExOGM3YTAwYzA0ZmMyOTdlYjAxMDAwMDAwODRkMTVhY2QwZjk5ZDM0NDllNzE5MTkwZGI0YzY2ZWUwMDAwMDAwMDAyMDAwMDAwMDAwMDAzNjYwMDAwYzAwMDAwMDAxMDAwMDAwMGMyNjFhZTY5YThjZjdlMTI0ZTJmZWI3MmVmMTk3YmRlMDAwMDAwMDAwNDgwMDAwMGEwMDAwMDAwMTAwMDAwMDA4NjUxZWJjZWY4MTE4MzEzMzljNDMyNjA5OWUxZWY3ZDIwMDAwMDAwZGQ3MDUyNGFkZGZlMmM5YzQyMDlhZDc2NjYzZTlhMzgxMTBjNDJkMjk3ZDNhOGQ2OGY4MGI1NDU0YTIxNTUyZjE0MDAwMDAwZThmYjFmY2YyMzYyM2U4NjRmMDliMzA1ZmI4ZTM1ZWRkMjBmNzU2NCIsIkRQQVBJSWRlbnRpdHkiOiJMTklQQzIwMzQ3NExcXEJvZGV0dEMifQ=='
    Secret message
 
    Feeding the previously output protected text to Unprotect-String will decrypt it and return the original string text.
    .EXAMPLE
    PS C:\> Protect-String "Secret message" -Encryption AES
    Enter Master Password: ********
    eyJFbmNyeXB0aW9uIjoiQUVTIiwiQ2lwaGVyVGV4dCI6IktUU2RYVG9tREt0M1N5eFN0OGsveGtxc2xjTjhseUZMQTllMDlWQWdkVTA9IiwiRFBBUElJZGVudGl0eSI6IiJ9
 
    This command will encrypt the provided string with AES 256-bit encryption. If no Master Password is found in the current session (set with Set-MasterPassword) then it will prompt for one to be set.
 
    PS C:\> Clear-MasterPassword
    PS C:\> Unprotect-String 'eyJFbmNyeXB0aW9uIjoiQUVTIiwiQ2lwaGVyVGV4dCI6IktUU2RYVG9tREt0M1N5eFN0OGsveGtxc2xjTjhseUZMQTllMDlWQWdkVTA9IiwiRFBBUElJZGVudGl0eSI6IiJ9'
    Enter Master Password: ********
    Secret message
 
    Clearing the master password from the sessino, providing the previously protected text to Unprotect-String will prompt for a master password and then decrypt the text and return the original string text.
    .NOTES
    Version: 1.0
    Author: C. Bodett
    Creation Date: 3/28/2022
    Purpose/Change: Initial function development
    Version: 1.1
    Author: C. Bodett
    Creation Date: 5/12/2022
    Purpose/Change: Fixed processing to handle pipeline input. Changed from Arraylist to Generic list
    Version: 1.2
    Author: C. Bodett
    Creation Date: 6/7/2022
    Purpose/Change: Redid Process block to accomodate new ConvertTo-CipherBlock function for better error handling on input text
    #>

    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [String]$InputString
    )
    
    Begin {
        $OutputString = [System.Collections.Generic.List[String]]::New()
    }

    Process {
        Write-Verbose "Converting supplied text to a Cipher Object for ProtectStrings"
        $CipherObject = Try {
            ConvertTo-CipherObject $InputString -ErrorAction Stop
        } Catch {
            Write-Debug $_
            Write-Warning "Supplied text could not be converted to a Cipher Object. Verify that it was produced by Protect-String."
            return
        }
        Write-Verbose "Encryption type: $($CipherObject.Encryption)"
        If ($CipherObject.Encryption -eq "AES") {
            $SecureAESKey = Get-AESMPVariable
            $ClearTextAESKey = ConvertFrom-SecureStringToPlainText $SecureAESKey
            $AESKey = ConvertTo-Bytes -InputString $ClearTextAESKey -Encoding Unicode
        }
        Switch ($CipherObject.Encryption) {
            "DPAPI" {
                Try {
                    Write-Verbose "Attempting to create a SecureString object from DPAPI cipher text"
                    $SecureStringObj = ConvertTo-SecureString -String $CipherObject.CipherText -ErrorAction Stop
                    $ConvertedString = ConvertFrom-SecureStringToPlainText -StringObj $SecureStringObj -ErrorAction Stop
                    $CorrectPassword = $true
                    $OutputString.add($ConvertedString)
                } Catch {
                    Write-Warning "Unable to decrypt as this user on this machine"
                    Write-Verbose "String protected by Identity: $($CipherObject.DPAPIIdentity)"
                    $CorrectPassword = $false
                }
            }
            "AES" {
                Try {
                    Write-Verbose "Attempting to decrypt AES cipher text"
                    $ConvertedString = ConvertFrom-AESCipherText -InputCipherText $CipherObject.CipherText -Key $AESKey -ErrorAction Stop
                    $CorrectPassword = $true
                    $OutputString.add($ConvertedString)
                } Catch {
                    Write-Warning "Incorrect Master Password. Please try again"
                    $CorrectPassword = $false
                    Clear-AESMPVariable
                }
            }
        }

    }

    End {
        If ($CorrectPassword) {
            Write-Verbose "Unprotect complete. Returning $($OutputString.count) objects"
            Return $OutputString
        }
    }
}
### --- PRIVATE FUNCTIONS --- ###
#Region - Clear-AESMPVariable.ps1
<#
.Synopsis
Clear the global variable of the previously stored master password/key
.NOTES
Version: 1.0
Author: C. Bodett
Creation Date: 03/28/2022
Purpose/Change: Initial function development
#>

Function Clear-AESMPVariable {
    [cmdletbinding()]
    Param (
    )

    Process {
        Write-Verbose "Clearing global variable where AES key is stored"
        Remove-Variable -Name "AESMP" -Force -Scope Global -ErrorAction SilentlyContinue
    }
}
#Region - ConvertFrom-AESCipherText.ps1
<#
.Synopsis
Convert input AES Cipher text to plain text
#>

Function ConvertFrom-AESCipherText {
        [cmdletbinding()]
        param(
            [Parameter(ValueFromPipeline = $true,Position = 0,Mandatory = $true)]
            [string]$InputCipherText,
            [Parameter(Position = 1,Mandatory = $true)]
            [Byte[]]$Key
        )
    
        Process {
            Write-Verbose "Creating new AES Cipher object with supplied key"
            $AESCipher = Initialize-AESCipher -Key $Key
            Write-Verbose "Convert input text from Base64"
            $EncryptedBytes = [System.Convert]::FromBase64String($InputCipherText)
            Write-Verbose "Using the first 16 bytes as the initialization vector"
            $AESCipher.IV = $EncryptedBytes[0..15]
            Write-Verbose "Decrypting AES cipher text"
            $Decryptor = $AESCipher.CreateDecryptor()
            $UnencryptedBytes = $Decryptor.TransformFinalBlock($EncryptedBytes, 16, $EncryptedBytes.Length - 16)
            Write-Verbose "Converting from bytes to string text using UTF8 encoding"
            $ConvertedString = ConvertFrom-Bytes -InputBytes $UnencryptedBytes -Encoding UTF8
        }
        End {
            Write-Verbose "Disposing of AES Cipher object"
            $AESCipher.Dispose()
            return $ConvertedString
        }

}
#Region - ConvertFrom-Bytes.ps1
<#
.Synopsis
Gets string from supplied input bytes
#>

Function ConvertFrom-Bytes{
        [cmdletbinding()]
        param(
        [Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)]
        [Byte[]]$InputBytes,
        [Parameter(Position=1)]
        [ValidateSet('UTF8','Unicode')]
        [string]$Encoding = 'Unicode'
        )
    
        Write-Verbose "Converting from bytes to string using $Encoding encoding"
        $OutputString = [System.Text.Encoding]::$Encoding.GetString($InputBytes)
        return $OutputString
    }
    
#Region - ConvertFrom-SecureStringToPlainText.ps1
Function ConvertFrom-SecureStringToPlainText {
    [cmdletbinding()]
    param(
        [parameter(Position=0,HelpMessage="Must provide a SecureString object",
        ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [ValidateNotNull()]
        [System.Security.SecureString]$StringObj
        )
    Process {
        $BSTR = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($StringObj)
        $PlainText = [Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
        [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR)
    }
    End {
        $PlainText
    }
} 
#Region - ConvertTo-AESCipherText.ps1
<#
.Synopsis
Convert input string to AES encrypted cipher text
#>

Function ConvertTo-AESCipherText {
    [cmdletbinding()]
    param(
        [Parameter(ValueFromPipeline = $true,Position = 0,Mandatory = $true)]
        [string]$InputString,
        [Parameter(Position = 1,Mandatory = $true)]
        [Byte[]]$Key
    )

    Process {
            $InitializationVector = [System.Byte[]]::new(16)
            Get-RandomBytes -Bytes $InitializationVector
            $AESCipher = Initialize-AESCipher -Key $Key
            $AESCipher.IV = $InitializationVector
            $ClearTextBytes = ConvertTo-Bytes -InputString $InputString -Encoding UTF8
            $Encryptor =  $AESCipher.CreateEncryptor()
            $EncryptedBytes = $Encryptor.TransformFinalBlock($ClearTextBytes, 0, $ClearTextBytes.Length)
            [byte[]]$FullData = $AESCipher.IV + $EncryptedBytes
            $ConvertedString = [System.Convert]::ToBase64String($FullData)
            $DebugInfo = @"
`r`n Input String Length : $($InputString.Length)
  Initialization Vector : $($InitializationVector.Count) Bytes
  Text Encoding : UTF8
  Output Encoding : Base64
"@

            Write-Debug $DebugInfo
        }

    End {
        $AESCipher.Dispose()
        return $ConvertedString
        }

}
#Region - ConvertTo-AESKey.ps1
<#
.Synopsis
Function to convert a SecureString object to a unique 32 Byte array for use with AES 256bit encryption
.NOTES
Version: 1.0
Author: C. Bodett
Creation Date: 03/24/2022
Purpose/Change: Initial function development
Version: 2.0
Author: C. Bodett
Creation Date: 03/27/2022
Purpose/Change: Abandoned my homebrew method of password based key derivation and leveraged an existing .NET
Version: 2.1
Author: C. Bodett
Creation Date: 04/01/2022
Purpose/Change: Changed Salt encoding from UTF8 to Unicode just to be the same as the SecureString method. Apparently SecureString deals exclusively with Unicode.
Version: 2.2
Author: C. Bodett
Creation Date: 06/09/2022
Purpose/Change: Changed verbose messages
#>

Function ConvertTo-AESKey {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [System.Security.SecureString]$SecureStringInput,
        #[Parameter(Mandatory = $false)]
        #[String]$Salt = '|½ÁôøwÚ♀å>~I©kâ—„=ýñíî',
        #[Parameter(Mandatory = $false)]
        #[Int32]$Iterations = 1000,
        [Parameter(Mandatory = $false)]
        [Switch]$ByteArray
    )

    Process {
        Write-Verbose "Retrieving AESKey settings"
        $Settings = Get-AESKeyConfig 
        Write-Verbose "Converting Salt to byte array"
        $SaltBytes = ConvertTo-Bytes -InputString $($Settings.Salt) -Encoding UTF8
        # Temporarily plaintext our SecureString password input. There's really no way around this.
        Write-Verbose "Converting supplied SecureString text to plaintext"
        $Password = ConvertFrom-SecureStringToPlainText $SecureStringInput
        # Create our PBKDF2 object and instantiate it with the necessary values
        $VMsg = @"
`r`n Creating PBKDF2 Object
        Password....: $("*"*$($Password.Length))
        Salt........: $($Settings.Salt)
        Iterations..: $($Settings.Iterations)
        Hash........: $($Settings.Hash)
"@

        Write-Verbose $VMsg
        $PBKDF2 = New-Object Security.Cryptography.Rfc2898DeriveBytes  -ArgumentList @($Password, $SaltBytes, $($Settings.Iterations), $($Settings.Hash))
        # Generate our AES Key
        Write-Verbose "Generating 32 byte key"
        $Key = $PBKDF2.GetBytes(32)
        # If the ByteArray switch is provided, return a plaintext byte array, otherwise turn our AES key in to a SecureString object
        If ($ByteArray) {
            Write-Verbose "ByteArray switch provided. Returning clear text array of bytes"
            $KeyOutput = $Key
        } Else {
            # Convert the key bytes to a unicode string -> SecureString
            $KeyBytesUnicode = ConvertFrom-Bytes -InputBytes $Key -Encoding Unicode
            $KeyAsSecureString = ConvertTo-SecureString -String $keyBytesUnicode -AsPlainText -Force
            $KeyOutput = $KeyAsSecureString
        }
        return $KeyOutput
    }
}
#Region - ConvertTo-Bytes.ps1
<#
.Synopsis
Gets bytes from supplied input string
#>

Function ConvertTo-Bytes{
    [cmdletbinding()]
    param(
    [Parameter(ValueFromPipeline=$true,Position=0,Mandatory=$true)]
    [string]$InputString,
    [Parameter(Position=1)]
    [ValidateSet('UTF8','Unicode')]
    [string]$Encoding = 'Unicode'
    )

    Write-Debug "Converting Input text to bytes with $Encoding encoding"
    Write-Debug "Input Text: $InputString"
    $Bytes = [System.Text.Encoding]::$Encoding.GetBytes($InputString)
    return $Bytes
}
#Region - ConvertTo-CipherObject.ps1
<#
.Synopsis
Convert output from Protect-String in to a Cipher Object
#>

Function ConvertTo-CipherObject {
    [cmdletbinding()]
    Param (
        [String]$B64String
    )

    $JSONBytes = Try {
        [System.Convert]::FromBase64String($B64String)
    } Catch {
        Write-Verbose "Unable to decode Input String. Expecting Base64"
        throw
    }

    $JSON = Try {
        ConvertFrom-Bytes -InputBytes $JSONBytes -Encoding UTF8
    } Catch {
        Write-Verbose "Unable to convert bytes with UTF8 encoding"
        throw
    }

    $ObjectData = Try {
        ConvertFrom-Json -InputObject $JSON -ErrorAction Stop
    } Catch {
        Write-Verbose "Unable to convert data from JSON"
        throw
    }
    $CipherObject = Try {
        New-CipherObject -Encryption $($ObjectData.Encryption) -CipherText $($ObjectData.CipherText)
    } Catch {
        Write-Verbose "Unable to create Cipher Object"
        throw
    }
    $CipherObject.DPAPIIdentity = $ObjectData.DPAPIIdentity
    return $CipherObject
}
#Region - Get-AESKeyConfig.ps1
<#
.Synopsis
Function to retrieve settings for use with PBKDF2 to create an AES Key
.NOTES
Version: 1.0
Author: C. Bodett
Creation Date: 06/09/2022
Purpose/Change: initial function development
#>

Function Get-AESKeyConfig {
    [cmdletbinding()]
    Param (
        # No Parameters
    )

    Process {
        $ConfigFileName = "ProtectStringsConfig.psd1"
        $ConfigFilePath = Join-Path -Path $Env:LOCALAPPDATA  -ChildPath $ConfigFileName

        if (-not (Test-Path  $ConfigFilePath)) {
            $DefaultConfigData = @"
@{
    Salt = '|½ÁôøwÚ♀å>~I©kâ—„=ýñíî'
    Iterations = 1000
    Hash = 'SHA256'
}
"@

            Try {
                $DefaultConfigData | Out-File -FilePath $ConfigFilePath -ErrorAction Stop
            } Catch {
                Write-Verbose "Failed to create configuration file for PBKDF2 settings. Temporarily loading defaults for this session."
                $Settings = Invoke-Expression $DefaultConfigData
                return $Settings
            }
        }

        if ($($PSVersionTable.PSVersion.Major) -lt 5) {
            $Settings = Import-LocalizedData -BaseDirectory $ENV:LOCALAPPDATA -FileName $ConfigFileName
        } else {
            $Settings = Import-PowerShellDataFile -Path $ConfigFilePath
        }

        return $Settings
    }
}
#Region - Get-AESMPVariable.ps1
<#
.Synopsis
Get the password derived key to a global session variable for future use.
.NOTES
Version: 1.0
Author: C. Bodett
Creation Date: 03/27/2022
Purpose/Change: Initial function development
Version: 1.1
Author: C. Bodett
Creation Date: 05/12/2022
Purpose/Change: changed logic from if/ifelse to switch statement
#>

Function Get-AESMPVariable {
    [cmdletbinding()]
    Param (
        [Switch]$Boolean
    )

    Process {
        $SecureAESKey = Try {
            $Global:AESMP
        } Catch {
            # do nothing
        }

        <#
        If (-not ($SecureAESKey) -and -not ($Boolean)) {
            Set-MasterPassword
            $SecureAESKey = Get-AESMPVariable
        } ElseIf (-not ($SecureAESKey) -and $Boolean) {
            Write-Verbose "No Master Password key found"
            return $false
        }
        #>

        Switch ('{0}{1}' -f [int][bool]$SecureAESKey,[int][bool]$Boolean) {
            "10" {
                Write-Debug "Master Password key found"
            }
            "11" {
                Write-Debug "Master Password key found"
                return $true
            }
            "01" {
                Write-Debug "No Master Password key found"
                return $false
            }
            "00" {
                Write-Debug "No Master Password key found"
                Set-MasterPassword
                $SecureAESKey = Get-AESMPVariable
            }
        }
        
        return $SecureAESKey

    }
}
#Region - Get-DPAPIIdentity.ps1
<#
.Synopsis
Get the current username and computer name
#>

Function Get-DPAPIIdentity {
    [cmdletbinding()]
    Param (
    )
    
    Write-Debug "Current ENV:Computername : $ENV:COMPUTERNAME"
    Write-Debug "Current ENV:Computername : $ENV:USERNAME"
    Write-Debug "Creating DPAPI Identity information"
    $Output = '{0}\{1}' -f $ENV:COMPUTERNAME,$ENV:USERNAME
    return $Output
}
#Region - Get-RandomBytes.ps1
<#
.Synopsis
A Function to leverage the .NET Random Number Generator
#>

Function Get-RandomBytes {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Byte[]]$Bytes
    )

    $RNG = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
    $RNG.GetBytes($Bytes)

}
#Region - Initialize-AESCipher.ps1
<#
.Synopsis
A Function to initiate the .NET AESCryptoServiceProvider
#>

Function Initialize-AESCipher {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [Byte[]]$Key
    )

    $AESServiceProvider = New-Object System.Security.Cryptography.AesCryptoServiceProvider
    $AESServiceProvider.Key = $Key
    return $AESServiceProvider

}
#Region - New-CipherObject.ps1
<#
.Synopsis
Create a new CipherObject
#>

Function New-CipherObject {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet("DPAPI","AES")]
        [String]$Encryption,
        [Parameter(Mandatory = $true, Position = 1)]
        [String]$CipherText
    )
 
    [CipherObject]::New($Encryption,$CipherText)
    
}
#Region - Set-AESMPVariable.ps1
<#
.Synopsis
Set the password derived key to a global session variable for future use.
.NOTES
Version: 1.0
Author: C. Bodett
Creation Date: 03/27/2022
Purpose/Change: Initial function development
#>

Function Set-AESMPVariable {
    [cmdletbinding()]
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [SecureString]$MPKey
    )

    Process {
        Write-Verbose "Creating new variable globally to store AES Key"
        New-Variable -Name "AESMP" -Value $MPKey -Option AllScope -Scope Global -Force
    }
}
### --- CLASS DEFINITIONS --- ###
#Region - CipherObject.ps1
Class CipherObject {
    [String]  $Encryption
    [String]  $CipherText
    hidden[String]  $DPAPIIdentity
    CipherObject ([String]$Encryption, [String]$CipherText) {
        $this.Encryption = $Encryption
        $this.CipherText = $CipherText
        $this.DPAPIIdentity = $null
    }
}