HarinezumiSama.Utilities.Cryptography.ps1

<#PSScriptInfo
 
.VERSION 0.1.0
.GUID fdc9a67a-4e68-4893-859f-a66738e5a85f
.AUTHOR Vitalii Maklai a.k.a. HarinezumiSama
.COMPANYNAME
.COPYRIGHT Copyright (C) Vitalii Maklai
.TAGS Cryptography Encryption
.LICENSEURI
.PROJECTURI
.ICONURI
.EXTERNALMODULEDEPENDENCIES
.REQUIREDSCRIPTS
.EXTERNALSCRIPTDEPENDENCIES
.RELEASENOTES
 
.DESCRIPTION
Provides:
    - class HarinezumiSamaUtilitiesCryptographyHelper
    - cmdlet Encrypt-Stream
    - cmdlet Decrypt-Stream
    - cmdlet Encrypt-File
    - cmdlet Decrypt-File
#>


#Requires -Version 5

using namespace System
using namespace System.Collections
using namespace System.IO
using namespace System.Security.Cryptography
using namespace System.Security.Cryptography.X509Certificates
using namespace System.Text
using namespace Microsoft.Win32

$Script:ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
Microsoft.PowerShell.Core\Set-StrictMode -Version 1

class HarinezumiSamaUtilitiesCryptographyHelper
{
    hidden static [int] $_KeySize = [HarinezumiSamaUtilitiesCryptographyHelper]::_GetKeySize()
    hidden static [int] $_BlockSize = [HarinezumiSamaUtilitiesCryptographyHelper]::_GetBlockSize()
    hidden static [int] $_SaltSize = 32
    hidden static [int] $_IterationCount = 65536

    hidden HarinezumiSamaUtilitiesCryptographyHelper()
    {
        # Nothing to do
    }

