Functions/New-EncryptedString.ps1

#Requires -Version 3

## Microsoft Function Naming Convention: http://msdn.microsoft.com/en-us/library/ms714428(v=vs.85).aspx

#region Function New-EncryptedString
Function New-EncryptedString
    {
        <#
          .SYNOPSIS
          Encrypts the specified value, creates a randomized encryption key, and optionally, outputs the information the pipeline for further processing, and/or saves the information to a xml.
 
          .DESCRIPTION
          Provides a secure method of using sensitive data within scripts as long as best practices are followed. Keeping decryption keys separate from encrypted data is ideal.
           
          .PARAMETER Value
          Specifies that the value that will be encrypted.
 
          .PARAMETER EncryptionKey
          Specifies the encryption key used to encrypt the string provided within the value parameter. This encryption key WILL BE required for furture decryption! The value must be exactly 16, 24, or 32 characters in length.
           
          .PARAMETER Export
          Specifies that the generated encryption data will be exported.
 
          .PARAMETER ExportPath
          Full path to the file where the encryption data will be saved to.
 
          .PARAMETER Force
          Specifies that the generated encryption data file will be overwritten.
 
          .PARAMETER Unsecure
          Specifies that the encryption key will be included in the export. This would be fine for testing purposes, but generally you want to separate the encryption key from the encrypted data.
 
          .PARAMETER ContinueOnError
          Causes any encountered errors to be considered non-terminating.
 
          .EXAMPLE
          $NewEncryptedStringInfo = New-EncryptedString -Value 'YourStringValue' -Export -Force -Verbose
 
          Write-Output -InputObject ($NewEncryptedStringInfo | Format-List)
   
          .EXAMPLE
          $Null = Add-Type -AssemblyName 'System.Web'
 
          [String]$NewEncryptedString_Value = 'YourStringValue'
          [String]$NewEncryptedString_EncryptionKey = [System.Web.Security.Membership]::GeneratePassword(32, 16)
          [Switch]$NewEncryptedString_Export = $True
          [System.IO.Fileinfo]$NewEncryptedString_ExportPath = "$($Env:Temp.TrimEnd('\'))\New-EncryptedString\EncryptionInfo.xml"
          [Switch]$NewEncryptedString_Force = $True
          [Switch]$NewEncryptedString_Unsecure = $True
          [Switch]$NewEncryptedString_Verbose = $True
 
          [Hashtable]$NewEncryptedStringParameters = @{}
            $NewEncryptedStringParameters.Add('Value', ($NewEncryptedString_Value))
            $NewEncryptedStringParameters.Add('EncryptionKey', ($NewEncryptedString_EncryptionKey))
            $NewEncryptedStringParameters.Add('Export', ($NewEncryptedString_Export))
            $NewEncryptedStringParameters.Add('ExportPath', ($NewEncryptedString_ExportPath))
            $NewEncryptedStringParameters.Add('Force', ($NewEncryptedString_Force))
            $NewEncryptedStringParameters.Add('Unsecure', ($NewEncryptedString_Unsecure))
            $NewEncryptedStringParameters.Add('Verbose', ($NewEncryptedString_Verbose))
 
          $NewEncryptedStringInfo = New-EncryptedString @NewEncryptedStringParameters
 
          Write-Output -InputObject ($NewEncryptedStringInfo)
 
          .EXAMPLE
           Loop through the values in a hashtable and encrypt each value using a randomly generated encryption key. Afterwards, separately export the encrypted data, and decryption key to their respective JSON formatted files.
           The encrypted values can be transported along with the script and the respective decryption key can be retrieved from a remote location.
           An alternative is that both the encrypted data and decryption key can be stored in a remote location.
           This will be useful for ensuring that sensitive data only runs through memory on the endpoint during process execution. Afterwards, the sensitive data is no longer exposed.
           
          [HashTable]$ValuesToEncrypt = [Ordered]@{}
            $ValuesToEncrypt.Add('DomainFQDN', 'mydomain.net')
            $ValuesToEncrypt.Add('DomainNBN', 'MyDomain')
            $ValuesToEncrypt.Add('UserName', 'MyReadOnlyUser')
            $ValuesToEncrypt.Add('Password', 'MyReadOnlyUserPassword')
 
          [System.Collections.ArrayList]$OutputObject = @()
 
          [System.IO.DirectoryInfo]$EncryptionInfoDirectory = "$($Env:Temp.TrimEnd('\'))\New-EncryptedString"
 
          [System.IO.DirectoryInfo]$EncryptedDataDirectory = "$($EncryptionInfoDirectory.FullName)\EncryptedData"
          If ($EncryptedDataDirectory.Exists -eq $False) {$Null = [System.IO.Directory]::CreateDirectory($EncryptedDataDirectory.FullName)}
 
          [System.IO.DirectoryInfo]$DecryptionKeysDirectory = "$($EncryptionInfoDirectory.FullName)\DecryptionKeys"
          If ($DecryptionKeysDirectory.Exists -eq $False) {$Null = [System.IO.Directory]::CreateDirectory($DecryptionKeysDirectory.FullName)}
 
          ForEach ($Item In ($ValuesToEncrypt.GetEnumerator()))
            {
                [String]$NewEncryptedString_Value = ($Item.Value)
                [Switch]$NewEncryptedString_Verbose = $False
 
                [Hashtable]$NewEncryptedStringParameters = @{}
                  $NewEncryptedStringParameters.Add('Value', ($NewEncryptedString_Value))
                  $NewEncryptedStringParameters.Add('Verbose', ($NewEncryptedString_Verbose))
 
                $NewEncryptedStringInfo = New-EncryptedString @NewEncryptedStringParameters
                 
                [System.IO.FileInfo]$EncryptedDataAsJSONPath = "$($EncryptedDataDirectory.FullName)\$($Item.Key).json"
                [String]$EncryptedDataAsJSON = ConvertTo-JSON -InputObject ($NewEncryptedStringInfo | Select-Object -Property @('EncryptedData'))
                $Null = [System.IO.File]::WriteAllText(($EncryptedDataAsJSONPath.FullName), ($EncryptedDataAsJSON), ([System.Text.Encoding]::Default))
                $EncryptedDataAsJSONHash = (Get-FileHash -Path ($EncryptedDataAsJSONPath.FullName) -Algorithm SHA256).Hash
 
                [System.IO.FileInfo]$DecryptionKeyAsJSONPath = "$($DecryptionKeysDirectory.FullName)\$($Item.Key).json"
                [String]$DecryptionKeyAsJSON = ConvertTo-JSON -InputObject ($NewEncryptedStringInfo | Select-Object -Property @('DecryptionKey'))
                $Null = [System.IO.File]::WriteAllText(($DecryptionKeyAsJSONPath.FullName), ($DecryptionKeyAsJSON), ([System.Text.Encoding]::Default))
                [String]$DecryptionKeyAsJSONHash = (Get-FileHash -Path ($DecryptionKeyAsJSONPath.FullName) -Algorithm SHA256).Hash
               
                $Null = Add-Member -InputObject ($NewEncryptedStringInfo) -Name 'ValueName' -Value ($Item.Name) -MemberType NoteProperty
               
                $Null = Add-Member -InputObject ($NewEncryptedStringInfo) -Name 'EncryptedDataPath' -Value ($EncryptedDataAsJSONPath.FullName) -MemberType NoteProperty
                $Null = Add-Member -InputObject ($NewEncryptedStringInfo) -Name 'EncryptedDataHash' -Value ($EncryptedDataAsJSONHash) -MemberType NoteProperty
               
                $Null = Add-Member -InputObject ($NewEncryptedStringInfo) -Name 'DecryptionKeyPath' -Value ($DecryptionKeyAsJSONPath.FullName) -MemberType NoteProperty
                $Null = Add-Member -InputObject ($NewEncryptedStringInfo) -Name 'DecryptionKeyHash' -Value ($DecryptionKeyAsJSONHash) -MemberType NoteProperty
                             
                $OutputObject += ($NewEncryptedStringInfo)
            }
                     
          Write-Output -InputObject ($OutputObject)
   
          .NOTES
          Do you have processes or scripts that require you to provide a password?
          Against the desires of your security officer, do you have to save those passwords in plain text, in your scripts?
           
          .LINK
          https://www.sqlshack.com/how-to-secure-your-passwords-with-powershell/
        #>

        
        [CmdletBinding(ConfirmImpact = 'Low', SupportsShouldProcess = $True)]
       
        Param
          (        
              [Parameter(Mandatory=$True)]
              [ValidateNotNullOrEmpty()]
              [String]$Value,
              
              [Parameter(Mandatory=$False)]
              [ValidateNotNullOrEmpty()]
              [ValidateScript({($_.Length -eq 16) -or ($_.Length -eq 24) -or ($_.Length -eq 32)})]
              [String]$EncryptionKey,
                                    
              [Parameter(Mandatory=$False, ParameterSetName = 'Export')]
              [Switch]$Export,
        
              [Parameter(Mandatory=$False, ParameterSetName = 'Export')]
              [ValidateNotNullOrEmpty()]
              [ValidatePattern('^.*\.xml$')]
              [System.IO.FileInfo]$ExportPath,
              
              [Parameter(Mandatory=$False, ParameterSetName = 'Export')]
              [Switch]$Force,
              
              [Parameter(Mandatory=$False, ParameterSetName = 'Export')]
              [Switch]$Unsecure,
                                                            
              [Parameter(Mandatory=$False)]
              [Switch]$ContinueOnError        
          )
                    
        Begin
          {
              [ScriptBlock]$ErrorHandlingDefinition = {
                                                          If ([String]::IsNullOrEmpty($_.Exception.Message)) {$ExceptionMessage = "$($_.Exception.Errors.Message -Join "`r`n`r`n")"} Else {$ExceptionMessage = "$($_.Exception.Message)"}
          
                                                          [String]$ErrorMessage = "[Error Message: $($ExceptionMessage)]`r`n`r`n[ScriptName: $($_.InvocationInfo.ScriptName)]`r`n[Line Number: $($_.InvocationInfo.ScriptLineNumber)]`r`n[Line Position: $($_.InvocationInfo.OffsetInLine)]`r`n[Code: $($_.InvocationInfo.Line.Trim())]"

                                                          If ($ContinueOnError.IsPresent -eq $True)
                                                            {
                                                                Write-Warning -Message ($ErrorMessage)
                                                            }
                                                          ElseIf ($ContinueOnError.IsPresent -eq $False)
                                                            {
                                                                Throw ($ErrorMessage)
                                                            }
                                                      }
              
              Try
                {
                    $DateTimeLogFormat = 'dddd, MMMM dd, yyyy @ hh:mm:ss.FFF tt'  ###Monday, January 01, 2019 @ 10:15:34.000 AM###
                    [ScriptBlock]$GetCurrentDateTimeLogFormat = {(Get-Date).ToString($DateTimeLogFormat)}
                    $DateTimeFileFormat = 'yyyyMMdd'  ###20190403###
                    [ScriptBlock]$GetDateTimeFileFormat = {(Get-Date).ToString($DateTimeFileFormat)}
                    [ScriptBlock]$GetCurrentDateTimeFileFormat = {(Get-Date).ToString($DateTimeFileFormat)}
                    $TextInfo = (Get-Culture).TextInfo
                    
                    #Determine the date and time we executed the function
                      $FunctionStartTime = (Get-Date)
                    
                    [String]$CmdletName = $MyInvocation.MyCommand.Name 
                    
                    $LogMessage = "Function `'$($CmdletName)`' is beginning. Please Wait..."
                    Write-Verbose -Message $LogMessage
              
                    #Define Default Action Preferences
                      $ErrorActionPreference = 'Stop'
                      
                    $LogMessage = "The following parameters and values were provided to the `'$($CmdletName)`' function." 
                    Write-Verbose -Message $LogMessage

                    $FunctionProperties = Get-Command -Name $CmdletName
                    
                    $FunctionParameters = $FunctionProperties.Parameters.Keys
              
                    ForEach ($Parameter In $FunctionParameters)
                      {
                          If (!([String]::IsNullOrEmpty($Parameter)))
                            {
                                $ParameterProperties = Get-Variable -Name $Parameter -ErrorAction SilentlyContinue
                                $ParameterValueCount = $ParameterProperties.Value | Measure-Object | Select-Object -ExpandProperty Count
                          
                                If ($ParameterValueCount -gt 1)
                                  {
                                      $ParameterValueStringFormat = ($ParameterProperties.Value | ForEach-Object {"`"$($_)`""}) -Join "`r`n"
                                      $LogMessage = "$($ParameterProperties.Name):`r`n`r`n$($ParameterValueStringFormat)"
                                  }
                                Else
                                  {
                                      $ParameterValueStringFormat = ($ParameterProperties.Value | ForEach-Object {"`"$($_)`""}) -Join ', '
                                      $LogMessage = "$($ParameterProperties.Name): $($ParameterValueStringFormat)"
                                  }
                           
                                Switch ($ParameterProperties.Name)
                                  {
                                      {([String]::IsNullOrEmpty($_) -eq $False) -and ($_ -inotmatch "^Password$|^PW$|^Passphrase$|^.*Encryption.*$|^.*Key.*$")}
                                        {
                                            Write-Verbose -Message $LogMessage
                                        }     
                                  }
                            }
                      }

                    $LogMessage = "Execution of `'$($CmdletName)`' began on $($FunctionStartTime.ToString($DateTimeLogFormat))"
                    Write-Verbose -Message $LogMessage
                }
              Catch
                {
                    $ErrorHandlingDefinition.Invoke()
                }
          }

        Process
          {           
              Try
                {                                      
                    Switch ($True)
                      {
                          {($PSBoundParameters.ContainsKey('EncryptionKey') -eq $False) -and ([String]::IsNullOrEmpty($EncryptionKey) -eq $True)}
                            {
                                $Null = Add-Type -AssemblyName 'System.Web'

                                [String]$EncryptionKey = [System.Web.Security.Membership]::GeneratePassword(32, 16)

                                [Byte[]]$Key = [System.Text.Encoding]::UTF8.GetBytes($EncryptionKey)
 
                                $Null = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)

                                $KeyEncoded = [System.Convert]::ToBase64String($Key)
                            }
                            
                          {($PSBoundParameters.ContainsKey('EncryptionKey') -eq $True) -and ([String]::IsNullOrEmpty($EncryptionKey) -eq $False)}
                            {                                
                                [Byte[]]$Key = [System.Text.Encoding]::UTF8.GetBytes($EncryptionKey)
                                
                                $Null = [System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($Key)
                                                            
                                $KeyEncoded = [System.Convert]::ToBase64String($Key)
                            }
                      }
                  
                    [String]$ConvertToSecureString_String = ($Value)
                    [Switch]$ConvertToSecureString_AsPlainText = $True
                    [Switch]$ConvertToSecureString_Force = $True

                    [Hashtable]$ConvertToSecureStringParameters = @{}
                      $ConvertToSecureStringParameters.Add('String', ($ConvertToSecureString_String))
                      $ConvertToSecureStringParameters.Add('AsPlainText', ($ConvertToSecureString_AsPlainText))
                      $ConvertToSecureStringParameters.Add('Force', ($ConvertToSecureString_Force))

                    [System.Security.SecureString]$ValueAsSecureString = ConvertTo-SecureString @ConvertToSecureStringParameters

                    [System.Security.SecureString]$ConvertFromSecureString_SecureString = ($ValueAsSecureString)

                    [Byte[]]$ConvertFromSecureString_Key = ($Key)

                    [Hashtable]$ConvertFromSecureStringParameters = @{}
                      $ConvertFromSecureStringParameters.Add('SecureString', ($ConvertFromSecureString_SecureString))
                      $ConvertFromSecureStringParameters.Add('Key', ($ConvertFromSecureString_Key))

                    [String]$EncryptedData = ConvertFrom-SecureString @ConvertFromSecureStringParameters
                                                                                                                 
                    [Hashtable]$OutputProperties = [Ordered]@{}
                     
                    $OutputProperties.Add('DecryptionKey', ($KeyEncoded))
                    $OutputProperties.Add('EncryptedData', ($EncryptedData))                           
                                
                    [PSObject]$OutputObject = New-Object -TypeName 'PSObject' -Property ($OutputProperties)
                    
                    Switch ($True)
                      {
                          {($Export.IsPresent)}
                            {                                 
                                Switch ($True)
                                  {
                                      {($PSBoundParameters.ContainsKey('ExportPath') -eq $False) -and ($Null -ieq $ExportPath)}
                                        {                                    
                                            [System.IO.FileInfo]$ExportPath = "$($Env:Temp.TrimEnd('\'))\$($CmdletName)\EncryptionInfo_$($GetDateTimeFileFormat.Invoke()).xml"
                                        }
                                        
                                      {$ExportPath.Directory.Exists -eq $False}
                                        {                                    
                                            $Null = [System.IO.Directory]::CreateDirectory($ExportPath.Directory.FullName)
                                        }  
                          
                                      {($Unsecure.IsPresent -eq $True)}
                                        {                                    
                                            $ExportableObject = $OutputObject
                                        }
                                        
                                      {($Unsecure.IsPresent -eq $False)}
                                        {
                                            $ExportableObject = $OutputObject | Select-Object -Property * -ExcludeProperty @('DecryptionKey') 
                                        }
                                  }
                                                                                                                                      
                                Switch ($True)
                                  {                                                                         
                                      {((Test-Path -Path $ExportPath.FullName -ErrorAction SilentlyContinue) -eq $True) -and ($Force.IsPresent -eq $False)}
                                        {
                                            [String]$WarningMessage = "File `"$($ExportPath.FullName)`" already exists and will not be overwritten!"
                                            Write-Warning -Message ($WarningMessage)
                                        }
                                        
                                      {((Test-Path -Path $ExportPath.FullName -ErrorAction SilentlyContinue) -eq $True) -and ($Force.IsPresent -eq $True)}
                                        {
                                            [String]$WarningMessage = "File `"$($ExportPath.FullName)`" already exists and will be overwritten!"
                                            Write-Warning -Message ($WarningMessage)
                                                                                               
                                            $Null = Export-CLIXML -InputObject ($ExportableObject) -Path ($ExportPath.FullName) -Depth 5 -Encoding Default -Force:($Force.IsPresent) 
                                        }
                            
                                      {((Test-Path -Path $ExportPath.FullName -ErrorAction SilentlyContinue) -eq $False)}
                                        {
                                            [String]$LogMessage = "File `"$($ExportPath.FullName)`" does not exist and will be created. Please Wait..."
                                            Write-Verbose -Message ($LogMessage)
                                                                                                     
                                            $Null = Export-CLIXML -InputObject ($ExportableObject) -Path ($ExportPath.FullName) -Depth 5 -Encoding Default -Force:($Force.IsPresent)
                                        }
                                  }      
                            }                       
                      }
                }
              Catch
                {
                    $ErrorHandlingDefinition.Invoke()
                }
          }
        
        End
          {                                        
              Try
                {
                    #Write .NET object returned by this function to the powershell pipeline
                      Write-Output -InputObject ($OutputObject)
                
                    #Determine the date and time the function completed execution
                      $FunctionEndTime = (Get-Date)

                      $LogMessage = "Execution of `'$($CmdletName)`' ended on $($FunctionEndTime.ToString($DateTimeLogFormat))"
                      Write-Verbose -Message $LogMessage

                    #Log the total script execution time
                      $FunctionExecutionTimespan = New-TimeSpan -Start ($FunctionStartTime) -End ($FunctionEndTime)

                      $LogMessage = "Function execution took $($FunctionExecutionTimespan.Hours.ToString()) hour(s), $($FunctionExecutionTimespan.Minutes.ToString()) minute(s), $($FunctionExecutionTimespan.Seconds.ToString()) second(s), and $($FunctionExecutionTimespan.Milliseconds.ToString()) millisecond(s)"
                      Write-Verbose -Message $LogMessage
                    
                    $LogMessage = "Function `'$($CmdletName)`' is completed."
                    Write-Verbose -Message $LogMessage
                }
              Catch
                {
                    $ErrorHandlingDefinition.Invoke()
                }
          }
    }
#endregion