Sodium.psm1

[CmdletBinding()]
param()
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath)
$script:PSModuleInfo = Test-ModuleManifest -Path "$PSScriptRoot\$baseName.psd1"
$script:PSModuleInfo | Format-List | Out-String -Stream | ForEach-Object { Write-Debug $_ }
$scriptName = $script:PSModuleInfo.Name
Write-Debug "[$scriptName] - Importing module"
#region [functions] - [private]
Write-Debug "[$scriptName] - [functions] - [private] - Processing folder"
#region [functions] - [private] - [Assert-VisualCRedistributableInstalled]
Write-Debug "[$scriptName] - [functions] - [private] - [Assert-VisualCRedistributableInstalled] - Importing"
function Assert-VisualCRedistributableInstalled {
    <#
        .SYNOPSIS
        Determines if a version of the Visual C++ Redistributable is installed and meets the specified minimum version.

        .DESCRIPTION
        This function checks whether the Visual C++ Redistributable for Visual Studio 2015 or later is installed on
        the system and ensures that the installed version is greater than or equal to the specified minimum version.
        If the required version is not found, a warning is displayed, suggesting where to download the latest
        redistributable package.

        .EXAMPLE
        Assert-VisualCRedistributableInstalled -Version '14.29.30037'

        Output:
        ```powershell
        True
        ```

        Checks if the installed Visual C++ Redistributable version is at least 14.29.30037 and returns `$true`
        if the requirement is met.
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param (
        # The minimum required version of the Visual C++ Redistributable.
        [Parameter(Mandatory)]
        [Version] $Version
    )

    process {
        $result = $false
        if ($IsWindows) {
            $key = 'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64'
            if (Test-Path -Path $key) {
                $installedVersion = (Get-ItemProperty -Path $key).Version
                $result = [Version]($installedVersion.SubString(1, $installedVersion.Length - 1)) -ge $Version
            }
        }
        if (-not $result) {
            Write-Warning 'The Visual C++ Redistributable for Visual Studio 2015 or later is required.'
            Write-Warning 'Download and install the appropriate version from:'
            Write-Warning ' - https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads'
        }
        $result
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [Assert-VisualCRedistributableInstalled] - Done"
#endregion [functions] - [private] - [Assert-VisualCRedistributableInstalled]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [ConvertFrom-SodiumSealedBox]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertFrom-SodiumSealedBox] - Importing"
function ConvertFrom-SodiumSealedBox {
    <#
        .SYNOPSIS
        Decrypts a base64-encoded, Sodium SealedBox-encrypted string.

        .DESCRIPTION
        Converts a base64-encoded, Sodium SealedBox-encrypted string into its original plaintext form.
        Uses the provided public and private keys to decrypt the sealed message.

        .EXAMPLE
        $params = @{
            SealedBox = $encryptedMessage
            PublicKey = $publicKey
            PrivateKey = $privateKey
        }
        ConvertFrom-SodiumSealedBox @params

        Output:
        ```powershell
        Secret message revealed!
        ```

        Decrypts the given encrypted message using the specified public and private keys and returns the original string.

        .EXAMPLE
        $encryptedMessage | ConvertFrom-SodiumSealedBox -PublicKey $publicKey -PrivateKey $privateKey

        Output:
        ```powershell
        Confidential Data
        ```

        Uses pipeline input to decrypt the given encrypted message with the specified keys.

        .OUTPUTS
        System.String

        .NOTES
        Returns the original plaintext string after decryption.
        If decryption fails, an exception is thrown.

        .LINK
        https://psmodule.io/Sodium/Functions/ConvertFrom-SodiumSealedBox/

        .LINK
        https://doc.libsodium.org/public-key_cryptography/sealed_boxes
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The base64-encoded encrypted secret string to decrypt.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [Alias('CipherText')]
        [string] $SealedBox,

        # The base64-encoded public key used for decryption.
        [Parameter(Mandatory)]
        [string] $PublicKey,

        # The base64-encoded private key used for decryption.
        [Parameter(Mandatory)]
        [string] $PrivateKey
    )

    begin {
        if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
        $null = [PSModule.Sodium]::sodium_init()
    }

    process {
        $ciphertext = [Convert]::FromBase64String($SealedBox)
        $publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
        $privateKeyByteArray = [Convert]::FromBase64String($PrivateKey)

        if ($publicKeyByteArray.Length -ne 32) { throw 'Invalid public key.' }
        if ($privateKeyByteArray.Length -ne 32) { throw 'Invalid private key.' }

        $overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
        $decryptedBytes = New-Object byte[] ($ciphertext.Length - $overhead)

        # Attempt to decrypt
        $result = [PSModule.Sodium]::crypto_box_seal_open(
            $decryptedBytes, $ciphertext, [uint64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
        )

        if ($result -ne 0) {
            throw 'Decryption failed.'
        }

        return [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertFrom-SodiumSealedBox] - Done"
#endregion [functions] - [public] - [ConvertFrom-SodiumSealedBox]
#region [functions] - [public] - [ConvertTo-SodiumSealedBox]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-SodiumSealedBox] - Importing"
function ConvertTo-SodiumSealedBox {
    <#
        .SYNOPSIS
        Encrypts a message using a sealed public key box.

        .DESCRIPTION
        This function encrypts a given message using a public key with the SealedPublicKeyBox method from the Sodium library.
        The result is a base64-encoded sealed box that can only be decrypted by the corresponding private key.

        .EXAMPLE
        ConvertTo-SodiumSealedBox -Message "Hello world!" -PublicKey $publicKey

        Output:
        ```powershell
        hhCon4PO1X0TIPeh1i4GM6Wg9HSF5ge/x4L7p1vNd3lIdiJqNmBfswkcHipyM4HUr9wDLebjARVp5tsB
        ```

        Encrypts the message "Hello world!" using the provided base64-encoded public key and returns a base64-encoded sealed box.

        .EXAMPLE
        "Sensitive Data" | ConvertTo-SodiumSealedBox -PublicKey $publicKey

        Output:
        ```powershell
        p3PGL162uLCvrsCRLUDrc/Kfc5biGVzxRDg25ZdJoR9Y6ABZUKo8pvDoOGdchv0iBYQO2LP0Q6BkVbIDBUw=
        ```

        Uses pipeline input to encrypt the provided message using the specified public key.

        .OUTPUTS
        System.String

        .NOTES
        The function returns a base64-encoded sealed box string that can only be decrypted by the corresponding private key.

        .LINK
        https://psmodule.io/Sodium/Functions/ConvertTo-SodiumSealedBox/

        .LINK
        https://doc.libsodium.org/public-key_cryptography/sealed_boxes
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The message string to be encrypted.
        [Parameter(
            Mandatory,
            ValueFromPipeline,
            ValueFromPipelineByPropertyName
        )]
        [string] $Message,

        # The base64-encoded public key used for encryption.
        [Parameter(Mandatory)]
        [string] $PublicKey
    )
    begin {
        if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
        $null = [PSModule.Sodium]::sodium_init()
    }

    process {
        # Convert public key from Base64 or space-separated string
        try {
            $publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
        } catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
        if ($publicKeyByteArray.Length -ne 32) {
            throw "Invalid public key. Expected 32 bytes but got $($publicKeyByteArray.Length)."
        }

        $messageBytes = [System.Text.Encoding]::UTF8.GetBytes($Message)
        $overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
        $cipherLength = $messageBytes.Length + $overhead
        $ciphertext = New-Object byte[] $cipherLength

        # Encrypt message
        $result = [PSModule.Sodium]::crypto_box_seal($ciphertext, $messageBytes, [uint64]$messageBytes.Length, $publicKeyByteArray)

        if ($result -ne 0) {
            throw 'Encryption failed.'
        }

        return [Convert]::ToBase64String($ciphertext)
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-SodiumSealedBox] - Done"
#endregion [functions] - [public] - [ConvertTo-SodiumSealedBox]
#region [functions] - [public] - [New-SodiumKeyPair]
Write-Debug "[$scriptName] - [functions] - [public] - [New-SodiumKeyPair] - Importing"
function New-SodiumKeyPair {
    <#
        .SYNOPSIS
        Generates a new Sodium key pair.

        .DESCRIPTION
        This function creates a new cryptographic key pair using Sodium's PublicKeyBox.
        The keys are returned as a PowerShell custom object, with both the public and private keys
        encoded in base64 format.

        If a seed is provided, the key pair is deterministically generated using a SHA-256 derived seed.
        This ensures that the same input seed will always produce the same key pair.

        .EXAMPLE
        New-SodiumKeyPair

        Output:
        ```powershell
        PublicKey PrivateKey
        --------- ----------
        Ac0wdsq6lqLGktckJrasPcTbVRuUCU+OKzVpMno+v0g= PVXI64v00+aT2b2O6Q4l+SfMBUY2R/Nogsl2mp/hXAs=
        ```

        Generates a new key pair and returns a custom object containing the base64-encoded
        public and private keys.

        .EXAMPLE
        New-SodiumKeyPair -Seed "MySecureSeed"

        Output:
        ```powershell
        PublicKey PrivateKey
        --------- ----------
        WQakMx2mIAQMwLqiZteHUTwmMP6mUdK2FL0WEybWgB8= ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU=
        ```

        Generates a deterministic key pair using the given seed string. The same seed will produce
        the same key pair every time.

        .EXAMPLE
        "MySecureSeed" | New-SodiumKeyPair

        Output:
        ```powershell
        PublicKey PrivateKey
        --------- ----------
        WQakMx2mIAQMwLqiZteHUTwmMP6mUdK2FL0WEybWgB8= ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU=
        ```

        Generates a deterministic key pair using the given seed string via pipeline. The same seed will produce
        the same key pair every time.

        .OUTPUTS

        PSCustomObject

        .NOTES
        Returns a PowerShell custom object with the following properties:
        - **PublicKey**: The base64-encoded public key.
        - **PrivateKey**: The base64-encoded private key.
        If key generation fails, an exception is thrown.

        .LINK
        https://psmodule.io/Sodium/Functions/New-SodiumKeyPair/

        .LINK
        https://doc.libsodium.org/public-key_cryptography/public-key_signatures
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Scope = 'Function',
        Justification = 'Does not change state'
    )]
    [OutputType([PSCustomObject])]
    [CmdletBinding(DefaultParameterSetName = 'NewKeyPair')]
    param(
        # A seed value to use for key generation.
        [Parameter(
            Mandatory,
            ParameterSetName = 'SeededKeyPair',
            ValueFromPipeline
        )]
        [string] $Seed
    )

    begin {
        if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
        $null = [PSModule.Sodium]::sodium_init()
    }

    process {
        $pkSize = [PSModule.Sodium]::crypto_box_publickeybytes().ToUInt32()
        $skSize = [PSModule.Sodium]::crypto_box_secretkeybytes().ToUInt32()
        $publicKey = New-Object byte[] $pkSize
        $privateKey = New-Object byte[] $skSize

        switch ($PSCmdlet.ParameterSetName) {
            'SeededKeyPair' {
                # Derive a 32-byte seed from the provided string seed (using SHA-256)
                $seedBytes = [System.Text.Encoding]::UTF8.GetBytes($Seed)
                $derivedSeed = [System.Security.Cryptography.SHA256]::Create().ComputeHash($seedBytes)
                $result = [PSModule.Sodium]::crypto_box_seed_keypair($publicKey, $privateKey, $derivedSeed)
                break
            }
            default {
                $result = [PSModule.Sodium]::crypto_box_keypair($publicKey, $privateKey)
            }
        }

        if ($result -ne 0) {
            throw 'Key pair generation failed.'
        }

        return [pscustomobject]@{
            PublicKey  = [Convert]::ToBase64String($publicKey)
            PrivateKey = [Convert]::ToBase64String($privateKey)
        }
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [New-SodiumKeyPair] - Done"
#endregion [functions] - [public] - [New-SodiumKeyPair]
Write-Debug "[$scriptName] - [functions] - [public] - Done"
#endregion [functions] - [public]
#region [variables] - [private]
Write-Debug "[$scriptName] - [variables] - [private] - Processing folder"
#region [variables] - [private] - [Supported]
Write-Debug "[$scriptName] - [variables] - [private] - [Supported] - Importing"
$script:Supported = $false
Write-Debug "[$scriptName] - [variables] - [private] - [Supported] - Done"
#endregion [variables] - [private] - [Supported]
Write-Debug "[$scriptName] - [variables] - [private] - Done"
#endregion [variables] - [private]
#region [main]
Write-Debug "[$scriptName] - [main] - Importing"
switch ($true) {
    $IsLinux {
        Import-Module "$PSScriptRoot/libs/linux-x64/PSModule.Sodium.dll"
        $script:Supported = $true
    }
    $IsMacOS {
        if ("$(sysctl -n machdep.cpu.brand_string)" -Like 'Apple*') {
            Import-Module "$PSScriptRoot/libs/osx-arm64/PSModule.Sodium.dll"
        } else {
            Import-Module "$PSScriptRoot/libs/osx-x64/PSModule.Sodium.dll"
        }
        $script:Supported = $true
    }
    $IsWindows {
        if ([System.Environment]::Is64BitProcess) {
            Import-Module "$PSScriptRoot/libs/win-x64/PSModule.Sodium.dll"
        } else {
            Import-Module "$PSScriptRoot/libs/win-x86/PSModule.Sodium.dll"
        }
        $script:Supported = Assert-VisualCRedistributableInstalled -Version '14.0'
    }
    default {
        throw 'Unsupported platform. Please refer to the documentation for more information.'
    }
}
Write-Debug "[$scriptName] - [main] - Done"
#endregion [main]

#region Member exporter
$exports = @{
    Alias    = '*'
    Cmdlet   = ''
    Function = @(
        'ConvertFrom-SodiumSealedBox'
        'ConvertTo-SodiumSealedBox'
        'New-SodiumKeyPair'
    )
}
Export-ModuleMember @exports
#endregion Member exporter