Skip to content

Commit cbc8978

Browse files
Harden Sodium interop validation and cache runtime constants
1 parent a054711 commit cbc8978

11 files changed

Lines changed: 375 additions & 132 deletions

PSModule/Sodium/Sodium.cs

Lines changed: 135 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,149 @@ namespace PSModule
55
{
66
public static class Sodium
77
{
8-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
9-
public static extern int sodium_init();
8+
private static class Native
9+
{
10+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
11+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
12+
public static extern int sodium_init();
1013

11-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
12-
public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);
14+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
15+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
16+
public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);
1317

14-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
15-
public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);
18+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
19+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
20+
public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);
1621

17-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
18-
public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
22+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
23+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
24+
public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
1925

20-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
21-
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey);
26+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
27+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
28+
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey);
2229

23-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
24-
public static extern UIntPtr crypto_box_publickeybytes();
30+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
31+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
32+
public static extern UIntPtr crypto_box_publickeybytes();
2533

26-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
27-
public static extern UIntPtr crypto_box_secretkeybytes();
34+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
35+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
36+
public static extern UIntPtr crypto_box_secretkeybytes();
2837

29-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
30-
public static extern UIntPtr crypto_box_sealbytes();
38+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
39+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
40+
public static extern UIntPtr crypto_box_sealbytes();
3141

32-
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
33-
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
42+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
43+
[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.SafeDirectories)]
44+
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
45+
}
3446

47+
public static int sodium_init()
48+
{
49+
return Native.sodium_init();
50+
}
51+
52+
public static int crypto_box_keypair(byte[] publicKey, byte[] privateKey)
53+
{
54+
ValidateMinimumBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
55+
ValidateMinimumBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));
56+
57+
return Native.crypto_box_keypair(publicKey, privateKey);
58+
}
59+
60+
public static int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed)
61+
{
62+
ValidateMinimumBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
63+
ValidateMinimumBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));
64+
ValidateExactBufferLength(seed, GetRequiredLength(crypto_box_secretkeybytes()), nameof(seed));
65+
66+
return Native.crypto_box_seed_keypair(publicKey, privateKey, seed);
67+
}
68+
69+
public static int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey)
70+
{
71+
ValidateMinimumBufferLength(message, mlen, nameof(message));
72+
ValidateMinimumBufferLength(ciphertext, checked(mlen + crypto_box_sealbytes().ToUInt64()), nameof(ciphertext));
73+
ValidateExactBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
74+
75+
return Native.crypto_box_seal(ciphertext, message, mlen, publicKey);
76+
}
77+
78+
public static int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey)
79+
{
80+
var sealBytes = crypto_box_sealbytes().ToUInt64();
81+
if (clen < sealBytes)
82+
{
83+
throw new ArgumentException($"The ciphertext must be at least {sealBytes} bytes.", nameof(ciphertext));
84+
}
85+
86+
ValidateMinimumBufferLength(ciphertext, clen, nameof(ciphertext));
87+
ValidateMinimumBufferLength(decrypted, clen - sealBytes, nameof(decrypted));
88+
ValidateExactBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
89+
ValidateExactBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));
90+
91+
return Native.crypto_box_seal_open(decrypted, ciphertext, clen, publicKey, privateKey);
92+
}
93+
94+
public static UIntPtr crypto_box_publickeybytes()
95+
{
96+
return Native.crypto_box_publickeybytes();
97+
}
98+
99+
public static UIntPtr crypto_box_secretkeybytes()
100+
{
101+
return Native.crypto_box_secretkeybytes();
102+
}
103+
104+
public static UIntPtr crypto_box_sealbytes()
105+
{
106+
return Native.crypto_box_sealbytes();
107+
}
108+
109+
public static int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey)
110+
{
111+
ValidateMinimumBufferLength(publicKey, GetRequiredLength(crypto_box_publickeybytes()), nameof(publicKey));
112+
ValidateExactBufferLength(privateKey, GetRequiredLength(crypto_box_secretkeybytes()), nameof(privateKey));
113+
114+
return Native.crypto_scalarmult_base(publicKey, privateKey);
115+
}
116+
117+
private static int GetRequiredLength(UIntPtr length)
118+
{
119+
var value = length.ToUInt64();
120+
if (value > int.MaxValue)
121+
{
122+
throw new OverflowException("The Sodium buffer length exceeds the maximum supported array length.");
123+
}
124+
125+
return (int)value;
126+
}
127+
128+
private static void ValidateExactBufferLength(byte[] buffer, int expectedLength, string parameterName)
129+
{
130+
ArgumentNullException.ThrowIfNull(buffer, parameterName);
131+
132+
if (buffer.Length != expectedLength)
133+
{
134+
throw new ArgumentException($"The buffer must be exactly {expectedLength} bytes.", parameterName);
135+
}
136+
}
137+
138+
private static void ValidateMinimumBufferLength(byte[] buffer, int minimumLength, string parameterName)
139+
{
140+
ValidateMinimumBufferLength(buffer, (ulong)minimumLength, parameterName);
141+
}
142+
143+
private static void ValidateMinimumBufferLength(byte[] buffer, ulong minimumLength, string parameterName)
144+
{
145+
ArgumentNullException.ThrowIfNull(buffer, parameterName);
146+
147+
if ((ulong)buffer.LongLength < minimumLength)
148+
{
149+
throw new ArgumentException($"The buffer must be at least {minimumLength} bytes.", parameterName);
150+
}
151+
}
35152
}
36153
}

