Skip to content
Draft
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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module github.com/jetstack/preflight
go 1.24.4

require (
filippo.io/hpke v0.4.0
github.com/Venafi/vcert/v5 v5.12.2
github.com/cenkalti/backoff/v5 v5.0.3
github.com/fatih/color v1.18.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
github.com/Venafi/vcert/v5 v5.12.2 h1:Ee3/A9fZRiisuwuz22/Nqgl19H0ztQjWv35AC63qPcA=
Expand Down
130 changes: 130 additions & 0 deletions internal/hpke/decryptor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package hpke

import (
"fmt"

"filippo.io/hpke"
)

// NewDecryptor creates a new Decryptor with the provided HPKE private key.
// It uses the default algorithms: X25519 KEM, HKDF-SHA256, and AES-256-GCM.
func NewDecryptor(privateKey hpke.PrivateKey) (*Decryptor, error) {
if privateKey == nil {
return nil, fmt.Errorf("HPKE private key cannot be nil")
}

return &Decryptor{
privateKey: privateKey,
kdf: DefaultKDF(),
aead: DefaultAEAD(),
}, nil
}

// NewDecryptorWithAlgorithms creates a new Decryptor with custom algorithms.
// The KDF and AEAD must match those used during encryption.
// Note: The KEM is determined by the private key itself.
func NewDecryptorWithAlgorithms(privateKey hpke.PrivateKey, kdf hpke.KDF, aead hpke.AEAD) (*Decryptor, error) {
if privateKey == nil {
return nil, fmt.Errorf("HPKE private key cannot be nil")
}

if kdf == nil {
return nil, fmt.Errorf("KDF cannot be nil")
}

if aead == nil {
return nil, fmt.Errorf("AEAD cannot be nil")
}

return &Decryptor{
privateKey: privateKey,
kdf: kdf,
aead: aead,
}, nil
}

// Decrypt decrypts the provided EncryptedData using HPKE.
func (d *Decryptor) Decrypt(encrypted *EncryptedData) ([]byte, error) {
if encrypted == nil {
return nil, fmt.Errorf("encrypted data cannot be nil")
}

if len(encrypted.EncapsulatedKey) == 0 {
return nil, fmt.Errorf("encapsulated key cannot be empty")
}

if len(encrypted.Ciphertext) == 0 {
return nil, fmt.Errorf("ciphertext cannot be empty")
}

// NewRecipient creates a receiver context from the encapsulated key.
// The info parameter must match what was used during encryption (nil in our case).
recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
}

// Open decrypts and authenticates the ciphertext.
plaintext, err := recipient.Open(nil, encrypted.Ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
}

return plaintext, nil
}

// DecryptWithInfo decrypts data that was encrypted with application-specific context information.
// The info parameter must match what was used during encryption.
func (d *Decryptor) DecryptWithInfo(encrypted *EncryptedData, info []byte) ([]byte, error) {
if encrypted == nil {
return nil, fmt.Errorf("encrypted data cannot be nil")
}

if len(encrypted.EncapsulatedKey) == 0 {
return nil, fmt.Errorf("encapsulated key cannot be empty")
}

if len(encrypted.Ciphertext) == 0 {
return nil, fmt.Errorf("ciphertext cannot be empty")
}

recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, info)
if err != nil {
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
}

plaintext, err := recipient.Open(nil, encrypted.Ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
}

return plaintext, nil
}

// DecryptWithAAD decrypts data that was encrypted with additional authenticated data.
// The aad parameter must match what was used during encryption.
func (d *Decryptor) DecryptWithAAD(encrypted *EncryptedData, aad []byte) ([]byte, error) {
if encrypted == nil {
return nil, fmt.Errorf("encrypted data cannot be nil")
}

if len(encrypted.EncapsulatedKey) == 0 {
return nil, fmt.Errorf("encapsulated key cannot be empty")
}

if len(encrypted.Ciphertext) == 0 {
return nil, fmt.Errorf("ciphertext cannot be empty")
}

recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, nil)
if err != nil {
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
}

plaintext, err := recipient.Open(aad, encrypted.Ciphertext)
if err != nil {
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
}

return plaintext, nil
}
144 changes: 144 additions & 0 deletions internal/hpke/decryptor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package hpke_test

