Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
153 changes: 135 additions & 18 deletions PSModule/Sodium/Sodium.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,149 @@ namespace PSModule
{
public static class Sodium
{
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int sodium_init();
private static class Native
{
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int sodium_init();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int crypto_box_seal(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);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong ciphertextLength, byte[] publicKey, byte[] privateKey);

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern UIntPtr crypto_box_publickeybytes();
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern UIntPtr crypto_box_publickeybytes();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern UIntPtr crypto_box_secretkeybytes();
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern UIntPtr crypto_box_secretkeybytes();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern UIntPtr crypto_box_sealbytes();
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern UIntPtr crypto_box_sealbytes();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
}

public static int sodium_init()
{
return Native.sodium_init();
}

public static int crypto_box_keypair(byte[] publicKey, byte[] privateKey)
{
ValidateMinimumBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
ValidateMinimumBufferLength(privateKey, GetRequiredLength(crypto_box_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, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
ValidateMinimumBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));
ValidateExactBufferLength(seed, GetRequiredLength(crypto_box_secretkeybytes()), 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 + crypto_box_sealbytes().ToUInt64()), nameof(ciphertext));
ValidateExactBufferLength(publicKey, GetRequiredLength(crypto_box_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 = crypto_box_sealbytes().ToUInt64();
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, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
ValidateExactBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));

return Native.crypto_box_seal_open(decrypted, ciphertext, ciphertextLength, publicKey, privateKey);
}

public static UIntPtr crypto_box_publickeybytes()
{
return Native.crypto_box_publickeybytes();
}

public static UIntPtr crypto_box_secretkeybytes()
{
return Native.crypto_box_secretkeybytes();
}

public static UIntPtr crypto_box_sealbytes()
{
return Native.crypto_box_sealbytes();
}

public static int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey)
{
ValidateMinimumBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
ValidateExactBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));

return Native.crypto_scalarmult_base(publicKey, privateKey);
}

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);
}
}
}
}
21 changes: 15 additions & 6 deletions PSModule/build.ps1
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
$ErrorActionPreference = 'Stop'

Remove-Item -Path "$PSScriptRoot/../src/libs" -Recurse -Force -ErrorAction SilentlyContinue

$targetRuntimes = @(
Expand All @@ -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)"
Expand Down
27 changes: 19 additions & 8 deletions src/functions/private/Assert-VisualCRedistributableInstalled.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
}
34 changes: 34 additions & 0 deletions src/functions/private/Initialize-Sodium.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 }

$initializationResult = [PSModule.Sodium]::sodium_init()
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:SodiumInitialized = $true
}

end {}
}
68 changes: 43 additions & 25 deletions src/functions/public/ConvertFrom-SodiumSealedBox.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
ValueFromPipelineByPropertyName
)]
[Alias('CipherText')]
[ValidateNotNullOrEmpty()]
[string] $SealedBox,

# The base64-encoded public key used for decryption.
Expand All @@ -63,38 +64,55 @@

# 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.' }
$privateKeyByteArray = $null
$decryptedBytes = $null
try {
$ciphertext = [System.Convert]::FromBase64String($SealedBox)
if ($ciphertext.Length -lt $script:SodiumSealBytes) {
throw "Invalid sealed box. Expected at least $script:SodiumSealBytes bytes but got $($ciphertext.Length)."
}

$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
if ($privateKeyByteArray.Length -ne $script:SodiumPrivateKeyBytes) {
throw "Invalid private key. Expected $script:SodiumPrivateKeyBytes bytes but got $($privateKeyByteArray.Length)."
}

if (-not $PublicKey) {
$publicKeyByteArray = [byte[]](Get-SodiumPublicKey -PrivateKey $PrivateKey -AsByteArray)
} else {
$publicKeyByteArray = [System.Convert]::FromBase64String($PublicKey)
if ($publicKeyByteArray.Length -ne $script:SodiumPublicKeyBytes) {
throw "Invalid public key. Expected $script:SodiumPublicKeyBytes bytes but got $($publicKeyByteArray.Length)."
}
}

$decryptedBytes = [byte[]]::new($ciphertext.Length - $script:SodiumSealBytes)

$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)
} finally {
if ($null -ne $privateKeyByteArray -and $privateKeyByteArray.Length -gt 0) {
[array]::Clear($privateKeyByteArray, 0, $privateKeyByteArray.Length)
}
if ($null -ne $decryptedBytes -and $decryptedBytes.Length -gt 0) {
[array]::Clear($decryptedBytes, 0, $decryptedBytes.Length)
}
}

$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)
}
}
Loading
Loading