PEMEncrypt.psm1

function Get-DefaultPath {
    [CmdletBinding()]
    Param()
    Process {
        $homePath = if ($HOME) {
            $HOME
        }
        elseif (Test-Path "~") {
            (Resolve-Path "~").Path
        }
        [System.IO.Path]::Combine($homePath,".ssh","id_rsa")
    }
}


function Import-Assemblies {
    Param()
    Begin {
        $dllPath = if ($PSVersionTable.PSVersion.Major -ge 6) {
            [System.IO.Path]::Combine($PSScriptRoot,'bin','netstandard')
        }
        else {
            [System.IO.Path]::Combine($PSScriptRoot,'bin','netfx')
        }
    }
    Process {
        try {
            $bouncyCastleDll = Join-Path $dllPath 'BouncyCastle.Crypto.dll'
            Add-Type -Path $bouncyCastleDll -ErrorAction SilentlyContinue | Out-Null
        }
        catch {
            $Global:Error.Remove($Global:Error[0])
        }
        try {
            $PEMEncrypt = Join-Path $dllPath 'SCRTHQ.PEMEncrypt.dll'
            Add-Type -Path $PEMEncrypt -ReferencedAssemblies $bouncyCastleDll -ErrorAction SilentlyContinue | Out-Null
        }
        catch {
            $Global:Error.Remove($Global:Error[0])
        }
    }
}


function Unprotect-SecureString {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory,Position = 0)]
        [SecureString]
        $SecureString
    )
    Process {
        [Runtime.InteropServices.Marshal]::PtrToStringAuto(
            [Runtime.InteropServices.Marshal]::SecureStringToBSTR(
                $SecureString
            )
        )
    }
}


