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] - [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 {
        $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 {
        $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 {
        $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 [main]
Write-Debug "[$scriptName] - [main] - Importing"
switch ($true) {
    $IsLinux {
        Import-Module "$PSScriptRoot/libs/linux-x64/PSModule.Sodium.dll"
    }
    $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"
        }
    }
    $IsWindows {
        if ([System.Environment]::Is64BitProcess) {
            Import-Module "$PSScriptRoot/libs/win-x64/PSModule.Sodium.dll"
        } else {
            Import-Module "$PSScriptRoot/libs/win-x86/PSModule.Sodium.dll"
        }
    }
    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