Public/Protect-FileWithEncryption.ps1

using namespace System
using namespace System.IO
using namespace System.Security.Cryptography
using namespace System.Collections.Immutable

Set-StrictMode -Version Latest

#### <h2 style="color: #DCA657;">Protect-FileWithEncryption</h2>
####
function Protect-FileWithEncryption {
    #### Encrypt a file with AES-256-CBC.
    ####
    #### Output is the source path with `.enc` appended. The source is left in place.
    ####
    #### <b style="color: #D2A8FF;">Parameters</b>
    ####
    [CmdletBinding()]
    param(
        #### - `[string]`: __Path__
        #### - *File to encrypt. Accepts pipeline input.*
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('FilePath')]
        [string]$Path,

        #### - `[securestring]`: __SecureKey__
        #### - *Encryption passphrase.*
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [securestring]$SecureKey
    )

    process {
        ####
        #### <b style="color: #C22514;">Throws</b>
        ####
        #### - When `Path` does not resolve.
        $FilePath = Resolve-Path -Path $Path -ErrorAction Stop

        #### A fresh salt and IV per file. Two encryptions of the same bytes with the
        #### same passphrase produce different ciphertext, which is the point.
        $salt = [byte[]]::new(16)
        [RandomNumberGenerator]::Fill($salt)

        $aes = [Aes]::Create()
        $aes.Key = ([Rfc2898DeriveBytes]::new($SecureKey, $salt, 100000)).GetBytes(32)
        $aes.GenerateIV()

        $encryptedFilePath = [string]$FilePath + '.enc'
        $fileStream = [File]::Open($FilePath, 'Open', 'Read')
        $encryptedStream = [File]::Open($encryptedFilePath, 'Create', 'Write')
        $cryptoStream = $null

        try {
            #### Header first, then ciphertext. The reader depends on this order.
            $encryptedStream.Write($salt, 0, $salt.Length)
            $encryptedStream.Write($aes.IV, 0, $aes.IV.Length)
            $cryptoStream = [CryptoStream]::new($encryptedStream, $aes.CreateEncryptor(), [CryptoStreamMode]::Write)

            $buffer = [byte[]]::new(4096)
            while (($bytesRead = $fileStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
                $cryptoStream.Write($buffer, 0, $bytesRead)
            }
        }
        finally {
            #### The crypto stream closes first. Closing it flushes the final padded
            #### block into the file stream underneath it.
            if ($cryptoStream) { $cryptoStream.Close() }
            $fileStream.Close()
            $encryptedStream.Close()
        }

        ####
        #### <b style="color: #369FFF;">Returns</b>
        ####
        #### - `[ImmutableDictionary[string, object]]`
        #### - `[string]`: __Path__
        #### - *Path of the encrypted file.*
        #### - `[long]`: __SizeBytes__
        #### - *Size of the encrypted file, header included.*
        #### Each Add returns a new dictionary rather than mutating the old one, so
        #### the second call is what produces the value this function emits.
        $encryptedSize = [FileInfo]::new($encryptedFilePath).Length
        $manifest = [ImmutableDictionary[string, object]]::Empty.Add('Path', $encryptedFilePath)
        $manifest.Add('SizeBytes', $encryptedSize)
    }
}
####
#### ---
####