Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion PSModule/Sodium/Sodium.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ public static KeyPairBase64 GenerateKeyPairBase64(string seedText)
}

public static string DerivePublicKeyBase64(string privateKeyBase64)
{
return Convert.ToBase64String(DerivePublicKey(privateKeyBase64));
}

public static byte[] DerivePublicKey(string privateKeyBase64)
{
ArgumentNullException.ThrowIfNull(privateKeyBase64);
var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, "private key");
Expand All @@ -213,7 +218,7 @@ public static string DerivePublicKeyBase64(string privateKeyBase64)
{
throw new InvalidOperationException("Unable to derive public key from private key.");
}
return Convert.ToBase64String(publicKey);
return publicKey;
}
finally
{
Expand Down
28 changes: 28 additions & 0 deletions src/functions/private/Assert-SodiumNativeRuntime.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function Assert-SodiumNativeRuntime {
<#
.SYNOPSIS
Provides platform-specific diagnostics after Sodium native initialization fails.

.DESCRIPTION
Checks the Windows Visual C++ runtime after a native initialization exception and throws a targeted message when the required
runtime is unavailable.

.NOTES
This function only runs after native library loading fails, which cannot be reproduced safely after the library is loaded in the test process.
#>
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute()]
[OutputType([void])]
[CmdletBinding()]
param()

process {
if ($IsWindows -and $script:ProcessArchitecture -in @('X64', 'X86')) {
$hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture
if (-not $hasRuntime) {
$message = "Sodium native initialization failed; the Visual C++ Redistributable for " +
"$($script:ProcessArchitecture) appears to be missing or below the required version."
throw $message
}
}
}
}
11 changes: 1 addition & 10 deletions src/functions/private/Initialize-Sodium.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,7 @@
$initializationResult = [PSModule.Sodium]::sodium_init()
} catch {
$script:Supported = $false
if ($IsWindows) {
if ($script:ProcessArchitecture -in @('X64', 'X86')) {
$hasRuntime = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $script:ProcessArchitecture
if (-not $hasRuntime) {
$message = "Sodium native initialization failed; the Visual C++ Redistributable for " +
"$($script:ProcessArchitecture) appears to be missing or below the required version."
throw $message
}
}
}
Assert-SodiumNativeRuntime
throw
}
if ($initializationResult -lt 0) {
Expand Down
65 changes: 65 additions & 0 deletions src/functions/private/Resolve-SodiumRuntimeIdentifier.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
function Resolve-SodiumRuntimeIdentifier {
<#
.SYNOPSIS
Resolves the native Sodium runtime identifier.

.DESCRIPTION
Maps the active operating system and process architecture to the runtime identifier used by the bundled native library.
#>
[OutputType([string])]
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[System.Runtime.InteropServices.Architecture] $ProcessArchitecture,

[Parameter()]
[switch] $Linux,

[Parameter()]
[switch] $MacOS,

[Parameter()]
[switch] $Windows
)

process {
switch ($true) {
$Linux {
switch ($ProcessArchitecture) {
'Arm64' { return 'linux-arm64' }
'X64' { return 'linux-x64' }
default {
$message = "Unsupported Linux process architecture: $ProcessArchitecture. " +
'Please refer to the documentation for supported architectures.'
throw $message
}
}
}
$MacOS {
switch ($ProcessArchitecture) {
'Arm64' { return 'osx-arm64' }
'X64' { return 'osx-x64' }
default {
$message = "Unsupported macOS process architecture: $ProcessArchitecture. " +
'Please refer to the documentation for supported architectures.'
throw $message
}
}
}
$Windows {
switch ($ProcessArchitecture) {
'X64' { return 'win-x64' }
'X86' { return 'win-x86' }
default {
$message = "Unsupported Windows process architecture: $ProcessArchitecture. " +
'Please refer to the documentation for supported architectures.'
throw $message
}
}
}
default {
throw 'Unsupported platform. Please refer to the documentation for more information.'
}
}
}
}
4 changes: 0 additions & 4 deletions src/functions/public/ConvertFrom-SodiumSealedBox.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,6 @@
[string] $PrivateKey
)

begin {
Initialize-Sodium
}

