diff --git a/PSModule/Sodium/Sodium.cs b/PSModule/Sodium/Sodium.cs index f4dfeeb..44b5f33 100644 --- a/PSModule/Sodium/Sodium.cs +++ b/PSModule/Sodium/Sodium.cs @@ -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(); @@ -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); + } } diff --git a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 index b216343..0fcaf62 100644 --- a/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 +++ b/src/functions/public/ConvertFrom-SodiumSealedBox.ps1 @@ -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. @@ -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) { diff --git a/src/functions/public/Get-SodiumPublicKey.ps1 b/src/functions/public/Get-SodiumPublicKey.ps1 new file mode 100644 index 0000000..fc75cef --- /dev/null +++ b/src/functions/public/Get-SodiumPublicKey.ps1 @@ -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) + } + } +} diff --git a/tests/Sodium.Tests.ps1 b/tests/Sodium.Tests.ps1 index 55b42e3..bad67a4 100644 --- a/tests/Sodium.Tests.ps1 +++ b/tests/Sodium.Tests.ps1 @@ -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 } @@ -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' { @@ -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 } @@ -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 @@ -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 + } + } }