diff --git a/PSModule/Sodium.csproj b/PSModule/Sodium.csproj
index 9bfec7c..b121fdd 100644
--- a/PSModule/Sodium.csproj
+++ b/PSModule/Sodium.csproj
@@ -3,11 +3,12 @@
net8.0
PSModule.Sodium
+ true
-
-
+
+
diff --git a/PSModule/Sodium/Sodium.cs b/PSModule/Sodium/Sodium.cs
index 44b5f33..08ec69e 100644
--- a/PSModule/Sodium/Sodium.cs
+++ b/PSModule/Sodium/Sodium.cs
@@ -1,36 +1,342 @@
using System;
using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
namespace PSModule
{
- public static class Sodium
+ public static partial class Sodium
{
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int sodium_init();
+ private static partial class Native
+ {
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int sodium_init();
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int crypto_box_keypair([Out] byte[] publicKey, [Out] byte[] privateKey);
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int crypto_box_seed_keypair([Out] byte[] publicKey, [Out] byte[] privateKey, byte[] seed);
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int crypto_box_seal([Out] byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey);
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int crypto_box_seal_open([Out] byte[] decrypted, byte[] ciphertext, ulong ciphertextLength, byte[] publicKey, byte[] privateKey);
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern UIntPtr crypto_box_publickeybytes();
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial UIntPtr crypto_box_publickeybytes();
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern UIntPtr crypto_box_secretkeybytes();
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial UIntPtr crypto_box_secretkeybytes();
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern UIntPtr crypto_box_sealbytes();
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial UIntPtr crypto_box_sealbytes();
- [DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
- public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial UIntPtr crypto_box_seedbytes();
+ [LibraryImport("libsodium")]
+ [DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
+ [UnmanagedCallConv(CallConvs = new[] { typeof(System.Runtime.CompilerServices.CallConvCdecl) })]
+ public static partial int crypto_scalarmult_base([Out] byte[] publicKey, byte[] privateKey);
+ }
+
+ // libsodium guarantees these *_bytes() functions return constants and are safe to call without sodium_init().
+ private static readonly int PublicKeyBytes = GetRequiredLength(Native.crypto_box_publickeybytes());
+ private static readonly int SecretKeyBytes = GetRequiredLength(Native.crypto_box_secretkeybytes());
+ private static readonly int SealBytes = GetRequiredLength(Native.crypto_box_sealbytes());
+ private static readonly int SeedBytes = GetRequiredLength(Native.crypto_box_seedbytes());
+
+ public static int sodium_init()
+ {
+ return Native.sodium_init();
+ }
+
+ public static int crypto_box_keypair(byte[] publicKey, byte[] privateKey)
+ {
+ ValidateMinimumBufferLength(publicKey, PublicKeyBytes, nameof(publicKey));
+ ValidateMinimumBufferLength(privateKey, SecretKeyBytes, nameof(privateKey));
+
+ return Native.crypto_box_keypair(publicKey, privateKey);
+ }
+
+ public static int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed)
+ {
+ ValidateMinimumBufferLength(publicKey, PublicKeyBytes, nameof(publicKey));
+ ValidateMinimumBufferLength(privateKey, SecretKeyBytes, nameof(privateKey));
+ ValidateExactBufferLength(seed, SeedBytes, nameof(seed));
+
+ return Native.crypto_box_seed_keypair(publicKey, privateKey, seed);
+ }
+
+ public static int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey)
+ {
+ ValidateMinimumBufferLength(message, mlen, nameof(message));
+ ValidateMinimumBufferLength(ciphertext, checked(mlen + (ulong)SealBytes), nameof(ciphertext));
+ ValidateExactBufferLength(publicKey, PublicKeyBytes, nameof(publicKey));
+
+ return Native.crypto_box_seal(ciphertext, message, mlen, publicKey);
+ }
+
+ public static int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong ciphertextLength, byte[] publicKey, byte[] privateKey)
+ {
+ var sealBytes = (ulong)SealBytes;
+ if (ciphertextLength < sealBytes)
+ {
+ throw new ArgumentException($"The ciphertext must be at least {sealBytes} bytes.", nameof(ciphertext));
+ }
+
+ ValidateMinimumBufferLength(ciphertext, ciphertextLength, nameof(ciphertext));
+ ValidateMinimumBufferLength(decrypted, ciphertextLength - sealBytes, nameof(decrypted));
+ ValidateExactBufferLength(publicKey, PublicKeyBytes, nameof(publicKey));
+ ValidateExactBufferLength(privateKey, SecretKeyBytes, nameof(privateKey));
+
+ return Native.crypto_box_seal_open(decrypted, ciphertext, ciphertextLength, publicKey, privateKey);
+ }
+
+ public static UIntPtr crypto_box_publickeybytes()
+ {
+ return (UIntPtr)PublicKeyBytes;
+ }
+
+ public static UIntPtr crypto_box_secretkeybytes()
+ {
+ return (UIntPtr)SecretKeyBytes;
+ }
+
+ public static UIntPtr crypto_box_sealbytes()
+ {
+ return (UIntPtr)SealBytes;
+ }
+
+ public static UIntPtr crypto_box_seedbytes()
+ {
+ return (UIntPtr)SeedBytes;
+ }
+
+ public static int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey)
+ {
+ ValidateMinimumBufferLength(publicKey, PublicKeyBytes, nameof(publicKey));
+ ValidateExactBufferLength(privateKey, SecretKeyBytes, nameof(privateKey));
+
+ return Native.crypto_scalarmult_base(publicKey, privateKey);
+ }
+
+ // ---------- Base64-centric high-level API (see issue #52) ----------
+ // These helpers do base64/UTF-8 encoding and native interop in a single managed call,
+ // avoiding the overhead of multiple PowerShell-level method invocations on the hot path.
+
+ public sealed class KeyPairBase64
+ {
+ public string PublicKey { get; }
+ public string PrivateKey { get; }
+
+ internal KeyPairBase64(string publicKey, string privateKey)
+ {
+ PublicKey = publicKey;
+ PrivateKey = privateKey;
+ }
+ }
+
+ public static KeyPairBase64 GenerateKeyPairBase64()
+ {
+ var publicKey = new byte[PublicKeyBytes];
+ var privateKey = new byte[SecretKeyBytes];
+ try
+ {
+ if (Native.crypto_box_keypair(publicKey, privateKey) != 0)
+ {
+ throw new InvalidOperationException("Key pair generation failed.");
+ }
+ return new KeyPairBase64(Convert.ToBase64String(publicKey), Convert.ToBase64String(privateKey));
+ }
+ finally
+ {
+ CryptographicOperations.ZeroMemory(privateKey);
+ }
+ }
+
+ public static KeyPairBase64 GenerateKeyPairBase64(string seedText)
+ {
+ ArgumentNullException.ThrowIfNull(seedText);
+ var publicKey = new byte[PublicKeyBytes];
+ var privateKey = new byte[SecretKeyBytes];
+ var seedSource = Encoding.UTF8.GetBytes(seedText);
+ var seed = new byte[SeedBytes];
+ try
+ {
+ if (!SHA256.TryHashData(seedSource, seed, out var written) || written != SeedBytes)
+ {
+ throw new InvalidOperationException("Failed to derive seed bytes from input.");
+ }
+ if (Native.crypto_box_seed_keypair(publicKey, privateKey, seed) != 0)
+ {
+ throw new InvalidOperationException("Seeded key pair generation failed.");
+ }
+ return new KeyPairBase64(Convert.ToBase64String(publicKey), Convert.ToBase64String(privateKey));
+ }
+ finally
+ {
+ CryptographicOperations.ZeroMemory(privateKey);
+ CryptographicOperations.ZeroMemory(seed);
+ CryptographicOperations.ZeroMemory(seedSource);
+ }
+ }
+
+ public static string DerivePublicKeyBase64(string privateKeyBase64)
+ {
+ ArgumentNullException.ThrowIfNull(privateKeyBase64);
+ var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, "private key");
+ var publicKey = new byte[PublicKeyBytes];
+ try
+ {
+ if (Native.crypto_scalarmult_base(publicKey, privateKey) != 0)
+ {
+ throw new InvalidOperationException("Unable to derive public key from private key.");
+ }
+ return Convert.ToBase64String(publicKey);
+ }
+ finally
+ {
+ CryptographicOperations.ZeroMemory(privateKey);
+ }
+ }
+
+ public static string SealBase64(string plaintext, string publicKeyBase64)
+ {
+ ArgumentNullException.ThrowIfNull(plaintext);
+ ArgumentNullException.ThrowIfNull(publicKeyBase64);
+ var publicKey = DecodeBase64Exact(publicKeyBase64, PublicKeyBytes, "public key");
+ var message = Encoding.UTF8.GetBytes(plaintext);
+ var ciphertext = new byte[message.Length + SealBytes];
+ try
+ {
+ if (Native.crypto_box_seal(ciphertext, message, (ulong)message.LongLength, publicKey) != 0)
+ {
+ throw new InvalidOperationException("Encryption failed.");
+ }
+ return Convert.ToBase64String(ciphertext);
+ }
+ finally
+ {
+ CryptographicOperations.ZeroMemory(message);
+ }
+ }
+
+ public static string OpenSealBase64(string ciphertextBase64, string privateKeyBase64)
+ {
+ return OpenSealBase64Core(ciphertextBase64, privateKeyBase64, publicKeyBase64: null);
+ }
+
+ public static string OpenSealBase64(string ciphertextBase64, string privateKeyBase64, string publicKeyBase64)
+ {
+ return OpenSealBase64Core(ciphertextBase64, privateKeyBase64, publicKeyBase64);
+ }
+
+ private static string OpenSealBase64Core(string ciphertextBase64, string privateKeyBase64, string publicKeyBase64)
+ {
+ ArgumentNullException.ThrowIfNull(ciphertextBase64);
+ ArgumentNullException.ThrowIfNull(privateKeyBase64);
+
+ var ciphertext = Convert.FromBase64String(ciphertextBase64);
+ if (ciphertext.Length < SealBytes)
+ {
+ throw new ArgumentException($"Invalid sealed box. Expected at least {SealBytes} bytes but got {ciphertext.Length}.");
+ }
+ var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, "private key");
+ var publicKey = new byte[PublicKeyBytes];
+ var decrypted = new byte[ciphertext.Length - SealBytes];
+ try
+ {
+ if (string.IsNullOrEmpty(publicKeyBase64))
+ {
+ if (Native.crypto_scalarmult_base(publicKey, privateKey) != 0)
+ {
+ throw new InvalidOperationException("Unable to derive public key from private key.");
+ }
+ }
+ else
+ {
+ var providedPk = DecodeBase64Exact(publicKeyBase64, PublicKeyBytes, "public key");
+ Buffer.BlockCopy(providedPk, 0, publicKey, 0, PublicKeyBytes);
+ }
+
+ if (Native.crypto_box_seal_open(decrypted, ciphertext, (ulong)ciphertext.LongLength, publicKey, privateKey) != 0)
+ {
+ throw new InvalidOperationException("Decryption failed.");
+ }
+ return Encoding.UTF8.GetString(decrypted);
+ }
+ finally
+ {
+ CryptographicOperations.ZeroMemory(privateKey);
+ CryptographicOperations.ZeroMemory(decrypted);
+ }
+ }
+
+ private static byte[] DecodeBase64Exact(string value, int expectedLength, string label)
+ {
+ var bytes = Convert.FromBase64String(value);
+ if (bytes.Length != expectedLength)
+ {
+ throw new ArgumentException($"Invalid {label}. Expected {expectedLength} bytes but got {bytes.Length}.");
+ }
+ return bytes;
+ }
+
+ private static int GetRequiredLength(UIntPtr length)
+ {
+ var value = length.ToUInt64();
+ if (value > int.MaxValue)
+ {
+ throw new OverflowException("The Sodium buffer length exceeds the maximum supported array length.");
+ }
+
+ return (int)value;
+ }
+
+ private static void ValidateExactBufferLength(byte[] buffer, int expectedLength, string parameterName)
+ {
+ ArgumentNullException.ThrowIfNull(buffer, parameterName);
+
+ if (buffer.Length != expectedLength)
+ {
+ throw new ArgumentException($"The buffer must be exactly {expectedLength} bytes.", parameterName);
+ }
+ }
+
+ private static void ValidateMinimumBufferLength(byte[] buffer, int minimumLength, string parameterName)
+ {
+ ValidateMinimumBufferLength(buffer, (ulong)minimumLength, parameterName);
+ }
+
+ private static void ValidateMinimumBufferLength(byte[] buffer, ulong minimumLength, string parameterName)
+ {
+ ArgumentNullException.ThrowIfNull(buffer, parameterName);
+
+ if ((ulong)buffer.LongLength < minimumLength)
+ {
+ throw new ArgumentException($"The buffer must be at least {minimumLength} bytes.", parameterName);
+ }
+ }
}
}
diff --git a/PSModule/build.ps1 b/PSModule/build.ps1
index 0cbd84b..738f89a 100644
--- a/PSModule/build.ps1
+++ b/PSModule/build.ps1
@@ -1,3 +1,5 @@
+$ErrorActionPreference = 'Stop'
+
Remove-Item -Path "$PSScriptRoot/../src/libs" -Recurse -Force -ErrorAction SilentlyContinue
$targetRuntimes = @(
@@ -10,13 +12,20 @@ $targetRuntimes = @(
)
Push-Location $PSScriptRoot
-$targetRuntimes | ForEach-Object {
- dotnet publish -r $_
- $source = "$PSScriptRoot/bin/Release/net8.0/$_/publish"
- $destination = "$PSScriptRoot/../src/libs/$_"
- Copy-Item -Path $source -Destination $destination -Recurse -Force
+try {
+ $targetRuntimes | ForEach-Object {
+ dotnet publish -c Release -r $_
+ if ($LASTEXITCODE -ne 0) {
+ throw "dotnet publish failed for runtime '$_'."
+ }
+
+ $source = "$PSScriptRoot/bin/Release/net8.0/$_/publish"
+ $destination = "$PSScriptRoot/../src/libs/$_"
+ Copy-Item -Path $source -Destination $destination -Recurse -Force
+ }
+} finally {
+ Pop-Location
}
-Pop-Location
Get-ChildItem -Path $PSScriptRoot -Directory -Recurse | Where-Object { $_.Name -in 'bin', 'obj' } | ForEach-Object {
Write-Warning "Deleting $($_.FullName)"
diff --git a/src/functions/private/Assert-VisualCRedistributableInstalled.ps1 b/src/functions/private/Assert-VisualCRedistributableInstalled.ps1
index c5ca9b6..18d2313 100644
--- a/src/functions/private/Assert-VisualCRedistributableInstalled.ps1
+++ b/src/functions/private/Assert-VisualCRedistributableInstalled.ps1
@@ -22,26 +22,37 @@ function Assert-VisualCRedistributableInstalled {
#>
[CmdletBinding()]
[OutputType([bool])]
- param (
+ param(
# The minimum required version of the Visual C++ Redistributable.
[Parameter(Mandatory)]
- [Version] $Version
+ [Version] $Version,
+
+ # The process architecture that determines which Redistributable runtime is required.
+ [Parameter()]
+ [ValidateSet('X64', 'X86')]
+ [string] $Architecture = $(if ([System.Environment]::Is64BitProcess) { 'X64' } else { 'X86' })
)
- process {
+ begin {
$result = $false
+ }
+
+ process {
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
+ $key = "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\$Architecture"
+ $runtimeInfo = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
+ if ($runtimeInfo -and $runtimeInfo.Version -and ($runtimeInfo.Installed -ne 0)) {
+ $installedVersion = [Version]$runtimeInfo.Version.TrimStart('v', 'V')
+ $result = $installedVersion -ge $Version
}
}
if (-not $result) {
- Write-Warning 'The Visual C++ Redistributable for Visual Studio 2015 or later is required.'
+ Write-Warning "The Visual C++ Redistributable for Visual Studio 2015 or later ($Architecture) 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
}
+
+ end {}
}
diff --git a/src/functions/private/Initialize-Sodium.ps1 b/src/functions/private/Initialize-Sodium.ps1
new file mode 100644
index 0000000..2ad47bb
--- /dev/null
+++ b/src/functions/private/Initialize-Sodium.ps1
@@ -0,0 +1,50 @@
+function Initialize-Sodium {
+ <#
+ .SYNOPSIS
+ Initializes Sodium for cryptographic operations.
+
+ .DESCRIPTION
+ Initializes the native Sodium library once per module session and caches fixed buffer sizes used by the public commands.
+
+ .NOTES
+ Requires the platform-specific PSModule.Sodium assembly and native libsodium runtime to be loaded.
+ #>
+ [OutputType([void])]
+ [CmdletBinding()]
+ param()
+
+ begin {}
+
+ process {
+ if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
+ if ($script:SodiumInitialized) { return }
+
+ try {
+ $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
+ }
+ }
+ }
+ throw
+ }
+ if ($initializationResult -lt 0) {
+ throw 'Sodium initialization failed.'
+ }
+
+ $script:SodiumPublicKeyBytes = [PSModule.Sodium]::crypto_box_publickeybytes().ToUInt32()
+ $script:SodiumPrivateKeyBytes = [PSModule.Sodium]::crypto_box_secretkeybytes().ToUInt32()
+ $script:SodiumSealBytes = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
+ $script:SodiumSeedBytes = [PSModule.Sodium]::crypto_box_seedbytes().ToUInt32()
+ $script:SodiumInitialized = $true
+ }
+
+ end {}
+}
diff --git a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1
index 0fcaf62..5b72e4a 100644
--- a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1
+++ b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1
@@ -55,6 +55,7 @@
ValueFromPipelineByPropertyName
)]
[Alias('CipherText')]
+ [ValidateNotNullOrEmpty()]
[string] $SealedBox,
# The base64-encoded public key used for decryption.
@@ -63,38 +64,22 @@
# The base64-encoded private key used for decryption.
[Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
[string] $PrivateKey
)
begin {
- if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
- $null = [PSModule.Sodium]::sodium_init()
+ Initialize-Sodium
}
process {
- $ciphertext = [System.Convert]::FromBase64String($SealedBox)
-
- $privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
- if ($privateKeyByteArray.Length -ne 32) { throw 'Invalid private key.' }
-
- if ([string]::IsNullOrWhiteSpace($PublicKey)) {
- $publicKeyByteArray = Get-SodiumPublicKey -PrivateKey $PrivateKey -AsByteArray
- } else {
- $publicKeyByteArray = [System.Convert]::FromBase64String($PublicKey)
- if ($publicKeyByteArray.Length -ne 32) { throw 'Invalid public key.' }
+ try {
+ if (-not [string]::IsNullOrWhiteSpace($PublicKey)) {
+ return [PSModule.Sodium]::OpenSealBase64($SealedBox, $PrivateKey, $PublicKey)
+ }
+ return [PSModule.Sodium]::OpenSealBase64($SealedBox, $PrivateKey)
+ } catch [System.Management.Automation.MethodInvocationException] {
+ throw $_.Exception.InnerException
}
-
- $overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
- $decryptedBytes = New-Object byte[] ($ciphertext.Length - $overhead)
-
- $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)
}
}
diff --git a/src/functions/public/ConvertTo-SodiumSealedBox.ps1 b/src/functions/public/ConvertTo-SodiumSealedBox.ps1
index b76a089..ff87d42 100644
--- a/src/functions/public/ConvertTo-SodiumSealedBox.ps1
+++ b/src/functions/public/ConvertTo-SodiumSealedBox.ps1
@@ -52,36 +52,18 @@
# The base64-encoded public key used for encryption.
[Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
[string] $PublicKey
)
begin {
- if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
- $null = [PSModule.Sodium]::sodium_init()
+ Initialize-Sodium
}
process {
- # Convert public key from Base64 or space-separated string
try {
- $publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
- } catch {
- $PSCmdlet.ThrowTerminatingError($_)
+ return [PSModule.Sodium]::SealBase64($Message, $PublicKey)
+ } catch [System.Management.Automation.MethodInvocationException] {
+ throw $_.Exception.InnerException
}
- 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)
}
}
diff --git a/src/functions/public/Get-SodiumPublicKey.ps1 b/src/functions/public/Get-SodiumPublicKey.ps1
index fc75cef..8a7ab08 100644
--- a/src/functions/public/Get-SodiumPublicKey.ps1
+++ b/src/functions/public/Get-SodiumPublicKey.ps1
@@ -70,10 +70,10 @@
[OutputType([string], ParameterSetName = 'Base64')]
[OutputType([byte[]], ParameterSetName = 'AsByteArray')]
[CmdletBinding(DefaultParameterSetName = 'Base64')]
- [CmdletBinding()]
param(
# The private key to derive the public key from.
[Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
[string] $PrivateKey,
# Returns the byte array
@@ -82,22 +82,23 @@
)
begin {
- if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
- $null = [PSModule.Sodium]::sodium_init()
+ Initialize-Sodium
}
process {
- $publicKeyByteArray = New-Object byte[] 32
- $privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
- $rc = [PSModule.Sodium]::crypto_scalarmult_base($publicKeyByteArray, $privateKeyByteArray)
- if ($rc -ne 0) { throw 'Unable to derive public key from private key.' }
- }
-
- end {
if ($AsByteArray) {
- return $publicKeyByteArray
- } else {
- return [System.Convert]::ToBase64String($publicKeyByteArray)
+ try {
+ return [System.Convert]::FromBase64String([PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey))
+ } catch [System.Management.Automation.MethodInvocationException] {
+ throw $_.Exception.InnerException
+ }
+ }
+ try {
+ return [PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey)
+ } catch [System.Management.Automation.MethodInvocationException] {
+ throw $_.Exception.InnerException
}
}
+
+ end {}
}
diff --git a/src/functions/public/New-SodiumKeyPair.ps1 b/src/functions/public/New-SodiumKeyPair.ps1
index c023557..a8bebe5 100644
--- a/src/functions/public/New-SodiumKeyPair.ps1
+++ b/src/functions/public/New-SodiumKeyPair.ps1
@@ -80,40 +80,27 @@
ParameterSetName = 'SeededKeyPair',
ValueFromPipeline
)]
+ [AllowEmptyString()]
[string] $Seed
)
begin {
- if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
- $null = [PSModule.Sodium]::sodium_init()
+ 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
-
- 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
+ try {
+ if ($PSCmdlet.ParameterSetName -eq 'SeededKeyPair') {
+ $kp = [PSModule.Sodium]::GenerateKeyPairBase64($Seed)
+ } else {
+ $kp = [PSModule.Sodium]::GenerateKeyPairBase64()
}
- default {
- $result = [PSModule.Sodium]::crypto_box_keypair($publicKey, $privateKey)
- }
- }
-
- if ($result -ne 0) {
- throw 'Key pair generation failed.'
+ } catch [System.Management.Automation.MethodInvocationException] {
+ throw $_.Exception.InnerException
}
-
return [pscustomobject]@{
- PublicKey = [Convert]::ToBase64String($publicKey)
- PrivateKey = [Convert]::ToBase64String($privateKey)
+ PublicKey = $kp.PublicKey
+ PrivateKey = $kp.PrivateKey
}
}
}
diff --git a/src/main.ps1 b/src/main.ps1
index 69b1199..0d1d6f2 100644
--- a/src/main.ps1
+++ b/src/main.ps1
@@ -1,33 +1,43 @@
+$processArchitecture = [System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture
+
switch ($true) {
$IsLinux {
- $architecture = (uname -m)
- if ($architecture -eq 'aarch64') {
- Import-Module "$PSScriptRoot/libs/linux-arm64/PSModule.Sodium.dll"
- $script:Supported = $true
- } elseif ($architecture -eq 'x86_64') {
- Import-Module "$PSScriptRoot/libs/linux-x64/PSModule.Sodium.dll"
- $script:Supported = $true
- } else {
- throw "Unsupported Linux architecture: $architecture. Please refer to the documentation for supported architectures."
+ 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 {
- 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"
+ 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."
+ }
}
- $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"
+ 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."
+ }
}
- $script:Supported = Assert-VisualCRedistributableInstalled -Version '14.0'
}
default {
throw 'Unsupported platform. Please refer to the documentation for more information.'
}
}
+
+$assemblyPath = Join-Path -Path $PSScriptRoot -ChildPath "libs/$runtimeIdentifier/PSModule.Sodium.dll"
+Import-Module $assemblyPath -ErrorAction Stop
+
+# Optimistically mark supported; Initialize-Sodium runs during module import and checks Windows VC++ runtime only if native init fails.
+$script:Supported = $true
+$script:ProcessArchitecture = $processArchitecture.ToString()
+
+Initialize-Sodium
diff --git a/src/variables/private/Initialized.ps1 b/src/variables/private/Initialized.ps1
new file mode 100644
index 0000000..868f814
--- /dev/null
+++ b/src/variables/private/Initialized.ps1
@@ -0,0 +1,5 @@
+$script:SodiumInitialized = $false
+$script:SodiumPublicKeyBytes = $null
+$script:SodiumPrivateKeyBytes = $null
+$script:SodiumSealBytes = $null
+$script:SodiumSeedBytes = $null
diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1
index bad67a4..84f8fb1 100644
--- a/tests/Sodium.Tests.ps1
+++ b/tests/Sodium.Tests.ps1
@@ -39,6 +39,14 @@
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $invalidPublicKey -PrivateKey $keyPair.PrivateKey } | Should -Throw
}
+ It 'Throws a clear error when the sealed box is shorter than the Sodium overhead' {
+ $keyPair = New-SodiumKeyPair
+ $shortSealedBox = [Convert]::ToBase64String([byte[]]::new(16))
+
+ { ConvertFrom-SodiumSealedBox -SealedBox $shortSealedBox -PrivateKey $keyPair.PrivateKey } |
+ Should -Throw 'Invalid sealed box. Expected at least 48 bytes but got 16.'
+ }
+
It 'Encrypts a message correctly when using pipeline input on ConvertTo-SodiumSealedBox' {
$keyPair = New-SodiumKeyPair
$publicKey = $keyPair.PublicKey
@@ -132,6 +140,14 @@
$keyPair1.PublicKey | Should -Not -Be $keyPair2.PublicKey
$keyPair1.PrivateKey | Should -Not -Be $keyPair2.PrivateKey
}
+
+ It 'Allows an empty seed and remains deterministic for compatibility' {
+ $keyPair1 = New-SodiumKeyPair -Seed ''
+ $keyPair2 = New-SodiumKeyPair -Seed ''
+
+ $keyPair1.PublicKey | Should -Be $keyPair2.PublicKey
+ $keyPair1.PrivateKey | Should -Be $keyPair2.PrivateKey
+ }
}
Context 'Public Key Derivation' {
@@ -150,5 +166,22 @@
{ Get-SodiumPublicKey -PrivateKey $invalidPrivateKey } | Should -Throw
}
+
+ It 'Get-SodiumPublicKey - Throws a clear error when a private key has the wrong length' {
+ $shortPrivateKey = [Convert]::ToBase64String([byte[]]::new(16))
+
+ { Get-SodiumPublicKey -PrivateKey $shortPrivateKey } |
+ Should -Throw 'Invalid private key. Expected 32 bytes but got 16.'
+ }
+ }
+
+ Context 'Runtime diagnostics' {
+ It 'Assert-VisualCRedistributableInstalled validates the current Windows architecture runtime' -Skip:(-not $IsWindows) {
+ InModuleScope Sodium {
+ $arch = if ([System.Environment]::Is64BitProcess) { 'X64' } else { 'X86' }
+ $result = Assert-VisualCRedistributableInstalled -Version '14.0' -Architecture $arch 3>$null
+ $result | Should -BeTrue
+ }
+ }
}
}