Skip to content
Merged
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
12 changes: 8 additions & 4 deletions PSModule/Sodium/Sodium.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ public static class Sodium
public static extern int sodium_init();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_box_keypair(byte[] pk, byte[] sk);
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[] pk, byte[] sk, byte[] seed);
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[] pk);
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[] pk, byte[] sk);
public static extern int crypto_box_seal_open(byte[] decrypted, byte[] ciphertext, ulong clen, byte[] publicKey, byte[] privateKey);

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

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern UIntPtr crypto_box_sealbytes();

[DllImport("libsodium", CallingConvention = CallingConvention.Cdecl)]
public static extern int crypto_scalarmult_base(byte[] publicKey, byte[] privateKey);

}
}
18 changes: 11 additions & 7 deletions src/functions/public/ConvertFrom-SodiumSealedBox.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
[string] $SealedBox,

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

# The base64-encoded private key used for decryption.
Expand All @@ -72,19 +72,23 @@
}

process {
$ciphertext = [Convert]::FromBase64String($SealedBox)
$publicKeyByteArray = [Convert]::FromBase64String($PublicKey)
$privateKeyByteArray = [Convert]::FromBase64String($PrivateKey)
$ciphertext = [System.Convert]::FromBase64String($SealedBox)

if ($publicKeyByteArray.Length -ne 32) { throw 'Invalid public key.' }
$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.' }
}

$overhead = [PSModule.Sodium]::crypto_box_sealbytes().ToUInt32()
$decryptedBytes = New-Object byte[] ($ciphertext.Length - $overhead)

# Attempt to decrypt
$result = [PSModule.Sodium]::crypto_box_seal_open(
$decryptedBytes, $ciphertext, [uint64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
$decryptedBytes, $ciphertext, [UInt64]$ciphertext.Length, $publicKeyByteArray, $privateKeyByteArray
)

if ($result -ne 0) {
Expand Down
103 changes: 103 additions & 0 deletions src/functions/public/Get-SodiumPublicKey.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
function Get-SodiumPublicKey {
<#
.SYNOPSIS
Derives a Curve25519 public key from a provided private key using the Sodium cryptographic library.

.DESCRIPTION
Takes a base64-encoded Curve25519 private key and returns the corresponding base64-encoded public key. This is accomplished using the
Libsodium `crypto_scalarmult_base` function provided by the PSModule.Sodium .NET wrapper. The function ensures compatibility with
cryptographic operations requiring key exchange mechanisms.

.EXAMPLE
Get-SodiumPublicKey -PrivateKey 'ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU='

Output:
```powershell
WQakMx2mIAQMwLqiZteHUTwmMP6mUdK2FL0WEybWgB8=
```

Derives and returns the public key corresponding to the given base64-encoded private key.

.EXAMPLE
Get-SodiumPublicKey -PrivateKey 'ci5/7eZ0IbGXtqQMaNvxhJ2d9qwFxA8Kjx+vivSTXqU=' -AsByteArray

Output:
```powershell
89
6
164
51
29
166
32
4
12
192
186
162
102
215
135
81
60
38
48
254
166
81
210
182
20
189
22
19
38
214
128
31
```

.OUTPUTS
string

.OUTPUTS
byte[]

.LINK
https://psmodule.io/Sodium/Functions/Get-SodiumPublicKey/
#>

[OutputType([string], ParameterSetName = 'Base64')]
[OutputType([byte[]], ParameterSetName = 'AsByteArray')]
[CmdletBinding(DefaultParameterSetName = 'Base64')]
[CmdletBinding()]
param(
# The private key to derive the public key from.
[Parameter(Mandatory)]
[string] $PrivateKey,

# Returns the byte array
[Parameter(Mandatory, ParameterSetName = 'AsByteArray')]
[switch] $AsByteArray
)

begin {
if (-not $script:Supported) { throw 'Sodium is not supported on this platform.' }
$null = [PSModule.Sodium]::sodium_init()
}

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)
}
}
}
107 changes: 63 additions & 44 deletions tests/Sodium.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
Describe 'Sodium' {
Context 'SealedBox - Encryption and Decryption' {
It 'Encrypts and decrypts a message correctly using valid keys' {
# Generate a key pair
$keyPair = New-SodiumKeyPair
$publicKey = $keyPair.PublicKey
$privateKey = $keyPair.PrivateKey

# Define a message to test
$message = 'Hello world!'

# Encrypt the message
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey

# Decrypt using the matching private key
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $publicKey -PrivateKey $privateKey
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $publicKey -PrivateKey $privateKey

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

Expand All @@ -24,44 +17,26 @@
$keyPair2 = New-SodiumKeyPair
$message = 'Test message'

$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair1.PublicKey
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair1.PublicKey

{
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $keyPair1.PublicKey -PrivateKey $keyPair2.PrivateKey
} | Should -Throw 'Decryption failed.'
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $keyPair1.PublicKey -PrivateKey $keyPair2.PrivateKey } |
Should -Throw 'Decryption failed.'
}

It 'Throws an error when encrypting with an invalid public key' {
It 'ConvertTo-SodiumSealedBox -Throws an error when encrypting with an invalid public key' {
$message = 'Invalid key test'
$invalidPublicKey = 'InvalidKey' # not 32 bytes when converted
$invalidPublicKey = 'InvalidKey'

{
ConvertTo-SodiumSealedBox -Message $message -PublicKey $invalidPublicKey
} | Should -Throw
{ ConvertTo-SodiumSealedBox -Message $message -PublicKey $invalidPublicKey } | Should -Throw
}

It 'Throws an error when decrypting with an invalid public key' {
$keyPair = New-SodiumKeyPair
$message = 'Another message'
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey

# Supply a public key that's clearly too short
$invalidPublicKey = 'AAA'
{
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $invalidPublicKey -PrivateKey $keyPair.PrivateKey
} | Should -Throw
}

It 'Throws an error when decrypting with an invalid private key' {
$keyPair = New-SodiumKeyPair
$message = 'Yet another message'
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $keyPair.PublicKey

# Supply a private key that's clearly too short
$invalidPrivateKey = 'BBB'
{
ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $keyPair.PublicKey -PrivateKey $invalidPrivateKey
} | Should -Throw
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $invalidPublicKey -PrivateKey $keyPair.PrivateKey } | Should -Throw
}

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

# Pass the message via pipeline input instead of -Message parameter
$sealedBox = $message | ConvertTo-SodiumSealedBox -PublicKey $publicKey

$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $sealedBox -PublicKey $publicKey -PrivateKey $privateKey
$encryptedMessage = $message | ConvertTo-SodiumSealedBox -PublicKey $publicKey
$decryptedString = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PublicKey $publicKey -PrivateKey $privateKey

$decryptedString | Should -Be $message
}
Expand All @@ -84,16 +57,44 @@
$privateKey = $keyPair.PrivateKey
$message = 'Pipeline input decryption test'

