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] - [ConvertTo-ByteArray]
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-ByteArray] - Importing"
function ConvertTo-ByteArray {
    <#
        .SYNOPSIS
        Converts a string into a byte array.

        .DESCRIPTION
        This function attempts to convert an input string into a byte array.
        It first checks if the input is a Base64-encoded string and attempts to decode it.
        If the Base64 decoding fails, it assumes the input consists of space-separated decimal values and converts them to bytes.

        .EXAMPLE
        ConvertTo-ByteArray "SGVsbG8gd29ybGQ="

        Converts the Base64-encoded string "SGVsbG8gd29ybGQ=" into a byte array.

        .EXAMPLE
        ConvertTo-ByteArray "72 101 108 108 111"

        Converts the space-separated decimal values into a byte array representing the string "Hello".

        .NOTES
        This function assumes that if the input is not valid Base64, it must be space-separated decimal values.
    #>

    [OutputType([byte[]])]
    [CmdletBinding()]
    param (
        # The input string to be converted into a byte array.
        [Parameter(Mandatory = $true)]
        [string] $InputString
    )

    # Check if it's a Base64 string first (GitHub API provides keys in Base64)
    try {
        return [Convert]::FromBase64String($InputString)
    } catch {
        # If not Base64, assume it's space-separated decimal values
        return $InputString -split '\s+' | ForEach-Object { [byte]$_ }
    }
}
Write-Debug "[$scriptName] - [functions] - [private] - [ConvertTo-ByteArray] - Done"
#endregion [functions] - [private] - [ConvertTo-ByteArray]
#region [functions] - [private] - [Initialize-Sodium]
Write-Debug "[$scriptName] - [functions] - [private] - [Initialize-Sodium] - Importing"
function Initialize-Sodium {
    <#
        .SYNOPSIS
        Initializes the Sodium cryptographic library.

        .DESCRIPTION
        Calls the sodium_init() function from the PSModule.Sodium namespace to initialize the Sodium cryptographic library.
        This function must be called before using any other Sodium cryptographic functions.

        .EXAMPLE
        Initialize-Sodium

        Initializes the Sodium cryptographic library for use.

        .NOTES
        Ensure that the PSModule.Sodium module is properly installed and loaded before calling this function.
    #>

    [CmdletBinding()]
    param ()

    $null = [PSModule.Sodium]::sodium_init()
}
Write-Debug "[$scriptName] - [functions] - [private] - [Initialize-Sodium] - Done"
#endregion [functions] - [private] - [Initialize-Sodium]
Write-Debug "[$scriptName] - [functions] - [private] - Done"
#endregion [functions] - [private]
#region [functions] - [public]
Write-Debug "[$scriptName] - [functions] - [public] - Processing folder"
#region [functions] - [public] - [ConvertFrom-SodiumEncryptedString]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertFrom-SodiumEncryptedString] - Importing"
function ConvertFrom-SodiumEncryptedString {
    <#
        .SYNOPSIS
        Decrypts a base64-encoded, Sodium-encrypted string.

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

        .EXAMPLE
        $params = @{
            EncryptedSecret = $encryptedSecret
            PublicKey = $publicKey
            PrivateKey = $privateKey
        }
        ConvertFrom-SodiumEncryptedString @params

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

        .LINK
        https://psmodule.io/Sodium/Functions/ConvertFrom-SodiumEncryptedString/
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The base64-encoded encrypted secret string to decrypt.
        [Parameter(Mandatory)]
        [string] $Secret,

        # 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 {
        Initialize-Sodium
    }

    process {
        $ciphertext = [Convert]::FromBase64String($Secret)
        $publicKeyByteArray = ConvertTo-ByteArray $PublicKey
        $privateKeyByteArray = ConvertTo-ByteArray $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-SodiumEncryptedString] - Done"
#endregion [functions] - [public] - [ConvertFrom-SodiumEncryptedString]
#region [functions] - [public] - [ConvertTo-SodiumEncryptedString]
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-SodiumEncryptedString] - Importing"
function ConvertTo-SodiumEncryptedString {
    <#
        .SYNOPSIS
        Encrypts a secret using a sealed public key box.

        .DESCRIPTION
        This function encrypts a given secret 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-SodiumEncryptedString -Secret "mysecret" -PublicKey "BASE64_PUBLIC_KEY"

        Encrypts the secret "mysecret" using the provided base64-encoded public key and returns a base64-encoded sealed box.

        .LINK
        https://psmodule.io/Sodium/Functions/ConvertTo-SodiumEncryptedString/
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        # The secret string to be encrypted.
        [Parameter(Mandatory)]
        [string] $Secret,

        # The base64-encoded public key used for encryption.
        [Parameter(Mandatory)]
        [string] $PublicKey
    )
    begin {
        Initialize-Sodium
    }

    process {
        # Convert public key from Base64 or space-separated string
        $publicKeyByteArray = ConvertTo-ByteArray $PublicKey
        if ($publicKeyByteArray.Length -ne 32) {
            throw "Invalid public key. Expected 32 bytes but got $($publicKeyByteArray.Length)."
        }

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

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

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

        return [Convert]::ToBase64String($ciphertext)
    }
}
Write-Debug "[$scriptName] - [functions] - [public] - [ConvertTo-SodiumEncryptedString] - Done"
#endregion [functions] - [public] - [ConvertTo-SodiumEncryptedString]
#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.

        .EXAMPLE
        New-SodiumKeyPair

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

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

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute(
        'PSUseShouldProcessForStateChangingFunctions', '',
        Scope = 'Function',
        Justification = 'Does not change state'
    )]
    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param()

    begin {
        Initialize-Sodium
    }

    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

        # Generate key pair
        $null = [PSModule.Sodium]::crypto_box_keypair($publicKey, $privateKey)

        # Convert to Base64 for easy storage/transfer
        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-SodiumEncryptedString'
        'ConvertTo-SodiumEncryptedString'
        'New-SodiumKeyPair'
    )
}
Export-ModuleMember @exports
#endregion Member exporter