Private/Crypto/SecretBoxPrimitives.ps1

function Initialize-WormholeNaCl {
    [CmdletBinding()]
    param()

    $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSCommandPath))
    $assemblyPath = Join-Path $moduleRoot 'lib\NaCl.Net\NaCl.dll'
    if (-not (Test-Path -Path $assemblyPath)) {
        throw "Required dependency not found: $assemblyPath"
    }

    $loadedAssembly = [AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object {
            $_.Location -and [string]::Equals($_.Location, $assemblyPath, [System.StringComparison]::OrdinalIgnoreCase)
        } |
        Select-Object -First 1

    if ($null -eq $loadedAssembly) {
        $script:WormholeNaClAssembly = [System.Reflection.Assembly]::LoadFrom($assemblyPath)
    }
    else {
        $script:WormholeNaClAssembly = $loadedAssembly
    }
}

function Get-WormholeNaClType {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $TypeName
    )

    Initialize-WormholeNaCl

    if ($script:WormholeNaClAssembly) {
        $resolved = $script:WormholeNaClAssembly.GetType($TypeName, $false, $false)
        if ($resolved) {
            return $resolved
        }
    }

    foreach ($assembly in [AppDomain]::CurrentDomain.GetAssemblies()) {
        $resolved = $assembly.GetType($TypeName, $false, $false)
        if ($resolved) {
            return $resolved
        }
    }

    throw "Unable to resolve $TypeName type."
}

function New-WormholeNaClSecretBox {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]] $Key
    )

    $xsalsaType = Get-WormholeNaClType -TypeName 'NaCl.XSalsa20Poly1305'
    $constructor = $xsalsaType.GetConstructor([type[]]@([byte[]]))
    if ($null -eq $constructor) {
        throw 'NaCl.XSalsa20Poly1305(byte[]) constructor was not found.'
    }

    $arguments = [object[]]@(, $Key)
    $constructor.Invoke($arguments)
}

function New-WormholeSecretBoxNonce {
    [CmdletBinding()]
    param()

    $nonce = [byte[]]::new(24)
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try {
        $rng.GetBytes($nonce)
    }
    finally {
        $rng.Dispose()
    }

    $nonce
}

function Invoke-Poly1305Mac {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]] $Key,

        [Parameter()]
        [byte[]] $Message
    )

    if ($Key.Length -ne 32) {
        throw 'Poly1305 key must be 32 bytes.'
    }

    if ($null -eq $Message) {
        $Message = [byte[]]::new(0)
    }

    $polyType = Get-WormholeNaClType -TypeName 'NaCl.Poly1305'

    $poly = [System.Activator]::CreateInstance($polyType)
    try {
        $poly.SetKey($Key, 0)
        if ($Message.Length -gt 0) {
            $poly.Update($Message, 0, $Message.Length)
        }

        $tag = [byte[]]::new(16)
        $poly.Final($tag)
        return $tag
    }
    finally {
        if ($poly -is [System.IDisposable]) {
            $poly.Dispose()
        }
    }
}

function Protect-WormholeSecretBoxInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]] $Key,

        [Parameter()]
        [byte[]] $Plaintext,

        [Parameter(Mandatory = $true)]
        [byte[]] $Nonce
    )

    if ($Key.Length -ne 32) {
        throw 'SecretBox key must be 32 bytes.'
    }

    if ($Nonce.Length -ne 24) {
        throw 'SecretBox nonce must be 24 bytes.'
    }

    if ($null -eq $Plaintext) {
        $Plaintext = [byte[]]::new(0)
    }

    $secretBox = New-WormholeNaClSecretBox -Key $Key
    try {
        $boxed = [byte[]]::new($Plaintext.Length + 16)
        $secretBox.Encrypt($boxed, 0, $Plaintext, 0, $Plaintext.Length, $Nonce, 0)

        $result = [byte[]]::new(24 + $boxed.Length)
        [Array]::Copy($Nonce, 0, $result, 0, 24)
        [Array]::Copy($boxed, 0, $result, 24, $boxed.Length)
        return $result
    }
    finally {
        if ($secretBox -is [System.IDisposable]) {
            $secretBox.Dispose()
        }
    }
}

function Unprotect-WormholeSecretBoxInternal {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [byte[]] $Key,

        [Parameter(Mandatory = $true)]
        [byte[]] $Ciphertext
    )

    if ($Key.Length -ne 32) {
        throw 'SecretBox key must be 32 bytes.'
    }

    if ($Ciphertext.Length -lt 40) {
        throw 'SecretBox ciphertext must be at least 40 bytes (24 nonce + 16 mac).'
    }

    $nonce = [byte[]]::new(24)
    [Array]::Copy($Ciphertext, 0, $nonce, 0, 24)

    $boxedLength = $Ciphertext.Length - 24
    $boxed = [byte[]]::new($boxedLength)
    [Array]::Copy($Ciphertext, 24, $boxed, 0, $boxedLength)

    $plaintext = [byte[]]::new($boxedLength - 16)

    $secretBox = New-WormholeNaClSecretBox -Key $Key
    try {
        $ok = $secretBox.TryDecrypt($plaintext, 0, $boxed, 0, $boxed.Length, $nonce, 0)
        if (-not $ok) {
            throw 'SecretBox authentication failed.'
        }

        return $plaintext
    }
    finally {
        if ($secretBox -is [System.IDisposable]) {
            $secretBox.Dispose()
        }
    }
}