# Encrypt using normal parameter binding
$sealedBox = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey

# Pass the sealed box via pipeline input to the decryption function
$decryptedString = $sealedBox | ConvertFrom-SodiumSealedBox -PublicKey $publicKey -PrivateKey $privateKey
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
$decryptedString = $encryptedMessage | ConvertFrom-SodiumSealedBox -PublicKey $publicKey -PrivateKey $privateKey

$decryptedString | Should -Be $message
}
}

Context 'SealedBox - Decryption without PublicKey' {

It 'Decrypts a sealed box when only the private key is supplied' {
$keyPair = New-SodiumKeyPair
$publicKey = $keyPair.PublicKey
$privateKey = $keyPair.PrivateKey

$message = 'Hello with secret key only!'
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $publicKey
$decrypted = ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PrivateKey $privateKey

$decrypted | Should -Be $message
}

It 'Fails when an incorrect private key is supplied (no public key given)' {
$kpGood = New-SodiumKeyPair
$kpBad = New-SodiumKeyPair
$message = 'Mismatch test'
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $kpGood.PublicKey
{ ConvertFrom-SodiumSealedBox -SealedBox $encryptedMessage -PrivateKey $kpBad.PrivateKey } | Should -Throw
}

It 'Accepts pipeline input for the sealed box when no public key is given' {
$kp = New-SodiumKeyPair
$message = 'Pipeline test'
$encryptedMessage = ConvertTo-SodiumSealedBox -Message $message -PublicKey $kp.PublicKey
$result = $encryptedMessage | ConvertFrom-SodiumSealedBox -PrivateKey $kp.PrivateKey
$result | Should -Be $message
}
}

Context 'Key Pair Generation' {
It 'Generates a valid key pair with keys of 32 bytes each' {
$keyPair = New-SodiumKeyPair
Expand Down Expand Up @@ -132,4 +133,22 @@
$keyPair1.PrivateKey | Should -Not -Be $keyPair2.PrivateKey
}
}

Context 'Public Key Derivation' {
It 'Get-SodiumPublicKey - Derives the correct public key from a private key' {
$keyPair = New-SodiumKeyPair
$privateKey = $keyPair.PrivateKey
$expectedPublicKey = $keyPair.PublicKey

$derivedPublicKey = Get-SodiumPublicKey -PrivateKey $privateKey

$derivedPublicKey | Should -Be $expectedPublicKey
}

It 'Get-SodiumPublicKey - Throws an error when an invalid private key is provided' {
$invalidPrivateKey = 'InvalidKey'

{ Get-SodiumPublicKey -PrivateKey $invalidPrivateKey } | Should -Throw
}
}
}
Loading