import (
"testing"

"github.com/jetstack/preflight/internal/hpke"
"github.com/stretchr/testify/require"
)

func TestNewDecryptor_NilKey(t *testing.T) {
dec, err := hpke.NewDecryptor(nil)
require.Error(t, err)
require.Nil(t, dec)
require.Contains(t, err.Error(), "cannot be nil")
}

func TestEncryptDecrypt_RoundTrip(t *testing.T) {
publicKey, privateKey, err := hpke.GenerateKeyPair()
require.NoError(t, err)

enc, err := hpke.NewEncryptor(publicKey)
require.NoError(t, err)

dec, err := hpke.NewDecryptor(privateKey)
require.NoError(t, err)

originalData := []byte("sensitive data to encrypt and decrypt")

// Encrypt
encrypted, err := enc.Encrypt(originalData)
require.NoError(t, err)
require.NotNil(t, encrypted)

// Decrypt
decrypted, err := dec.Decrypt(encrypted)
require.NoError(t, err)
require.Equal(t, originalData, decrypted)
}

func TestEncryptDecrypt_WithInfo(t *testing.T) {
publicKey, privateKey, err := hpke.GenerateKeyPair()
require.NoError(t, err)

enc, err := hpke.NewEncryptor(publicKey)
require.NoError(t, err)

dec, err := hpke.NewDecryptor(privateKey)
require.NoError(t, err)

originalData := []byte("sensitive data")
info := []byte("application-specific context")

// Encrypt with info
encrypted, err := enc.EncryptWithInfo(originalData, info)
require.NoError(t, err)

// Decrypt with matching info
decrypted, err := dec.DecryptWithInfo(encrypted, info)
require.NoError(t, err)
require.Equal(t, originalData, decrypted)

// Decrypt with wrong info should fail
_, err = dec.DecryptWithInfo(encrypted, []byte("wrong info"))
require.Error(t, err)

// Decrypt without info should fail
_, err = dec.Decrypt(encrypted)
require.Error(t, err)
}

func TestEncryptDecrypt_WithAAD(t *testing.T) {
publicKey, privateKey, err := hpke.GenerateKeyPair()
require.NoError(t, err)

enc, err := hpke.NewEncryptor(publicKey)
require.NoError(t, err)

dec, err := hpke.NewDecryptor(privateKey)
require.NoError(t, err)

originalData := []byte("sensitive data")
aad := []byte("additional authenticated data")

// Encrypt with AAD
encrypted, err := enc.EncryptWithAAD(originalData, aad)
require.NoError(t, err)

// Decrypt with matching AAD
decrypted, err := dec.DecryptWithAAD(encrypted, aad)
require.NoError(t, err)
require.Equal(t, originalData, decrypted)

// Decrypt with wrong AAD should fail
_, err = dec.DecryptWithAAD(encrypted, []byte("wrong aad"))
require.Error(t, err)

// Decrypt without AAD should fail
_, err = dec.Decrypt(encrypted)
require.Error(t, err)
}

func TestDecrypt_WrongKey(t *testing.T) {
publicKey1, _, err := hpke.GenerateKeyPair()
require.NoError(t, err)

_, privateKey2, err := hpke.GenerateKeyPair()
require.NoError(t, err)

enc, err := hpke.NewEncryptor(publicKey1)
require.NoError(t, err)

dec, err := hpke.NewDecryptor(privateKey2)
require.NoError(t, err)

data := []byte("test data")
encrypted, err := enc.Encrypt(data)
require.NoError(t, err)

// Decryption with wrong key should fail
_, err = dec.Decrypt(encrypted)
require.Error(t, err)
}

func TestDecrypt_CorruptedData(t *testing.T) {
publicKey, privateKey, err := hpke.GenerateKeyPair()
require.NoError(t, err)

enc, err := hpke.NewEncryptor(publicKey)
require.NoError(t, err)

dec, err := hpke.NewDecryptor(privateKey)
require.NoError(t, err)

data := []byte("test data")
encrypted, err := enc.Encrypt(data)
require.NoError(t, err)

// Corrupt the ciphertext
encrypted.Ciphertext[0] ^= 0xFF

// Decryption should fail due to authentication failure
_, err = dec.Decrypt(encrypted)
require.Error(t, err)
}
Loading
Loading