    static [psobject] GetEncryptionData([string] $password, [byte[]] $salt)
    {
        [Rfc2898DeriveBytes] $deriveBytes = [Rfc2898DeriveBytes]::new(
            $password,
            [HarinezumiSamaUtilitiesCryptographyHelper]::_SaltSize,
            [HarinezumiSamaUtilitiesCryptographyHelper]::_IterationCount)
        try
        {
            if (![object]::ReferenceEquals($salt, $null))
            {
                $deriveBytes.Salt = $salt
            }

            [byte[]] $key = $deriveBytes.GetBytes([HarinezumiSamaUtilitiesCryptographyHelper]::_KeySize)
            [byte[]] $iv = $deriveBytes.GetBytes([HarinezumiSamaUtilitiesCryptographyHelper]::_BlockSize)

            [psobject] $result = [psobject]::new() `
                | Add-Member -MemberType NoteProperty -Name Key -Value $key -PassThru `
                | Add-Member -MemberType NoteProperty -Name IV -Value $iv -PassThru `
                | Add-Member -MemberType NoteProperty -Name Salt -Value $deriveBytes.Salt -PassThru

            return $result
        }
        finally
        {
            $deriveBytes.Dispose()
        }
    }

    static [psobject] GetEncryptionData([string] $password)
    {
        return [HarinezumiSamaUtilitiesCryptographyHelper]::GetEncryptionData($password, $null)
    }

    static [SymmetricAlgorithm] CreateAlgorithm()
    {
        [SymmetricAlgorithm] $algorithm = [AesManaged]::new()
        try
        {
            $algorithm.Mode = [CipherMode]::CBC
            $algorithm.Padding = [PaddingMode]::PKCS7
        }
        catch
        {
            $algorithm.Dispose()
        }

        return $algorithm
    }

    static [int] GetSaltSize()
    {
        return [HarinezumiSamaUtilitiesCryptographyHelper]::_SaltSize
    }

    hidden static [int] _GetKeySize()
    {
        [SymmetricAlgorithm] $algorithm = [HarinezumiSamaUtilitiesCryptographyHelper]::CreateAlgorithm()
        try
        {
            return $algorithm.KeySize / 8
        }
        finally
        {
            $algorithm.Dispose()
        }
    }

    hidden static [int] _GetBlockSize()
    {
        [SymmetricAlgorithm] $algorithm = [HarinezumiSamaUtilitiesCryptographyHelper]::CreateAlgorithm()
        try
        {
            return $algorithm.BlockSize / 8
        }
        finally
        {
            $algorithm.Dispose()
        }
    }
}

function Encrypt-Stream
{
    [CmdletBinding(PositionalBinding = $false)]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [Stream] $InputStream,

        [Parameter()]
        [string] $Password,

        [Parameter()]
        [Stream] $OutputStream
    )
    process
    {
        if ($InputStream -eq $null)
        {
            throw [ArgumentNullException]::new('InputStream')
        }
        if (!$InputStream.CanRead)
        {
            throw [ArgumentException]::new('The input stream (plain data) must be readable.', 'InputStream')
        }

        if ($OutputStream -eq $null)
        {
            throw [ArgumentNullException]::new('OutputStream')
        }
        if (!$OutputStream.CanWrite)
        {
            throw [ArgumentException]::new('The output stream (cipher data) must be writable.', 'OutputStream')
        }

        [psobject] $encryptionData = [HarinezumiSamaUtilitiesCryptographyHelper]::GetEncryptionData($Password)

        [SymmetricAlgorithm] $algorithm = [HarinezumiSamaUtilitiesCryptographyHelper]::CreateAlgorithm()
        try
        {
            $algorithm.IV = $encryptionData.IV
            $algorithm.Key = $encryptionData.Key

            $OutputStream.Write($encryptionData.Salt, 0, $encryptionData.Salt.Length) | Out-Null

            [ICryptoTransform] $encryptor = $algorithm.CreateEncryptor()
            try
            {
                [CryptoStream] $cryptoStream = [CryptoStream]::new($InputStream, $encryptor, [CryptoStreamMode]::Read)
                try
                {
                    $cryptoStream.CopyTo($OutputStream) | Out-Null
                }
                finally
                {
                    $cryptoStream.Dispose()
                }
            }
            finally
            {
                $encryptor.Dispose()
            }
        }
        finally
        {
            $algorithm.Dispose()
        }
    }
}

function Decrypt-Stream
{
    [CmdletBinding(PositionalBinding = $false)]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [Stream] $InputStream,

        [Parameter()]
        [string] $Password,

        [Parameter()]
        [Stream] $OutputStream
    )
    process
    {
        if ($InputStream -eq $null)
        {
            throw [ArgumentNullException]::new('InputStream')
        }
        if (!$InputStream.CanRead)
        {
            throw [ArgumentException]::new('The input stream (cipher data) must be readable.', 'InputStream')
        }

        if ($OutputStream -eq $null)
        {
            throw [ArgumentNullException]::new('OutputStream')
        }
        if (!$OutputStream.CanWrite)
        {
            throw [ArgumentException]::new('The output stream (plain data) must be writable.', 'OutputStream')
        }

        [byte[]] $salt = [byte[]]::new([HarinezumiSamaUtilitiesCryptographyHelper]::GetSaltSize())

        [int] $saltBytesRead = $InputStream.Read($salt, 0, $salt.Length)
        if ($saltBytesRead -ne $salt.Length)
        {
            throw 'The cipher data is corrupted.'
        }

        [psobject] $encryptionData = [HarinezumiSamaUtilitiesCryptographyHelper]::GetEncryptionData($Password, $salt)

        [SymmetricAlgorithm] $algorithm = [HarinezumiSamaUtilitiesCryptographyHelper]::CreateAlgorithm()
        try
        {
            $algorithm.IV = $encryptionData.IV
            $algorithm.Key = $encryptionData.Key

            [ICryptoTransform] $decryptor = $algorithm.CreateDecryptor()
            try
            {
                [CryptoStream] $cryptoStream = [CryptoStream]::new($InputStream, $decryptor, [CryptoStreamMode]::Read)
                try
                {
                    $cryptoStream.CopyTo($OutputStream) | Out-Null
                }
                finally
                {
                    $cryptoStream.Dispose()
                }
            }
            finally
            {
                $decryptor.Dispose()
            }
        }
        finally
        {
            $algorithm.Dispose()
        }
    }
}

function Encrypt-File
{
    [CmdletBinding(PositionalBinding = $false)]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [string] $Path,

        [Parameter()]
        [string] $Password,

        [Parameter()]
        [string] $DestinationPath
    )
    process
    {
        if ([string]::IsNullOrWhiteSpace($Path))
        {
            throw [ArgumentException]::new('The file path cannot be blank.', 'Path')
        }

        if ([string]::IsNullOrWhiteSpace($DestinationPath))
        {
            throw [ArgumentException]::new('The destination file path cannot be blank.', 'DestinationPath')
        }

        [FileStream] $inputStream = [File]::Open($Path, [FileMode]::Open, [FileAccess]::Read, [FileShare]::Read)
        try
        {
            [FileStream] $outputStream = [File]::Open($DestinationPath, [FileMode]::Create, [FileAccess]::Write, [FileShare]::Read)
            try
            {
                Encrypt-Stream -InputStream $inputStream -Password $Password -OutputStream $outputStream
            }
            finally
            {
                $outputStream.Dispose()
            }
        }
        finally
        {
            $inputStream.Dispose()
        }
    }
}

function Decrypt-File
{
    [CmdletBinding(PositionalBinding = $false)]
    param
    (
        [Parameter(ValueFromPipeline = $true)]
        [string] $Path,

        [Parameter()]
        [string] $Password,

        [Parameter()]
        [string] $DestinationPath
    )
    process
    {
        if ([string]::IsNullOrWhiteSpace($Path))
        {
            throw [ArgumentException]::new('The file path cannot be blank.', 'Path')
        }

        if ([string]::IsNullOrWhiteSpace($DestinationPath))
        {
            throw [ArgumentException]::new('The destination file path cannot be blank.', 'DestinationPath')
        }

        [FileStream] $inputStream = [File]::Open($Path, [FileMode]::Open, [FileAccess]::Read, [FileShare]::Read)
        try
        {
            [FileStream] $outputStream = [File]::Open($DestinationPath, [FileMode]::Create, [FileAccess]::Write, [FileShare]::Read)
            try
            {
                Decrypt-Stream -InputStream $inputStream -Password $Password -OutputStream $outputStream
            }
            finally
            {
                $outputStream.Dispose()
            }
        }
        finally
        {
            $inputStream.Dispose()
        }
    }
}