Skip to content

Commit 25ac3a4

Browse files
perf(#52): delegate cmdlets to base64-centric C# APIs
Adds high-level SealBase64 / OpenSealBase64 / GenerateKeyPairBase64 / DerivePublicKeyBase64 entrypoints to PSModule.Sodium that perform base64/UTF-8 encoding and the native libsodium call in a single managed transition. The PowerShell cmdlets become thin wrappers, removing 4-6 .NET method invocations from the hot path per call. Refs #52.
1 parent 9963c23 commit 25ac3a4

5 files changed

Lines changed: 185 additions & 121 deletions

File tree

PSModule/Sodium/Sodium.cs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Runtime.InteropServices;
3+
using System.Security.Cryptography;
4+
using System.Text;
35

46
namespace PSModule
57
{
@@ -139,6 +141,161 @@ public static int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey)
139141
return Native.crypto_scalarmult_base(publicKey, privateKey);
140142
}
141143

144+
// ---------- Base64-centric high-level API (see issue #52) ----------
145+
// These helpers do base64/UTF-8 encoding and native interop in a single managed call,
146+
// avoiding the overhead of multiple PowerShell-level method invocations on the hot path.
147+
148+
public sealed class KeyPairBase64
149+
{
150+
public string PublicKey { get; }
151+
public string PrivateKey { get; }
152+
153+
internal KeyPairBase64(string publicKey, string privateKey)
154+
{
155+
PublicKey = publicKey;
156+
PrivateKey = privateKey;
157+
}
158+
}
159+
160+
public static KeyPairBase64 GenerateKeyPairBase64()
161+
{
162+
var publicKey = new byte[PublicKeyBytes];
163+
var privateKey = new byte[SecretKeyBytes];
164+
try
165+
{
166+
if (Native.crypto_box_keypair(publicKey, privateKey) != 0)
167+
{
168+
throw new InvalidOperationException("Key pair generation failed.");
169+
}
170+
return new KeyPairBase64(Convert.ToBase64String(publicKey), Convert.ToBase64String(privateKey));
171+
}
172+
finally
173+
{
174+
CryptographicOperations.ZeroMemory(privateKey);
175+
}
176+
}
177+
178+
public static KeyPairBase64 GenerateKeyPairBase64(string seedText)
179+
{
180+
ArgumentNullException.ThrowIfNull(seedText);
181+
var publicKey = new byte[PublicKeyBytes];
182+
var privateKey = new byte[SecretKeyBytes];
183+
var seedSource = Encoding.UTF8.GetBytes(seedText);
184+
var seed = new byte[SeedBytes];
185+
try
186+
{
187+
if (!SHA256.TryHashData(seedSource, seed, out var written) || written != SeedBytes)
188+
{
189+
throw new InvalidOperationException("Failed to derive seed bytes from input.");
190+
}
191+
if (Native.crypto_box_seed_keypair(publicKey, privateKey, seed) != 0)
192+
{
193+
throw new InvalidOperationException("Seeded key pair generation failed.");
194+
}
195+
return new KeyPairBase64(Convert.ToBase64String(publicKey), Convert.ToBase64String(privateKey));
196+
}
197+
finally
198+
{
199+
CryptographicOperations.ZeroMemory(privateKey);
200+
CryptographicOperations.ZeroMemory(seed);
201+
CryptographicOperations.ZeroMemory(seedSource);
202+
}
203+
}
204+
205+
public static string DerivePublicKeyBase64(string privateKeyBase64)
206+
{
207+
ArgumentNullException.ThrowIfNull(privateKeyBase64);
208+
var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, nameof(privateKeyBase64));
209+
var publicKey = new byte[PublicKeyBytes];
210+
try
211+
{
212+
if (Native.crypto_scalarmult_base(publicKey, privateKey) != 0)
213+
{
214+
throw new InvalidOperationException("Unable to derive public key from private key.");
215+
}
216+
return Convert.ToBase64String(publicKey);
217+
}
218+
finally
219+
{
220+
CryptographicOperations.ZeroMemory(privateKey);
221+
}
222+
}
223+
224+
public static string SealBase64(string plaintext, string publicKeyBase64)
225+
{
226+
ArgumentNullException.ThrowIfNull(plaintext);
227+
ArgumentNullException.ThrowIfNull(publicKeyBase64);
228+
var publicKey = DecodeBase64Exact(publicKeyBase64, PublicKeyBytes, nameof(publicKeyBase64));
229+
var message = Encoding.UTF8.GetBytes(plaintext);
230+
var ciphertext = new byte[message.Length + SealBytes];
231+
if (Native.crypto_box_seal(ciphertext, message, (ulong)message.LongLength, publicKey) != 0)
232+
{
233+
throw new InvalidOperationException("Encryption failed.");
234+
}
235+
return Convert.ToBase64String(ciphertext);
236+
}
237+
238+
public static string OpenSealBase64(string ciphertextBase64, string privateKeyBase64)
239+
{
240+
return OpenSealBase64Core(ciphertextBase64, privateKeyBase64, publicKeyBase64: null);
241+
}
242+
243+
public static string OpenSealBase64(string ciphertextBase64, string privateKeyBase64, string publicKeyBase64)
244+
{
245+
return OpenSealBase64Core(ciphertextBase64, privateKeyBase64, publicKeyBase64);
246+
}
247+
248+
private static string OpenSealBase64Core(string ciphertextBase64, string privateKeyBase64, string publicKeyBase64)
249+
{
250+
ArgumentNullException.ThrowIfNull(ciphertextBase64);
251+
ArgumentNullException.ThrowIfNull(privateKeyBase64);
252+
253+
var ciphertext = Convert.FromBase64String(ciphertextBase64);
254+
if (ciphertext.Length < SealBytes)
255+
{
256+
throw new ArgumentException($"Invalid sealed box. Expected at least {SealBytes} bytes but got {ciphertext.Length}.", nameof(ciphertextBase64));
257+
}
258+
var privateKey = DecodeBase64Exact(privateKeyBase64, SecretKeyBytes, nameof(privateKeyBase64));
259+
var publicKey = new byte[PublicKeyBytes];
260+
var decrypted = new byte[ciphertext.Length - SealBytes];
261+
try
262+
{
263+
if (string.IsNullOrEmpty(publicKeyBase64))
264+
{
265+
if (Native.crypto_scalarmult_base(publicKey, privateKey) != 0)
266+
{
267+
throw new InvalidOperationException("Unable to derive public key from private key.");
268+
}
269+
}
270+
else
271+
{
272+
var providedPk = DecodeBase64Exact(publicKeyBase64, PublicKeyBytes, nameof(publicKeyBase64));
273+
Buffer.BlockCopy(providedPk, 0, publicKey, 0, PublicKeyBytes);
274+
}
275+
276+
if (Native.crypto_box_seal_open(decrypted, ciphertext, (ulong)ciphertext.LongLength, publicKey, privateKey) != 0)
277+
{
278+
throw new InvalidOperationException("Decryption failed.");
279+
}
280+
return Encoding.UTF8.GetString(decrypted);
281+
}
282+
finally
283+
{
284+
CryptographicOperations.ZeroMemory(privateKey);
285+
CryptographicOperations.ZeroMemory(decrypted);
286+
}
287+
}
288+
289+
private static byte[] DecodeBase64Exact(string value, int expectedLength, string parameterName)
290+
{
291+
var bytes = Convert.FromBase64String(value);
292+
if (bytes.Length != expectedLength)
293+
{
294+
throw new ArgumentException($"Invalid base64 value. Expected {expectedLength} bytes but got {bytes.Length}.", parameterName);
295+
}
296+
return bytes;
297+
}
298+
142299
private static int GetRequiredLength(UIntPtr length)
143300
{
144301
var value = length.ToUInt64();

src/functions/public/ConvertFrom-SodiumSealedBox.ps1

Lines changed: 3 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -71,50 +71,9 @@
7171
begin {}
7272

7373
process {
74-
$privateKeyByteArray = $null
75-
$decryptedBytes = $null
76-
try {
77-
$ciphertext = [System.Convert]::FromBase64String($SealedBox)
78-
if ($ciphertext.Length -lt $script:SodiumSealBytes) {
79-
throw "Invalid sealed box. Expected at least $script:SodiumSealBytes bytes but got $($ciphertext.Length)."
80-
}
81-
82-
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
83-
if ($privateKeyByteArray.Length -ne $script:SodiumPrivateKeyBytes) {
84-
throw "Invalid private key. Expected $script:SodiumPrivateKeyBytes bytes but got $($privateKeyByteArray.Length)."
85-
}
86-
87-
if (-not $PublicKey) {
88-
$publicKeyByteArray = [byte[]]::new($script:SodiumPublicKeyBytes)
89-
$deriveResult = [PSModule.Sodium]::crypto_scalarmult_base($publicKeyByteArray, $privateKeyByteArray)
90-
if ($deriveResult -ne 0) {
91-
throw 'Unable to derive public key from private key.'
92-
}
93-
} else {
94-
$publicKeyByteArray = [System.Convert]::FromBase64String($PublicKey)
95-
if ($publicKeyByteArray.Length -ne $script:SodiumPublicKeyBytes) {
96-
throw "Invalid public key. Expected $script:SodiumPublicKeyBytes bytes but got $($publicKeyByteArray.Length)."
97-
}
98-
}
99-
100-
$decryptedBytes = [byte[]]::new($ciphertext.Length - $script:SodiumSealBytes)
101-
102-
$result = [PSModule.Sodium]::crypto_box_seal_open(
103-
$decryptedBytes, $ciphertext, [UInt64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
104-
)
105-
106-
if ($result -ne 0) {
107-
throw 'Decryption failed.'
108-
}
109-
110-
return [System.Text.Encoding]::UTF8.GetString($decryptedBytes)
111-
} finally {
112-
if ($null -ne $privateKeyByteArray -and $privateKeyByteArray.Length -gt 0) {
113-
[array]::Clear($privateKeyByteArray, 0, $privateKeyByteArray.Length)
114-
}
115-
if ($null -ne $decryptedBytes -and $decryptedBytes.Length -gt 0) {
116-
[array]::Clear($decryptedBytes, 0, $decryptedBytes.Length)
117-
}
74+
if ($PublicKey) {
75+
return [PSModule.Sodium]::OpenSealBase64($SealedBox, $PrivateKey, $PublicKey)
11876
}
77+
return [PSModule.Sodium]::OpenSealBase64($SealedBox, $PrivateKey)
11978
}
12079
}

src/functions/public/ConvertTo-SodiumSealedBox.ps1

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,28 +58,6 @@
5858
begin {}
5959

6060
process {
61-
$messageBytes = $null
62-
try {
63-
$publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
64-
if ($publicKeyByteArray.Length -ne $script:SodiumPublicKeyBytes) {
65-
throw "Invalid public key. Expected $script:SodiumPublicKeyBytes bytes but got $($publicKeyByteArray.Length)."
66-
}
67-
68-
$messageBytes = [System.Text.Encoding]::UTF8.GetBytes($Message)
69-
$cipherLength = $messageBytes.Length + $script:SodiumSealBytes
70-
$ciphertext = [byte[]]::new($cipherLength)
71-
72-
$result = [PSModule.Sodium]::crypto_box_seal($ciphertext, $messageBytes, [uint64]$messageBytes.Length, $publicKeyByteArray)
73-
74-
if ($result -ne 0) {
75-
throw 'Encryption failed.'
76-
}
77-
78-
return [Convert]::ToBase64String($ciphertext)
79-
} finally {
80-
if ($null -ne $messageBytes -and $messageBytes.Length -gt 0) {
81-
[array]::Clear($messageBytes, 0, $messageBytes.Length)
82-
}
83-
}
61+
return [PSModule.Sodium]::SealBase64($Message, $PublicKey)
8462
}
8563
}

src/functions/public/Get-SodiumPublicKey.ps1

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -84,27 +84,24 @@
8484
begin {}
8585

8686
process {
87-
$privateKeyByteArray = $null
88-
try {
89-
$publicKeyByteArray = [byte[]]::new($script:SodiumPublicKeyBytes)
90-
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
91-
if ($privateKeyByteArray.Length -ne $script:SodiumPrivateKeyBytes) {
92-
throw "Invalid private key. Expected $script:SodiumPrivateKeyBytes bytes but got $($privateKeyByteArray.Length)."
93-
}
94-
95-
$deriveResult = [PSModule.Sodium]::crypto_scalarmult_base($publicKeyByteArray, $privateKeyByteArray)
96-
if ($deriveResult -ne 0) { throw 'Unable to derive public key from private key.' }
97-
98-
if ($AsByteArray) {
99-
return $publicKeyByteArray
100-
} else {
101-
return [System.Convert]::ToBase64String($publicKeyByteArray)
102-
}
103-
} finally {
104-
if ($null -ne $privateKeyByteArray -and $privateKeyByteArray.Length -gt 0) {
105-
[array]::Clear($privateKeyByteArray, 0, $privateKeyByteArray.Length)
87+
if ($AsByteArray) {
88+
$privateKeyByteArray = $null
89+
try {
90+
$publicKeyByteArray = [byte[]]::new($script:SodiumPublicKeyBytes)
91+
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
92+
if ($privateKeyByteArray.Length -ne $script:SodiumPrivateKeyBytes) {
93+
throw "Invalid private key. Expected $script:SodiumPrivateKeyBytes bytes but got $($privateKeyByteArray.Length)."
94+
}
95+
$deriveResult = [PSModule.Sodium]::crypto_scalarmult_base($publicKeyByteArray, $privateKeyByteArray)
96+
if ($deriveResult -ne 0) { throw 'Unable to derive public key from private key.' }
97+
return , $publicKeyByteArray
98+
} finally {
99+
if ($null -ne $privateKeyByteArray -and $privateKeyByteArray.Length -gt 0) {
100+
[array]::Clear($privateKeyByteArray, 0, $privateKeyByteArray.Length)
101+
}
106102
}
107103
}
104+
return [PSModule.Sodium]::DerivePublicKeyBase64($PrivateKey)
108105
}
109106

110107
end {}

src/functions/public/New-SodiumKeyPair.ps1

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,41 +87,14 @@
8787
begin {}
8888

8989
process {
90-
$publicKey = [byte[]]::new($script:SodiumPublicKeyBytes)
91-
$privateKey = [byte[]]::new($script:SodiumPrivateKeyBytes)
92-
$seedBytes = $null
93-
$derivedSeed = $null
94-
try {
95-
switch ($PSCmdlet.ParameterSetName) {
96-
'SeededKeyPair' {
97-
$seedBytes = [System.Text.Encoding]::UTF8.GetBytes($Seed)
98-
$derivedSeed = [System.Security.Cryptography.SHA256]::HashData($seedBytes)
99-
$result = [PSModule.Sodium]::crypto_box_seed_keypair($publicKey, $privateKey, $derivedSeed)
100-
break
101-
}
102-
default {
103-
$result = [PSModule.Sodium]::crypto_box_keypair($publicKey, $privateKey)
104-
}
105-
}
106-
107-
if ($result -ne 0) {
108-
throw 'Key pair generation failed.'
109-
}
110-
111-
return [pscustomobject]@{
112-
PublicKey = [Convert]::ToBase64String($publicKey)
113-
PrivateKey = [Convert]::ToBase64String($privateKey)
114-
}
115-
} finally {
116-
if ($null -ne $privateKey -and $privateKey.Length -gt 0) {
117-
[array]::Clear($privateKey, 0, $privateKey.Length)
118-
}
119-
if ($null -ne $seedBytes -and $seedBytes.Length -gt 0) {
120-
[array]::Clear($seedBytes, 0, $seedBytes.Length)
121-
}
122-
if ($null -ne $derivedSeed -and $derivedSeed.Length -gt 0) {
123-
[array]::Clear($derivedSeed, 0, $derivedSeed.Length)
124-
}
90+
if ($PSCmdlet.ParameterSetName -eq 'SeededKeyPair') {
91+
$kp = [PSModule.Sodium]::GenerateKeyPairBase64($Seed)
92+
} else {
93+
$kp = [PSModule.Sodium]::GenerateKeyPairBase64()
94+
}
95+
return [pscustomobject]@{
96+
PublicKey = $kp.PublicKey
97+
PrivateKey = $kp.PrivateKey
12598
}
12699
}
127100
}

0 commit comments

Comments
 (0)