PSModule/build.ps1

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
$ErrorActionPreference = 'Stop'
2+
13
Remove-Item -Path "$PSScriptRoot/../src/libs" -Recurse -Force -ErrorAction SilentlyContinue
24

35
$targetRuntimes = @(
@@ -10,13 +12,20 @@ $targetRuntimes = @(
1012
)
1113

1214
Push-Location $PSScriptRoot
13-
$targetRuntimes | ForEach-Object {
14-
dotnet publish -r $_
15-
$source = "$PSScriptRoot/bin/Release/net8.0/$_/publish"
16-
$destination = "$PSScriptRoot/../src/libs/$_"
17-
Copy-Item -Path $source -Destination $destination -Recurse -Force
15+
try {
16+
$targetRuntimes | ForEach-Object {
17+
dotnet publish -c Release -r $_
18+
if ($LASTEXITCODE -ne 0) {
19+
throw "dotnet publish failed for runtime '$_'."
20+
}
21+
22+
$source = "$PSScriptRoot/bin/Release/net8.0/$_/publish"
23+
$destination = "$PSScriptRoot/../src/libs/$_"
24+
Copy-Item -Path $source -Destination $destination -Recurse -Force
25+
}
26+
} finally {
27+
Pop-Location
1828
}
19-
Pop-Location
2029

2130
Get-ChildItem -Path $PSScriptRoot -Directory -Recurse | Where-Object { $_.Name -in 'bin', 'obj' } | ForEach-Object {
2231
Write-Warning "Deleting $($_.FullName)"

src/functions/private/Assert-VisualCRedistributableInstalled.ps1

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,37 @@ function Assert-VisualCRedistributableInstalled {
2222
#>
2323
[CmdletBinding()]
2424
[OutputType([bool])]
25-
param (
25+
param(
2626
# The minimum required version of the Visual C++ Redistributable.
2727
[Parameter(Mandatory)]
28-
[Version] $Version
28+
[Version] $Version,
29+
30+
# The process architecture that determines which Redistributable runtime is required.
31+
[Parameter()]
32+
[ValidateSet('X64', 'X86')]
33+
[string] $Architecture = $(if ([System.Environment]::Is64BitProcess) { 'X64' } else { 'X86' })
2934
)
3035

31-
process {
36+
begin {
3237
$result = $false
38+
}
39+
40+
process {
3341
if ($IsWindows) {
34-
$key = 'HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64'
35-
if (Test-Path -Path $key) {
36-
$installedVersion = (Get-ItemProperty -Path $key).Version
37-
$result = [Version]($installedVersion.SubString(1, $installedVersion.Length - 1)) -ge $Version
42+
$key = "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\$Architecture"
43+
$runtimeInfo = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
44+
if ($runtimeInfo -and $runtimeInfo.Version -and ($runtimeInfo.Installed -ne 0)) {
45+
$installedVersion = [Version]$runtimeInfo.Version.TrimStart('v', 'V')
46+
$result = $installedVersion -ge $Version
3847
}
3948
}
4049
if (-not $result) {
41-
Write-Warning 'The Visual C++ Redistributable for Visual Studio 2015 or later is required.'
50+
Write-Warning "The Visual C++ Redistributable for Visual Studio 2015 or later ($Architecture) is required."
4251
Write-Warning 'Download and install the appropriate version from:'
4352
Write-Warning ' - https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads'
4453
}
4554
$result
4655
}
56+
57+
end {}
4758
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
function Initialize-Sodium {
2+
<#
3+
.SYNOPSIS
4+
Initializes Sodium for cryptographic operations.
5+
6+
.DESCRIPTION
7+
Initializes the native Sodium library once per module session and caches fixed buffer sizes used by the public commands.
8+
9+
.NOTES
10+
Requires the platform-specific PSModule.Sodium assembly and native libsodium runtime to be loaded.
11+
#>
12+
[OutputType([void])]
13+
[CmdletBinding()]
14+
param()
15+
16+
begin {}
17+
18+
process {
19+
if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
20+
if ($script:SodiumInitialized) { return }
21+
22+
$initializationResult = [PSModule.Sodium]::sodium_init()
23+
if ($initializationResult -lt 0) {
24+
throw 'Sodium initialization failed.'
25+
}
26+
27+
$script:SodiumPublicKeyBytes = [PSModule.Sodium]::crypto_box_publickeybytes().ToUInt32()
28+
$script:SodiumPrivateKeyBytes = [PSModule.Sodium]::crypto_box_secretkeybytes().ToUInt32()
29+
$script:SodiumSealBytes = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
30+
$script:SodiumInitialized = $true
31+
}
32+
33+
end {}
34+
}

src/functions/public/ConvertFrom-SodiumSealedBox.ps1

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
ValueFromPipelineByPropertyName
5656
)]
5757
[Alias('CipherText')]
58+
[ValidateNotNullOrEmpty()]
5859
[string] $SealedBox,
5960

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

6465
# The base64-encoded private key used for decryption.
6566
[Parameter(Mandatory)]
67+
[ValidateNotNullOrEmpty()]
6668
[string] $PrivateKey
6769
)
6870

6971
begin {
70-
if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
71-
$null = [PSModule.Sodium]::sodium_init()
72+
Initialize-Sodium
7273
}
7374

7475
process {
75-
$ciphertext = [System.Convert]::FromBase64String($SealedBox)
76-
77-
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
78-
if ($privateKeyByteArray.Length -ne 32) { throw 'Invalid private key.' }
79-
80-
if ([string]::IsNullOrWhiteSpace($PublicKey)) {
81-
$publicKeyByteArray = Get-SodiumPublicKey -PrivateKey $PrivateKey -AsByteArray
82-
} else {
83-
$publicKeyByteArray = [System.Convert]::FromBase64String($PublicKey)
84-
if ($publicKeyByteArray.Length -ne 32) { throw 'Invalid public key.' }
76+
$privateKeyByteArray = $null
77+
$decryptedBytes = $null
78+
try {
79+
$ciphertext = [System.Convert]::FromBase64String($SealedBox)
80+
if ($ciphertext.Length -lt $script:SodiumSealBytes) {
81+
throw "Invalid sealed box. Expected at least $script:SodiumSealBytes bytes but got $($ciphertext.Length)."
82+
}
83+
84+
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
85+
if ($privateKeyByteArray.Length -ne $script:SodiumPrivateKeyBytes) {
86+
throw "Invalid private key. Expected $script:SodiumPrivateKeyBytes bytes but got $($privateKeyByteArray.Length)."
87+
}
88+
89+
if (-not $PublicKey) {
90+
$publicKeyByteArray = [byte[]](Get-SodiumPublicKey -PrivateKey $PrivateKey -AsByteArray)
91+
} else {
92+
$publicKeyByteArray = [System.Convert]::FromBase64String($PublicKey)
93+
if ($publicKeyByteArray.Length -ne $script:SodiumPublicKeyBytes) {
94+
throw "Invalid public key. Expected $script:SodiumPublicKeyBytes bytes but got $($publicKeyByteArray.Length)."
95+
}
96+
}
97+
98+
$decryptedBytes = [byte[]]::new($ciphertext.Length - $script:SodiumSealBytes)
99+
100+
$result = [PSModule.Sodium]::crypto_box_seal_open(
101+
$decryptedBytes, $ciphertext, [UInt64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
102+
)
103+
104+
if ($result -ne 0) {
105+
throw 'Decryption failed.'
106+
}
107+
108+
return [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
109+
} finally {
110+
if ($null -ne $privateKeyByteArray -and $privateKeyByteArray.Length -gt 0) {
111+
[array]::Clear($privateKeyByteArray, 0, $privateKeyByteArray.Length)
112+
}
113+
if ($null -ne $decryptedBytes -and $decryptedBytes.Length -gt 0) {
114+
[array]::Clear($decryptedBytes, 0, $decryptedBytes.Length)
115+
}
85116
}
86-
87-
$overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
88-
$decryptedBytes = New-Object byte[] ($ciphertext.Length - $overhead)
89-
90-
$result = [PSModule.Sodium]::crypto_box_seal_open(
91-
$decryptedBytes, $ciphertext, [UInt64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
92-
)
93-
94-
if ($result -ne 0) {
95-
throw 'Decryption failed.'
96-
}
97-
98-
return [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
99117
}
100118
}

0 commit comments

Comments
 (0)