process {
try {
if (-not [string]::IsNullOrWhiteSpace($PublicKey)) {
Expand Down
4 changes: 0 additions & 4 deletions src/functions/public/ConvertTo-SodiumSealedBox.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,6 @@
[ValidateNotNullOrEmpty()]
[string] $PublicKey
)
begin {
Initialize-Sodium
}

process {
try {
return [PSModule.Sodium]::SealBase64($Message, $PublicKey)
Expand Down
11 changes: 5 additions & 6 deletions src/functions/public/Get-SodiumPublicKey.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
https://psmodule.io/Sodium/Functions/Get-SodiumPublicKey/
#>

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseOutputTypeCorrectly', '',
Justification = 'The unary comma preserves the byte array as one pipeline object.'
)]
[OutputType([string], ParameterSetName = 'Base64')]
[OutputType([byte[]], ParameterSetName = 'AsByteArray')]
[CmdletBinding(DefaultParameterSetName = 'Base64')]
Expand All @@ -81,14 +85,10 @@
[switch] $AsByteArray
)

begin {
Initialize-Sodium
}

process {
if ($AsByteArray) {
try {
return [System.Convert]::FromBase64String([PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey))
return , ([PSModule.Sodium]::DerivePublicKey($PrivateKey))
} catch [System.Management.Automation.MethodInvocationException] {
throw $_.Exception.InnerException
}
Expand All @@ -100,5 +100,4 @@
}
}

end {}
}
4 changes: 0 additions & 4 deletions src/functions/public/New-SodiumKeyPair.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,6 @@
[string] $Seed
)

begin {
Initialize-Sodium
}

