Public/cloud-sshkey.ps1

function Get-CloudSSHKey {
    <#
    .SYNOPSIS
        Gets management SSH keys from the Cloud Server.
 
    .DESCRIPTION
        Retrieves a list of SSH keys authorized to log in to the dashboard.
 
        The API response is normalized into individual PowerShell objects with:
        - Index
        - Type
        - Fingerprint
        - Comment
        - RawKey
 
        The RawKey property contains the complete SSH public key string as returned
        by the API.
 
    .EXAMPLE
        # Get all SSH keys
        Get-CloudSSHKey
 
    .EXAMPLE
        # Get the raw key for the first SSH key
        (Get-CloudSSHKey)[0].RawKey
 
    .EXAMPLE
        # Get all raw SSH keys
        Get-CloudSSHKey | Select-Object -ExpandProperty RawKey
 
    .EXAMPLE
        # Find SSH keys matching a comment
        Get-CloudSSHKey | Where-Object Comment -Like "*tverkade*"
 
    .EXAMPLE
        # Get the raw key for a specific comment
        Get-CloudSSHKey |
            Where-Object Comment -Like "*tverkade*" |
            Select-Object -ExpandProperty RawKey
 
    .EXAMPLE
        # Display only the key type and comment
        Get-CloudSSHKey | Format-Table Type, Comment
 
    .EXAMPLE
        # Display the full object including the raw key
        Get-CloudSSHKey | Format-List *
    #>


    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key"

    $response = Invoke-CloudApiRequest `
        -Uri $uri `
        -Method Get

    $index = 0

    foreach ($entry in $response.keys) {

        #
        # Some API responses may contain multiple SSH public keys concatenated
        # together. Split whenever another SSH key type begins.
        #
        $individualKeys = $entry -split `
            '(?=(?:ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-[^\s]+)\s)'

        foreach ($key in $individualKeys) {

            $key = $key.Trim()

            if ([string]::IsNullOrWhiteSpace($key)) {
                continue
            }

            #
            # Standard OpenSSH public key format:
            #
            # <type> <base64-key> [comment]
            #
            if ($key -match '^(?<Type>\S+)\s+(?<KeyData>\S+)(?:\s+(?<Comment>.*))?$') {

                #
                # Save the values immediately because PowerShell's automatic
                # $Matches variable can be overwritten by later regex operations.
                #
                $rawType = $Matches.Type
                $keyData = $Matches.KeyData
                $comment = $Matches.Comment

                #
                # Convert the raw SSH key type into a cleaner display value.
                #
                $displayType = switch ($rawType) {
                    'ssh-rsa' {
                        'RSA'
                    }

                    'ssh-ed25519' {
                        'ED25519'
                    }

                    'ssh-dss' {
                        'DSA'
                    }

                    default {
                        if ($rawType -like 'ecdsa-*') {
                            'ECDSA'
                        }
                        else {
                            $rawType
                        }
                    }
                }

                #
                # Generate an OpenSSH-style SHA256 fingerprint.
                #
                try {
                    $keyBytes = [Convert]::FromBase64String($keyData)

                    $sha256 = [System.Security.Cryptography.SHA256]::Create()

                    try {
                        $hash = $sha256.ComputeHash($keyBytes)

                        $fingerprint = "SHA256:" +
                            [Convert]::ToBase64String($hash).TrimEnd('=')
                    }
                    finally {
                        $sha256.Dispose()
                    }
                }
                catch {
                    $fingerprint = "Invalid key"
                }

                #
                # Return one PowerShell object per SSH key.
                #
                [PSCustomObject]@{
                    Index       = $index
                    Type        = $displayType
                    Fingerprint = $fingerprint
                    Comment     = $comment
                    RawKey      = $key
                }

                $index++
            }
        }
    }
}

function Add-CloudSSHKey {
    <#
    .SYNOPSIS
        Adds an SSH public key to the Cloud Server.
 
    .DESCRIPTION
        Adds an SSH public key to the Cloud Server management authorized key list.
 
        The function supports three methods:
 
        1. Generate a new RSA 4096-bit SSH keypair.
           If no public key is supplied, a new keypair is created in the
           current user's .ssh directory and the public key is uploaded.
 
        2. Supply a public key directly using -PublicKey.
 
        3. Supply the path to an existing public key using -PublicKeyPath.
 
        Generated keys default to RSA 4096 for broad compatibility with
        environments requiring FIPS-approved cryptography.
 
        The private key is never uploaded. Only the public key is sent
        to the Cloud Server.
 
    .PARAMETER PublicKey
        An existing OpenSSH-format public key supplied as a string.
 
    .PARAMETER PublicKeyPath
        Path to an existing SSH public key file.
 
    .PARAMETER KeyName
        File name to use when generating a new SSH keypair.
 
        The key is created in:
 
        $env:USERPROFILE\.ssh\<KeyName>
 
        Default:
        id_cloud_rsa
 
    .PARAMETER Comment
        Comment added to a newly generated SSH public key.
 
        Default:
        <username>@<computername>
 
    .PARAMETER Force
        Allows an existing generated key file with the same KeyName to
        be replaced.
 
        This parameter only applies when generating a new key.
 
    .EXAMPLE
        # Generate a new RSA 4096-bit keypair and upload the public key
        Add-CloudSSHKey
 
    .EXAMPLE
        # Generate a new key with a custom file name
        Add-CloudSSHKey -KeyName "hypercloud"
 
    .EXAMPLE
        # Generate a new key with a custom comment
        Add-CloudSSHKey `
            -KeyName "hypercloud-admin" `
            -Comment "tverkade@admin-workstation"
 
    .EXAMPLE
        # Upload an existing public key from a file
        Add-CloudSSHKey `
            -PublicKeyPath "$env:USERPROFILE\.ssh\id_rsa.pub"
 
    .EXAMPLE
        # Upload a public key supplied directly as a string
        Add-CloudSSHKey `
            -PublicKey "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... user@host"
 
    .EXAMPLE
        # Generate a replacement key if the specified key already exists
        Add-CloudSSHKey `
            -KeyName "hypercloud" `
            -Force
    #>


    [CmdletBinding(DefaultParameterSetName = 'Generate')]
    param (
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'String'
        )]
        [string]$PublicKey,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'File'
        )]
        [string]$PublicKeyPath,

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Generate'
        )]
        [string]$KeyName = 'id_cloud_rsa',

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Generate'
        )]
        [string]$Comment = "$env:USERNAME@$env:COMPUTERNAME",

        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Generate'
        )]
        [switch]$Force
    )

    #
    # Verify there is an active Cloud connection.
    #
    if (-not $script:CloudConnection -or -not $script:CloudConnection.Connected) {
        throw "No active Cloud Server connection. Run Connect-CloudServer first."
    }

    $generatedKey      = $false
    $privateKeyPath    = $null
    $publicKeyFilePath = $null
    $keyToUpload       = $null

    switch ($PSCmdlet.ParameterSetName) {

        #
        # Public key supplied directly as a string.
        #
        'String' {
            $keyToUpload = $PublicKey.Trim()
        }

        #
        # Public key supplied as a file.
        #
        'File' {
            if (-not (Test-Path -LiteralPath $PublicKeyPath -PathType Leaf)) {
                throw "SSH public key file does not exist: $PublicKeyPath"
            }

            $publicKeyFilePath = (Resolve-Path -LiteralPath $PublicKeyPath).Path

            $keyToUpload = (
                Get-Content `
                    -LiteralPath $publicKeyFilePath `
                    -Raw
            ).Trim()
        }

        #
        # No public key supplied, so generate one.
        #
        'Generate' {
            $sshDirectory = Join-Path `
                -Path $env:USERPROFILE `
                -ChildPath '.ssh'

            if (-not (Test-Path -LiteralPath $sshDirectory)) {
                New-Item `
                    -Path $sshDirectory `
                    -ItemType Directory `
                    -Force |
                    Out-Null
            }

            $privateKeyPath = Join-Path `
                -Path $sshDirectory `
                -ChildPath $KeyName

            $publicKeyFilePath = "${privateKeyPath}.pub"

            #
            # Protect against accidentally replacing an existing key.
            #
            if (
                (Test-Path -LiteralPath $privateKeyPath) -or
                (Test-Path -LiteralPath $publicKeyFilePath)
            ) {
                if (-not $Force) {
                    throw @"
An SSH key already exists:
 
$privateKeyPath
 
Specify a different -KeyName or use -Force to replace it.
"@

                }

                Remove-Item `
                    -LiteralPath $privateKeyPath `
                    -Force `
                    -ErrorAction SilentlyContinue

                Remove-Item `
                    -LiteralPath $publicKeyFilePath `
                    -Force `
                    -ErrorAction SilentlyContinue
            }

            #
            # Find ssh-keygen.
            #
            $sshKeygen = Get-Command `
                ssh-keygen.exe `
                -ErrorAction SilentlyContinue

            if (-not $sshKeygen) {
                $sshKeygen = Get-Command `
                    ssh-keygen `
                    -ErrorAction SilentlyContinue
            }

            if (-not $sshKeygen) {
                throw @"
ssh-keygen could not be found.
 
Install the Windows OpenSSH Client feature before generating SSH keys.
"@

            }

            Write-Verbose "Generating RSA 4096-bit SSH key: $privateKeyPath"

            #
            # Generate an RSA 4096 key without a passphrase.
            #
            & $sshKeygen.Source `
                -t rsa `
                -b 4096 `
                -f $privateKeyPath `
                -N '""' `
                -C $Comment

            if ($LASTEXITCODE -ne 0) {
                throw "ssh-keygen failed with exit code $LASTEXITCODE."
            }

            if (-not (Test-Path -LiteralPath $privateKeyPath)) {
                throw "ssh-keygen did not create the expected private key: $privateKeyPath"
            }

            if (-not (Test-Path -LiteralPath $publicKeyFilePath)) {
                throw "ssh-keygen did not create the expected public key: $publicKeyFilePath"
            }

            $keyToUpload = (
                Get-Content `
                    -LiteralPath $publicKeyFilePath `
                    -Raw
            ).Trim()

            $generatedKey = $true
        }
    }

    #
    # Validate that the supplied/generated value looks like an
    # OpenSSH-format public key.
    #
    if (
        $keyToUpload -notmatch
        '^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-\S+)\s+\S+'
    ) {
        throw "The supplied value does not appear to be a valid OpenSSH public key."
    }

    #
    # Parse the SSH key before upload so we can calculate useful metadata.
    #
    $parts = $keyToUpload -split '\s+', 3

    $rawType = $parts[0]
    $keyData = $parts[1]

    if ($parts.Count -ge 3) {
        $keyComment = $parts[2]
    }
    else {
        $keyComment = ''
    }

    $displayType = switch ($rawType) {
        'ssh-rsa' {
            'RSA'
        }

        'ssh-ed25519' {
            'ED25519'
        }

        'ssh-dss' {
            'DSA'
        }

        default {
            if ($rawType -like 'ecdsa-*') {
                'ECDSA'
            }
            else {
                $rawType
            }
        }
    }

    #
    # Calculate an OpenSSH-style SHA256 fingerprint.
    #
    try {
        $keyBytes = [Convert]::FromBase64String($keyData)

        $sha256 = [System.Security.Cryptography.SHA256]::Create()

        try {
            $hash = $sha256.ComputeHash($keyBytes)

            $fingerprint = "SHA256:" +
                [Convert]::ToBase64String($hash).TrimEnd('=')
        }
        finally {
            $sha256.Dispose()
        }
    }
    catch {
        throw "Unable to calculate SSH key fingerprint. The key data does not appear to be valid Base64."
    }

    #
    # Build the API request.
    #
    $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key"

    #
    # Keep the body as an object.
    #
    # Invoke-CloudApiRequest is responsible for serializing the request
    # appropriately.
    #
    $body = @{
        key = $keyToUpload
    }

    Write-Verbose "Uploading SSH public key to $uri"

    #
    # Upload the public key.
    #
    # Do not return a success object if the API request fails.
    #
    try {
        Invoke-CloudApiRequest `
            -Uri $uri `
            -Method Put `
            -Body $body `
            -ErrorAction Stop |
            Out-Null
    }
    catch {
        throw "Failed to upload SSH key: $($_.Exception.Message)"
    }

    #
    # Return information about the successfully uploaded key.
    #
    [PSCustomObject]@{
        Type          = $displayType
        Fingerprint   = $fingerprint
        Comment       = $keyComment
        Generated     = $generatedKey
        PrivateKey    = $privateKeyPath
        PublicKeyFile = $publicKeyFilePath
        RawKey        = $keyToUpload
    }
}

function Remove-CloudSSHKey {
    <#
    .SYNOPSIS
        Removes an SSH public key from the Cloud Server.
 
    .DESCRIPTION
        Removes an SSH public key from the Cloud Server management
        authorized key list.
 
        The SSH key can be supplied in one of three ways:
 
        1. As a raw OpenSSH public key string using -PublicKey.
 
        2. From an existing public key file using -PublicKeyPath.
 
        3. By piping an object returned by Get-CloudSSHKey into
           Remove-CloudSSHKey.
 
        The complete public key is sent to the Cloud Server in the
        DELETE request.
 
    .PARAMETER PublicKey
        An OpenSSH-format public key supplied as a string.
 
        Example:
        ssh-rsa AAAAB3NzaC1yc2E... user@host
 
    .PARAMETER PublicKeyPath
        Path to an existing SSH public key file.
 
        Example:
        C:\Users\user\.ssh\id_rsa.pub
 
    .PARAMETER InputObject
        An SSH key object returned by Get-CloudSSHKey.
 
        The object must contain a RawKey property.
 
    .PARAMETER Force
        Removes the key without prompting for confirmation.
 
    .EXAMPLE
        # Remove a public key supplied directly as a string
        Remove-CloudSSHKey `
            -PublicKey "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQ... user@host"
 
    .EXAMPLE
        # Remove a public key using a .pub file
        Remove-CloudSSHKey `
            -PublicKeyPath "$env:USERPROFILE\.ssh\id_cloud_rsa.pub"
 
    .EXAMPLE
        # Remove the first key returned by Get-CloudSSHKey
        Get-CloudSSHKey | Select-Object -First 1 | Remove-CloudSSHKey
 
    .EXAMPLE
        # Remove a key based on its comment
        Get-CloudSSHKey |
            Where-Object Comment -Like "*LAVASTONE*" |
            Remove-CloudSSHKey
 
    .EXAMPLE
        # Remove a specific key without confirmation
        Get-CloudSSHKey |
            Where-Object Fingerprint -eq "SHA256:abc123..." |
            Remove-CloudSSHKey -Force
 
    .EXAMPLE
        # Remove a key from a public key file without confirmation
        Remove-CloudSSHKey `
            -PublicKeyPath "$env:USERPROFILE\.ssh\tv-hypershell-test.pub" `
            -Force
    #>


    [CmdletBinding(
        SupportsShouldProcess = $true,
        ConfirmImpact = 'High',
        DefaultParameterSetName = 'String'
    )]
    param (
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'String',
            Position = 0
        )]
        [string]$PublicKey,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'File'
        )]
        [string]$PublicKeyPath,

        [Parameter(
            Mandatory = $true,
            ValueFromPipeline = $true,
            ParameterSetName = 'Object'
        )]
        [PSObject]$InputObject,

        [Parameter()]
        [switch]$Force
    )

    process {

        #
        # Verify there is an active Cloud connection.
        #
        if (-not $script:CloudConnection -or -not $script:CloudConnection.Connected) {
            throw "No active Cloud Server connection. Run Connect-CloudServer first."
        }

        $keyToRemove = $null

        switch ($PSCmdlet.ParameterSetName) {

            #
            # Public key supplied directly.
            #
            'String' {
                $keyToRemove = $PublicKey.Trim()
            }

            #
            # Read public key from file.
            #
            'File' {
                if (-not (Test-Path -LiteralPath $PublicKeyPath -PathType Leaf)) {
                    throw "SSH public key file does not exist: $PublicKeyPath"
                }

                $keyToRemove = (
                    Get-Content `
                        -LiteralPath $PublicKeyPath `
                        -Raw
                ).Trim()
            }

            #
            # Object supplied from Get-CloudSSHKey.
            #
            'Object' {
                if (-not $InputObject.PSObject.Properties['RawKey']) {
                    throw "The supplied object does not contain a RawKey property."
                }

                $keyToRemove = ([string]$InputObject.RawKey).Trim()
            }
        }

        #
        # Validate the OpenSSH public key format.
        #
        if (
            $keyToRemove -notmatch
            '^(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-\S+)\s+\S+'
        ) {
            throw "The supplied value does not appear to be a valid OpenSSH public key."
        }

        #
        # Parse the key for display purposes.
        #
        $parts = $keyToRemove -split '\s+', 3

        $rawType = $parts[0]
        $keyData = $parts[1]

        if ($parts.Count -ge 3) {
            $keyComment = $parts[2]
        }
        else {
            $keyComment = ''
        }

        $displayType = switch ($rawType) {
            'ssh-rsa' {
                'RSA'
            }

            'ssh-ed25519' {
                'ED25519'
            }

            'ssh-dss' {
                'DSA'
            }

            default {
                if ($rawType -like 'ecdsa-*') {
                    'ECDSA'
                }
                else {
                    $rawType
                }
            }
        }

        #
        # Calculate the OpenSSH-style SHA256 fingerprint.
        #
        try {
            $keyBytes = [Convert]::FromBase64String($keyData)

            $sha256 = [System.Security.Cryptography.SHA256]::Create()

            try {
                $hash = $sha256.ComputeHash($keyBytes)

                $fingerprint = "SHA256:" +
                    [Convert]::ToBase64String($hash).TrimEnd('=')
            }
            finally {
                $sha256.Dispose()
            }
        }
        catch {
            throw "Unable to calculate SSH key fingerprint. The key data does not appear to be valid Base64."
        }

        #
        # Build API request.
        #
        $uri = "$($script:CloudConnection.BaseUri)/manifold-api/v3-preview/management/ssh-key"

        $body = @{
            key = $keyToRemove
        }

        #
        # Build a useful confirmation description.
        #
        if ([string]::IsNullOrWhiteSpace($keyComment)) {
            $description = "$displayType key [$fingerprint]"
        }
        else {
            $description = "$displayType key '$keyComment' [$fingerprint]"
        }

        #
        # -Force bypasses ShouldProcess confirmation.
        #
        $shouldRemove = $Force -or $PSCmdlet.ShouldProcess(
            $description,
            "Remove SSH key"
        )

        if (-not $shouldRemove) {
            return
        }

        Write-Verbose "Removing SSH public key from $uri"

        try {
            Invoke-CloudApiRequest `
                -Uri $uri `
                -Method Delete `
                -Body $body `
                -ErrorAction Stop |
                Out-Null
        }
        catch {
            throw "Failed to remove SSH key: $($_.Exception.Message)"
        }

        #
        # Return information about the key that was successfully removed.
        #
        [PSCustomObject]@{
            Type        = $displayType
            Fingerprint = $fingerprint
            Comment     = $keyComment
            Removed     = $true
            RawKey      = $keyToRemove
        }
    }
}