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

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

        .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)]
        [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 "BASE64_PUBLIC_KEY"

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

        .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)]
        [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.

        .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')]
        [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