process {
try {
if ($PSCmdlet.ParameterSetName -eq 'SeededKeyPair') {
Expand Down
34 changes: 2 additions & 32 deletions src/main.ps1
Original file line number Diff line number Diff line change
@@ -1,37 +1,7 @@
$processArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture

switch ($true) {
$IsLinux {
switch ($processArchitecture) {
'Arm64' { $runtimeIdentifier = 'linux-arm64' }
'X64' { $runtimeIdentifier = 'linux-x64' }
default {
throw "Unsupported Linux process architecture: $processArchitecture. Please refer to the documentation for supported architectures."
}
}
}
$IsMacOS {
switch ($processArchitecture) {
'Arm64' { $runtimeIdentifier = 'osx-arm64' }
'X64' { $runtimeIdentifier = 'osx-x64' }
default {
throw "Unsupported macOS process architecture: $processArchitecture. Please refer to the documentation for supported architectures."
}
}
}
$IsWindows {
switch ($processArchitecture) {
'X64' { $runtimeIdentifier = 'win-x64' }
'X86' { $runtimeIdentifier = 'win-x86' }
default {
throw "Unsupported Windows process architecture: $processArchitecture. Please refer to the documentation for supported architectures."
}
}
}
default {
throw 'Unsupported platform. Please refer to the documentation for more information.'
}
}
$runtimeIdentifier = Resolve-SodiumRuntimeIdentifier -ProcessArchitecture $processArchitecture `
-Linux:$IsLinux -MacOS:$IsMacOS -Windows:$IsWindows

$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath "libs/$runtimeIdentifier/PSModule.Sodium.dll"
Import-Module $assemblyPath -ErrorAction Stop
Expand Down
138 changes: 138 additions & 0 deletions tests/Sodium.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,14 @@ Describe 'Sodium' {
$derivedPublicKey | Should -Be $expectedPublicKey
}

It 'Get-SodiumPublicKey - Returns the public key as a byte array' {
$keyPair = New-SodiumKeyPair
$derivedPublicKey = Get-SodiumPublicKey -PrivateKey $keyPair.PrivateKey -AsByteArray

($derivedPublicKey -is [byte[]]) | Should -BeTrue
[Convert]::ToBase64String($derivedPublicKey) | Should -Be $keyPair.PublicKey
}

It 'Get-SodiumPublicKey - Throws an error when an invalid private key is provided' {
$invalidPrivateKey = 'InvalidKey'

Expand All @@ -185,5 +193,135 @@ Describe 'Sodium' {
$result | Should -BeTrue
}
}

It 'Assert-VisualCRedistributableInstalled reports a missing runtime' {
InModuleScope Sodium {
Mock Get-ItemProperty { $null }

$result = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture 'X64' 3>$null
$result | Should -BeFalse
}
}
Comment thread
Copilot marked this conversation as resolved.

It 'Assert-VisualCRedistributableInstalled accepts a matching runtime' {
InModuleScope Sodium {
Mock Get-ItemProperty {
[pscustomobject]@{
Installed = 1
Version = 'v14.30.30704.0'
}
}

Set-Variable -Name IsWindows -Value $true -Scope Script
try {
$result = Assert-VisualCRedistributableInstalled -Version '14.0'
$result | Should -BeTrue
} finally {
Remove-Variable -Name IsWindows -Scope Script
}
}
}

It 'Initialize-Sodium treats repeated initialization as a no-op' {
InModuleScope Sodium {
$script:SodiumInitialized | Should -BeTrue
{ Initialize-Sodium } | Should -Not -Throw
}
}

It 'Initialize-Sodium restores cached native buffer sizes' {
InModuleScope Sodium {
$script:SodiumInitialized = $false
$script:SodiumPublicKeyBytes = $null
$script:SodiumPrivateKeyBytes = $null
$script:SodiumSealBytes = $null
$script:SodiumSeedBytes = $null

Initialize-Sodium

$script:SodiumInitialized | Should -BeTrue
$script:SodiumPublicKeyBytes | Should -Be 32
$script:SodiumPrivateKeyBytes | Should -Be 32
$script:SodiumSealBytes | Should -Be 48
$script:SodiumSeedBytes | Should -Be 32
}
}

It 'Initialize-Sodium rejects an unsupported platform' {
InModuleScope Sodium {
$script:Supported = $false
try {
{ Initialize-Sodium } | Should -Throw 'Sodium is not supported on this platform.'
} finally {
$script:Supported = $true
}
}
}

It 'Resolve-SodiumRuntimeIdentifier maps every supported runtime' {
InModuleScope Sodium {
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm64' -Linux | Should -Be 'linux-arm64'
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -Linux | Should -Be 'linux-x64'
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm64' -MacOS | Should -Be 'osx-arm64'
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -MacOS | Should -Be 'osx-x64'
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' -Windows | Should -Be 'win-x64'
Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X86' -Windows | Should -Be 'win-x86'
}
}

It 'Resolve-SodiumRuntimeIdentifier rejects unsupported runtimes' {
InModuleScope Sodium {
{ Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -Linux } |
Should -Throw 'Unsupported Linux process architecture: Arm.*'
{ Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -MacOS } |
Should -Throw 'Unsupported macOS process architecture: Arm.*'
{ Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'Arm' -Windows } |
Should -Throw 'Unsupported Windows process architecture: Arm.*'
{ Resolve-SodiumRuntimeIdentifier -ProcessArchitecture 'X64' } |
Should -Throw 'Unsupported platform.*'
}
}
}

Context 'Parallel sessions' {
It 'Loads and completes crypto round trips in parallel runspaces' {
$modulePath = (Get-Module -Name Sodium -ErrorAction Stop).Path
Test-Path -Path $modulePath | Should -BeTrue
$results = 1..4 | ForEach-Object -Parallel {
Import-Module -Name $using:modulePath -Force
$keyPair = New-SodiumKeyPair -Seed "Runspace-$_"
$message = "Parallel runspace $_"
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PrivateKey $keyPair.PrivateKey
} -ThrottleLimit 4

$results | Should -HaveCount 4
$results | Should -Contain 'Parallel runspace 1'
$results | Should -Contain 'Parallel runspace 4'
}

It 'Loads and completes crypto round trips in parallel processes' {
$modulePath = (Get-Module -Name Sodium -ErrorAction Stop).Path
$jobs = 1..4 | ForEach-Object {
Start-Job -ScriptBlock {
$id = $args[0]
$modulePath = $args[1]
Import-Module -Name $modulePath -Force
$keyPair = New-SodiumKeyPair -Seed "Process-$id"
$message = "Parallel process $id"
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PrivateKey $keyPair.PrivateKey
} -ArgumentList $_, $modulePath
}

try {
$results = $jobs | Receive-Job -Wait
$results | Should -HaveCount 4
$results | Should -Contain 'Parallel process 1'
$results | Should -Contain 'Parallel process 4'
} finally {
$jobs | Remove-Job -Force
}
}
}
}
Loading