Skip to content

Commit ffe7f9c

Browse files
🚀 [Feature]: Add support to run ConvertFrom-SodiumSealedBox without a public key (#30)
## Description This pull request introduces a new function to derive public keys from private keys, making the public key parameter optional during decryption. ### Updates to cryptographic functionality: * Parameter name cleanup in `Sodium.cs`: * Updated method parameter names in the `Sodium` class to use descriptive names (`publicKey` and `privateKey`) for better readability and consistency. * New cryptographic function in `Sodium.cs`: * Added `crypto_scalarmult_base`, a function for deriving Curve25519 public keys from private keys. ### Enhancements to decryption usability * New `Get-SodiumPublicKey` function: * Introduced a function to derive a public key from a private key using the `crypto_scalarmult_base` method, supporting both base64 and byte array outputs. * Make public key optional in `ConvertFrom-SodiumSealedBox.ps1`: * Made the `PublicKey` parameter optional. If not provided, the public key is derived from the private key using the new `Get-SodiumPublicKey` function. ### Improvements to tests: * **Expanded test coverage in `Sodium.Tests.ps1`:** - Added tests for decryption without providing a public key, ensuring functionality when only the private key is supplied. - Introduced tests for the new `Get-SodiumPublicKey` function, including validation of derived public keys and error handling for invalid private keys. - Refactored existing tests to use consistent variable names (`$encryptedMessage`) and streamlined error handling assertions. ## Type of change <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] 📖 [Docs] - [ ] 🪲 [Fix] - [ ] 🩹 [Patch] - [ ] ⚠️ [Security fix] - [ ] 🚀 [Feature] - [ ] 🌟 [Breaking change] ## Checklist <!-- Use the check-boxes [x] on the options that are relevant. --> - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas
1 parent 9032f46 commit ffe7f9c

4 files changed

Lines changed: 185 additions & 55 deletions

File tree

PSModule/Sodium/Sodium.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ public static class Sodium
99
public static extern int sodium_init();
1010

1111
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
12-
public static extern int crypto_box_keypair(byte[] pk, byte[] sk);
12+
public static extern int crypto_box_keypair(byte[] publicKey, byte[] privateKey);
1313

1414
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
15-
public static extern int crypto_box_seed_keypair(byte[] pk, byte[] sk, byte[] seed);
15+
public static extern int crypto_box_seed_keypair(byte[] publicKey, byte[] privateKey, byte[] seed);
1616

1717
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
18-
public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] pk);
18+
public static extern int crypto_box_seal(byte[] ciphertext, byte[] message, ulong mlen, byte[] publicKey);
1919

2020
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
21-
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] pk, byte[] sk);
21+
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey);
2222

2323
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
2424
public static extern UIntPtr crypto_box_publickeybytes();
@@ -28,5 +28,9 @@ public static class Sodium
2828

2929
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
3030
public static extern UIntPtr crypto_box_sealbytes();
31+
32+
[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
33+
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);
34+
3135
}
3236
}

src/functions/public/ConvertFrom-SodiumSealedBox.ps1

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
[string] $SealedBox,
5959

6060
# The base64-encoded public key used for decryption.
61-
[Parameter(Mandatory)]
61+
[Parameter()]
6262
[string] $PublicKey,
6363

6464
# The base64-encoded private key used for decryption.
@@ -72,19 +72,23 @@
7272
}
7373

7474
process {
75-
$ciphertext = [Convert]::FromBase64String($SealedBox)
76-
$publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
77-
$privateKeyByteArray = [Convert]::FromBase64String($PrivateKey)
75+
$ciphertext = [System.Convert]::FromBase64String($SealedBox)
7876

79-
if ($publicKeyByteArray.Length -ne 32) { throw 'Invalid public key.' }
77+
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
8078
if ($privateKeyByteArray.Length -ne 32) { throw 'Invalid private key.' }
8179

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.' }
85+
}
86+
8287
$overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
8388
$decryptedBytes = New-Object byte[] ($ciphertext.Length - $overhead)
8489