function New-RSAKeyPair {
    <#
    .SYNOPSIS
    Generates an RSA PEM key pair and corresponding public SSH key.
 
    .DESCRIPTION
    Generates an RSA PEM key pair and corresponding public SSH key.
 
    .PARAMETER Length
    Alias: [-l,-b]
    The bit-length of the key to generate. Defaults to 4096.
 
    .PARAMETER Password
    Alias: -p
    A SecureString or plain-text String containing the password to encrypt the private key with. Exclude to create the private key without a password. For security, SecureString is recommended, although plain-text strings are allowed for broader compatibility.
 
    .PARAMETER Path
    Alias: -out
    The path to save the private key to. Defaults to ~/.ssh/id_rsa
 
    .PARAMETER Interactive
    Alias: -i
    If $true, prompt the user for the options to create the key with. Similar to creating a key with ssh-keygen.
 
    .PARAMETER NoFile
    Alias: -nof
    If $true, do not save any keys to file. Sets PassThru to $true and returns the generated RSAKey object containing the PublicPEM, PublicSSH, and PrivatePEM as string properties.
 
    .PARAMETER NoSSH
    Alias: -nos
    If $true, do not save the SSH key to file. Use when only the RSA PEM key pair is needed.
 
    .PARAMETER NoPEM
    Alias: -nop
    If $true, do not save the Public PEM key to file. Use when only the SSH key pair is needed.
 
    .PARAMETER PassThru
    Alias: -pt
    Returns the generated RSAKey object containing the PublicPEM, PublicSSH, and PrivatePEM as string properties.
 
    .PARAMETER Force
    Alias: -f
    If the keys at the file path already exist, overwrite them.
 
    .EXAMPLE
    New-RSAKeyPair -Interactive
 
    Generating public/private RSA key pair...
    Enter the path to save the key to (Default: C:\Users\nate\.ssh\id_rsa): .\Testing\id_pemencrypt
    Enter desired key length (Default: 4096):
    Enter passphrase (Default: No passphrase):
    Saving private key to path : .\Testing\id_pemencrypt
    Saving public SSH key to path : .\Testing\id_pemencrypt.pub
    Saving public PEM key to path : .\Testing\id_pemencrypt.pem
 
    .EXAMPLE
    New-RSAKeyPair -NoFile
 
    PublicPEM
    ---------
    -----BEGIN PUBLIC KEY-----...
 
    .EXAMPLE
    New-RSAKeyPair -Length 1024 -NoFile | Select-Object -ExpandProperty PublicSSH
    ssh-rsa AAAAB3NzaC1yc2EAAAABAwAAAIEAo2CDoZRSy7JDJbX3ygsj3L09rMxq+46lMkWv6K33Cng3y4DokqqyUc2KCzhspBViGzVl3mJ+Y4S9O+D4bktcSDRZbEmZ0cVsFZFEAI17iEKnZHZnaqMIoIzaK2TS0rnQbkYpSDfKUAZtwSNiWB0TfMFdnOY6UJdlfLGzPeFJWTU= PEMEncrypt@User@Computer
 
    .EXAMPLE
    New-RSAKeyPair
    Key already exists at desired path: C:\Users\nate\.ssh\id_rsa. Use -Force to overwrite the existing key or choose a different path.
    At E:\Git\PEMEncrypt\BuildOutput\PEMEncrypt\0.2.0\PEMEncrypt.psm1:177 char:21
    + ... throw "Key already exists at desired path: $Path. Use -Fo ...
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : OperationStopped: (Key already exists \u2026e a different path.:String) [], RuntimeException
    + FullyQualifiedErrorId : Key already exists at desired path: C:\Users\nate\.ssh\id_rsa. Use -Force to overwrite the existing key or choose a different path.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "")]
    [OutputType('SCRTHQ.PEMEncrypt.RSAKey')]
    [CmdletBinding()]
    Param(
        [Parameter(Position = 0)]
        [Alias('l','b')]
        [Int]
        $Length = 4096,
        [parameter()]
        [Alias('p')]
        [Object]
        $Password,
        [parameter()]
        [Alias('out')]
        [String]
        $Path = (Get-DefaultPath),
        [Parameter()]
        [Alias('i')]
        [Switch]
        $Interactive,
        [Parameter()]
        [Alias('nof')]
        [Switch]
        $NoFile,
        [Parameter()]
        [Alias('nos')]
        [Switch]
        $NoSSH,
        [Parameter()]
        [Alias('nop')]
        [Switch]
        $NoPEM,
        [Parameter()]
        [Alias('pt')]
        [Switch]
        $PassThru,
        [Parameter()]
        [Alias('f')]
        [Switch]
        $Force
    )
    Begin {
        Import-Assemblies
        if ($MyInvocation.InvocationName -eq 'genrsa') {
            $NoPEM = $false
            $NoSSH = $true
        }
        if ($MyInvocation.InvocationName -eq 'genssh') {
            $NoSSH = $false
            $NoPEM = $true
        }
    }
    Process {
        if ($Interactive) {
            Write-Host "Generating public/private RSA key pair..."
            if (-not $NoFile) {
                $newPath = if ($choice = Read-Host -Prompt "Enter the path to save the key to (Default: $Path)") {
                    $choice
                }
                else {
                    $Path
                }
                if (-not $Force -and (Test-Path $newPath)) {
                    Write-Error "Key already exists at desired path: $newPath. Use -Force to overwrite the existing key or choose a different path"
                    return
                }
                $parent = Split-Path $newPath -Parent
                if (-not (Test-Path $parent)) {
                    Write-Host "Creating missing parent folder: $parent"
                    New-Item -ItemType Directory $parent -Force | Out-Null
                }
            }
            $Length = if ($choice = Read-Host -Prompt "Enter desired key bit length (Default: 4096)") {
                $choice
            }
            else {
                4096
            }
            $Password = Read-Host -AsSecureString -Prompt "Enter passphrase (Default: No passphrase)"
            if (-not ([System.String]::IsNullOrEmpty((Unprotect-SecureString -SecureString $Password)))) {
                $confirmed = Read-Host -AsSecureString -Prompt "Enter the same passphrase to confirm"
                if ((Unprotect-SecureString -SecureString $confirmed) -ne (Unprotect-SecureString -SecureString $Password)) {
                    Write-Error "Passphrases provided do not match! Exiting"
                    return
                }
                Write-Host "Generating passphrase protected key pair"
                $keys = [SCRTHQ.PEMEncrypt.RSA]::Generate(
                    $Length,
                    (Unprotect-SecureString -SecureString $Password)
                )
            }
            else {
                Write-Host "Generating key pair"
                $keys = [SCRTHQ.PEMEncrypt.RSA]::Generate(
                    $Length
                )
            }
            if (-not $NoFile) {
                Write-Host "Saving private key to path : $newPath"
                $keys.PrivatePEM | Set-Content -Path $newPath -Force
                if (-not $NoSSH) {
                    $sshPath = "{0}.pub" -f $newPath
                    Write-Host "Saving public SSH key to path : $sshPath"
                    $keys.PublicSSH | Set-Content -Path $sshPath -Force
                }
                if (-not $NoPEM) {
                    $pemPath = "{0}.pem" -f $newPath
                    Write-Host "Saving public PEM key to path : $pemPath"
                    $keys.PublicPEM | Set-Content -Path $pemPath -Force
                }
            }
            if ($PassThru -or $NoFile) {
                $keys
            }
        }
        else {
            if (-not $NoFile -and -not $Force -and (Test-Path $Path)) {
                Write-Error "Key already exists at desired path: $Path. Use -Force to overwrite the existing key or choose a different path."
                return
            }
            else {
                $parent = Split-Path $Path -Parent
                if (-not (Test-Path $parent)) {
                    Write-Host "Creating missing parent folder: $parent"
                    New-Item -ItemType Directory $parent -Force | Out-Null
                }
                $keys = if ($PSBoundParameters.ContainsKey('Password')) {
                    Write-Host "Generating passphrase protected key pair"
                    [SCRTHQ.PEMEncrypt.RSA]::Generate(
                        $Length,
                        $(
                            if ($Password -is [SecureString]) {
                                (Unprotect-SecureString -SecureString $Password)
                            }
                            else {
                                "$Password"
                            }
                        )
                    )
                }
                else {
                    Write-Host "Generating key pair"
                    [SCRTHQ.PEMEncrypt.RSA]::Generate(
                        $Length
                    )
                }
                if (-not $NoFile) {
                    Write-Host "Saving private key to path : $Path"
                    $keys.PrivatePEM | Set-Content -Path $Path -Force
                    if (-not $NoSSH) {
                        $sshPath = "{0}.pub" -f $Path
                        Write-Host "Saving public SSH key to path : $sshPath"
                        $keys.PublicSSH | Set-Content -Path $sshPath -Force
                    }
                    if (-not $NoPEM) {
                        $pemPath = "{0}.pem" -f $Path
                        Write-Host "Saving public PEM key to path : $pemPath"
                        $keys.PublicPEM | Set-Content -Path $pemPath -Force
                    }
                }
                if ($PassThru -or $NoFile) {
                    $keys
                }
            }
        }
    }
}