85-
# Attempt to decrypt
8690
$result = [PSModule.Sodium]::crypto_box_seal_open(
87-
$decryptedBytes, $ciphertext, [uint64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
91+
$decryptedBytes, $ciphertext, [UInt64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
8892
)
8993

9094
if ($result -ne 0) {
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
function Get-SodiumPublicKey {
2+
<#
3+
.SYNOPSIS
4+
Derives a Curve25519 public key from a provided private key using the Sodium cryptographic library.
5+
6+
.DESCRIPTION
7+
Takes a base64-encoded Curve25519 private key and returns the corresponding base64-encoded public key. This is accomplished using the
8+
Libsodium `crypto_scalarmult_base` function provided by the PSModule.Sodium .NET wrapper. The function ensures compatibility with
9+
cryptographic operations requiring key exchange mechanisms.
10+
11+
.EXAMPLE
12+
Get-SodiumPublicKey -PrivateKey 'ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU='
13+
14+
Output:
15+
```powershell
16+
WQakMx2mIAQMwLqiZteHUTwmMP6mUdK2FL0WEybWgB8=
17+
```
18+
19+
Derives and returns the public key corresponding to the given base64-encoded private key.
20+
21+
.EXAMPLE
22+
Get-SodiumPublicKey -PrivateKey 'ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU=' -AsByteArray
23+
24+
Output:
25+
```powershell
26+
89
27+
6
28+
164
29+
51
30+
29
31+
166
32+
32
33+
4
34+
12
35+
192
36+
186
37+
162
38+
102
39+
215
40+
135
41+
81
42+
60
43+
38
44+
48
45+
254
46+
166
47+
81
48+
210
49+
182
50+
20
51+
189
52+
22
53+
19
54+
38
55+
214
56+
128
57+
31
58+
```
59+
60+
.OUTPUTS
61+
string
62+
63+
.OUTPUTS
64+
byte[]
65+
66+
.LINK
67+
https://psmodule.io/Sodium/Functions/Get-SodiumPublicKey/
68+
#>
69+
70+
[OutputType([string], ParameterSetName = 'Base64')]
71+
[OutputType([byte[]], ParameterSetName = 'AsByteArray')]
72+
[CmdletBinding(DefaultParameterSetName = 'Base64')]
73+
[CmdletBinding()]
74+
param(
75+
# The private key to derive the public key from.
76+
[Parameter(Mandatory)]
77+
[string] $PrivateKey,
78+
79+
# Returns the byte array
80+
[Parameter(Mandatory, ParameterSetName = 'AsByteArray')]
81+
[switch] $AsByteArray
82+
)
83+
84+
begin {
85+
if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
86+
$null = [PSModule.Sodium]::sodium_init()
87+
}
88+
89+
process {
90+
$publicKeyByteArray = New-Object byte[] 32
91+
$privateKeyByteArray = [System.Convert]::FromBase64String($PrivateKey)
92+
$rc = [PSModule.Sodium]::crypto_scalarmult_base($publicKeyByteArray, $privateKeyByteArray)
93+
if ($rc -ne 0) { throw 'Unable to derive public key from private key.' }
94+
}
95+
96+
end {
97+
if ($AsByteArray) {
98+
return $publicKeyByteArray
99+
} else {
100+
return [System.Convert]::ToBase64String($publicKeyByteArray)
101+
}
102+
}
103+
}

tests/Sodium.Tests.ps1

Lines changed: 63 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,14 @@
11
Describe 'Sodium' {
22
Context 'SealedBox - Encryption and Decryption' {
33
It 'Encrypts and decrypts a message correctly using valid keys' {
4-
# Generate a key pair
54
$keyPair = New-SodiumKeyPair
65
$publicKey = $keyPair.PublicKey
76
$privateKey = $keyPair.PrivateKey
8-
9-
# Define a message to test
107
$message = 'Hello world!'
118

12-
# Encrypt the message
13-
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
14-
15-
# Decrypt using the matching private key
16-
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $publicKey -PrivateKey $privateKey
9+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
10+
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $publicKey -PrivateKey $privateKey
1711

18-
# Verify that the decrypted string matches the original message
1912
$decryptedString | Should -Be $message
2013
}
2114

@@ -24,44 +17,26 @@
2417
$keyPair2 = New-SodiumKeyPair
2518
$message = 'Test message'
2619

27-
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair1.PublicKey
20+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair1.PublicKey
2821

29-
{
30-
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $keyPair1.PublicKey -PrivateKey $keyPair2.PrivateKey
31-
} | Should -Throw 'Decryption failed.'
22+
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $keyPair1.PublicKey -PrivateKey $keyPair2.PrivateKey } |
23+
Should -Throw 'Decryption failed.'
3224
}
3325

34-
It 'Throws an error when encrypting with an invalid public key' {
26+
It 'ConvertTo-SodiumSealedBox -Throws an error when encrypting with an invalid public key' {
3527
$message = 'Invalid key test'
36-
$invalidPublicKey = 'InvalidKey' # not 32 bytes when converted
28+
$invalidPublicKey = 'InvalidKey'
3729

38-
{
39-
ConvertTo-SodiumSealedBox -Message $message -PublicKey $invalidPublicKey
40-
} | Should -Throw
30+
{ ConvertTo-SodiumSealedBox -Message $message -PublicKey $invalidPublicKey } | Should -Throw
4131
}
4232

4333
It 'Throws an error when decrypting with an invalid public key' {
4434
$keyPair = New-SodiumKeyPair
4535
$message = 'Another message'
46-
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
36+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
4737

48-
# Supply a public key that's clearly too short
4938
$invalidPublicKey = 'AAA'
50-
{
51-
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $invalidPublicKey -PrivateKey $keyPair.PrivateKey
52-
} | Should -Throw
53-
}
54-
55-
It 'Throws an error when decrypting with an invalid private key' {
56-
$keyPair = New-SodiumKeyPair
57-
$message = 'Yet another message'
58-
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
59-
60-
# Supply a private key that's clearly too short
61-
$invalidPrivateKey = 'BBB'
62-
{
63-
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $keyPair.PublicKey -PrivateKey $invalidPrivateKey
64-
} | Should -Throw
39+
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $invalidPublicKey -PrivateKey $keyPair.PrivateKey } | Should -Throw
6540
}
6641

6742
It 'Encrypts a message correctly when using pipeline input on ConvertTo-SodiumSealedBox' {
@@ -70,10 +45,8 @@
7045
$privateKey = $keyPair.PrivateKey
7146
$message = 'Pipeline input encryption test'
7247

73-
# Pass the message via pipeline input instead of -Message parameter
74-
$sealedBox = $message | ConvertTo-SodiumSealedBox -PublicKey $publicKey
75-
76-
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $publicKey -PrivateKey $privateKey
48+
$encryptedMessage = $message | ConvertTo-SodiumSealedBox -PublicKey $publicKey
49+
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $publicKey -PrivateKey $privateKey
7750

7851
$decryptedString | Should -Be $message
7952
}
@@ -84,16 +57,44 @@
8457
$privateKey = $keyPair.PrivateKey
8558
$message = 'Pipeline input decryption test'
8659

87-
# Encrypt using normal parameter binding
88-
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
89-
90-
# Pass the sealed box via pipeline input to the decryption function
91-
$decryptedString = $sealedBox | ConvertFrom-SodiumSealedBox -PublicKey $publicKey -PrivateKey $privateKey
60+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
61+
$decryptedString = $encryptedMessage | ConvertFrom-SodiumSealedBox -PublicKey $publicKey -PrivateKey $privateKey
9262

9363
$decryptedString | Should -Be $message
9464
}
9565
}
9666

67+
Context 'SealedBox - Decryption without PublicKey' {
68+
69+
It 'Decrypts a sealed box when only the private key is supplied' {
70+
$keyPair = New-SodiumKeyPair
71+
$publicKey = $keyPair.PublicKey
72+
$privateKey = $keyPair.PrivateKey
73+
74+
$message = 'Hello with secret key only!'
75+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
76+
$decrypted = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PrivateKey $privateKey
77+
78+
$decrypted | Should -Be $message
79+
}
80+
81+
It 'Fails when an incorrect private key is supplied (no public key given)' {
82+
$kpGood = New-SodiumKeyPair
83+
$kpBad = New-SodiumKeyPair
84+
$message = 'Mismatch test'
85+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $kpGood.PublicKey
86+
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PrivateKey $kpBad.PrivateKey } | Should -Throw
87+
}
88+
89+
It 'Accepts pipeline input for the sealed box when no public key is given' {
90+
$kp = New-SodiumKeyPair
91+
$message = 'Pipeline test'
92+
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $kp.PublicKey
93+
$result = $encryptedMessage | ConvertFrom-SodiumSealedBox -PrivateKey $kp.PrivateKey
94+
$result | Should -Be $message
95+
}
96+
}
97+
9798
Context 'Key Pair Generation' {
9899
It 'Generates a valid key pair with keys of 32 bytes each' {
99100
$keyPair = New-SodiumKeyPair
@@ -132,4 +133,22 @@
132133
$keyPair1.PrivateKey | Should -Not -Be $keyPair2.PrivateKey
133134
}
134135
}
136+
137+
Context 'Public Key Derivation' {
138+
It 'Get-SodiumPublicKey - Derives the correct public key from a private key' {
139+
$keyPair = New-SodiumKeyPair
140+
$privateKey = $keyPair.PrivateKey
141+
$expectedPublicKey = $keyPair.PublicKey
142+
143+
$derivedPublicKey = Get-SodiumPublicKey -PrivateKey $privateKey
144+
145+
$derivedPublicKey | Should -Be $expectedPublicKey
146+
}
147+
148+
It 'Get-SodiumPublicKey - Throws an error when an invalid private key is provided' {
149+
$invalidPrivateKey = 'InvalidKey'
150+
151+
{ Get-SodiumPublicKey -PrivateKey $invalidPrivateKey } | Should -Throw
152+
}
153+
}
135154
}

0 commit comments

Comments
 (0)