Export-ModuleMember -Function 'New-RSAKeyPair'

function Protect-PEMString {
    <#
    .SYNOPSIS
    Encrypts a string by using a public RSA key.
 
    .DESCRIPTION
    Encrypts a string by using a public RSA key.
 
    .PARAMETER StringToEncrypt
    The plain-text string that you would like to encrypt with the public key.
 
    .PARAMETER PublicKey
    The full or relative path to the public key OR the key itself in string format.
 
    .EXAMPLE
    # Using a password-less private key
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public.pem
    $encrypted | Unprotect-PEMString -PrivateKey .\private.pem
 
    .EXAMPLE
    # Use Get-Credential to prompt for credentials so it's not in plain text
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public_des3.pem
    $keyCreds = Get-Credential -UserName key -Message 'Please enter the password for the private key'
    $encrypted | Unprotect-PEMString -PrivateKey .\private_des3.pem -Password $keyCreds.Password
 
    .EXAMPLE
    # Build a SecureString using a plain-text password
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public_des3.pem
    $password = ConvertTo-SecureString 'P@$$w0rd' -AsPlainText -Force
    $encrypted | Unprotect-PEMString -PrivateKey .\private_des3.pem -Password $password
    #>

    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [Alias('String')]
        [String[]]
        $StringToEncrypt,
        [parameter(Mandatory,Position = 1)]
        [Alias('PublicKeyPath','Key')]
        [String]
        $PublicKey
    )
    Begin {
        Import-Assemblies
        if ([System.IO.File]::Exists($PublicKey)) {
            $PublicKey = ([System.IO.File]::ReadAllText((Resolve-Path $PublicKey).Path))
        }
    }
    Process {
        foreach ($string in $StringToEncrypt) {
            try {
                [SCRTHQ.PEMEncrypt.Crypto]::Encrypt(
                    $string,
                    $PublicKey
                )
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
    }
}

Export-ModuleMember -Function 'Protect-PEMString'

function Unprotect-PEMString {
    <#
    .SYNOPSIS
    Decrypts an encrypted string by using the private RSA key corresponding to the public key the string was encrypted with.
 
    .DESCRIPTION
    Decrypts an encrypted string by using the private RSA key corresponding to the public key the string was encrypted with.
 
    .PARAMETER StringToDecrypt
    The Base64 string that you would like to decrypt with the private key.
 
    .PARAMETER PrivateKey
    The full or relative path to the private key OR the key itself in string format.
 
    .PARAMETER Password
    A SecureString containing the password for the private key, if applicable.
 
    Exclude if the private key does not have a password.
 
    .EXAMPLE
    # Using a password-less private key
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public.pem
    $encrypted | Unprotect-PEMString -PrivateKey .\private.pem
 
    .EXAMPLE
    # Use Get-Credential to prompt for credentials so it's not in plain text
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public_des3.pem
    $keyCreds = Get-Credential -UserName key -Message 'Please enter the password for the private key'
    $encrypted | Unprotect-PEMString -PrivateKey .\private_des3.pem -Password $keyCreds.Password
 
    .EXAMPLE
    # Build a SecureString using a plain-text password
    $encrypted = 'Hello','How are you today?' | Protect-PEMString -PublicKey .\public_des3.pem
    $password = ConvertTo-SecureString 'P@$$w0rd' -AsPlainText -Force
    $encrypted | Unprotect-PEMString -PrivateKey .\private_des3.pem -Password $password
    #>

    [OutputType('System.String')]
    [CmdletBinding()]
    Param (
        [parameter(Mandatory,Position = 0,ValueFromPipeline)]
        [Alias('String')]
        [String[]]
        $StringToDecrypt,
        [parameter(Mandatory,Position = 1)]
        [Alias('PrivateKeyPath','Key')]
        [String]
        $PrivateKey,
        [parameter(Position = 2)]
        [AllowNull()]
        [SecureString]
        $Password
    )
    Begin {
        Import-Assemblies
        if ([System.IO.File]::Exists($PrivateKey)) {
            $PrivateKey = ([System.IO.File]::ReadAllText((Resolve-Path $PrivateKey).Path))
        }
    }
    Process {
        foreach ($string in $StringToDecrypt) {
            try {
                if ($PSBoundParameters.ContainsKey('Password')) {
                    [SCRTHQ.PEMEncrypt.Crypto]::Decrypt(
                        $string,
                        $PrivateKey,
                        (Unprotect-SecureString -SecureString $Password)
                    )
                }
                else {
                    [SCRTHQ.PEMEncrypt.Crypto]::Decrypt(
                        $string,
                        $PrivateKey
                    )
                }
            }
            catch {
                $PSCmdlet.ThrowTerminatingError($_)
            }
        }
    }
}

Export-ModuleMember -Function 'Unprotect-PEMString'

New-Alias -Name 'genrsa' -Value 'New-RSAKeyPair'
New-Alias -Name 'genssh' -Value 'New-RSAKeyPair'
New-Alias -Name 'genkey' -Value 'New-RSAKeyPair'
Export-ModuleMember -Alias @('genrsa','genssh','genkey')