Skip to content

Commit 1bcbc93

Browse files
committed
wip: hpke
Signed-off-by: Ashley Davis <ashley.davis@cyberark.com>
1 parent dbbf22e commit 1bcbc93

7 files changed

Lines changed: 572 additions & 0 deletions

File tree

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module github.com/jetstack/preflight
44
go 1.24.4
55

66
require (
7+
filippo.io/hpke v0.4.0
78
github.com/Venafi/vcert/v5 v5.12.2
89
github.com/cenkalti/backoff/v5 v5.0.3
910
github.com/fatih/color v1.18.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
22
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
3+
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
4+
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
35
github.com/Khan/genqlient v0.8.1 h1:wtOCc8N9rNynRLXN3k3CnfzheCUNKBcvXmVv5zt6WCs=
46
github.com/Khan/genqlient v0.8.1/go.mod h1:R2G6DzjBvCbhjsEajfRjbWdVglSH/73kSivC9TLWVjU=
57
github.com/Venafi/vcert/v5 v5.12.2 h1:Ee3/A9fZRiisuwuz22/Nqgl19H0ztQjWv35AC63qPcA=

internal/hpke/decryptor.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package hpke
2+
3+
import (
4+
"fmt"
5+
6+
"filippo.io/hpke"
7+
)
8+
9+
// NewDecryptor creates a new Decryptor with the provided HPKE private key.
10+
// It uses the default algorithms: X25519 KEM, HKDF-SHA256, and AES-256-GCM.
11+
func NewDecryptor(privateKey hpke.PrivateKey) (*Decryptor, error) {
12+
if privateKey == nil {
13+
return nil, fmt.Errorf("HPKE private key cannot be nil")
14+
}
15+
16+
return &Decryptor{
17+
privateKey: privateKey,
18+
kdf: DefaultKDF(),
19+
aead: DefaultAEAD(),
20+
}, nil
21+
}
22+
23+
// NewDecryptorWithAlgorithms creates a new Decryptor with custom algorithms.
24+
// The KDF and AEAD must match those used during encryption.
25+
// Note: The KEM is determined by the private key itself.
26+
func NewDecryptorWithAlgorithms(privateKey hpke.PrivateKey, kdf hpke.KDF, aead hpke.AEAD) (*Decryptor, error) {
27+
if privateKey == nil {
28+
return nil, fmt.Errorf("HPKE private key cannot be nil")
29+
}
30+
31+
if kdf == nil {
32+
return nil, fmt.Errorf("KDF cannot be nil")
33+
}
34+
35+
if aead == nil {
36+
return nil, fmt.Errorf("AEAD cannot be nil")
37+
}
38+
39+
return &Decryptor{
40+
privateKey: privateKey,
41+
kdf: kdf,
42+
aead: aead,
43+
}, nil
44+
}
45+
46+
// Decrypt decrypts the provided EncryptedData using HPKE.
47+
func (d *Decryptor) Decrypt(encrypted *EncryptedData) ([]byte, error) {
48+
if encrypted == nil {
49+
return nil, fmt.Errorf("encrypted data cannot be nil")
50+
}
51+
52+
if len(encrypted.EncapsulatedKey) == 0 {
53+
return nil, fmt.Errorf("encapsulated key cannot be empty")
54+
}
55+
56+
if len(encrypted.Ciphertext) == 0 {
57+
return nil, fmt.Errorf("ciphertext cannot be empty")
58+
}
59+
60+
// NewRecipient creates a receiver context from the encapsulated key.
61+
// The info parameter must match what was used during encryption (nil in our case).
62+
recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, nil)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
65+
}
66+
67+
// Open decrypts and authenticates the ciphertext.
68+
plaintext, err := recipient.Open(nil, encrypted.Ciphertext)
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
71+
}
72+
73+
return plaintext, nil
74+
}
75+
76+
// DecryptWithInfo decrypts data that was encrypted with application-specific context information.
77+
// The info parameter must match what was used during encryption.
78+
func (d *Decryptor) DecryptWithInfo(encrypted *EncryptedData, info []byte) ([]byte, error) {
79+
if encrypted == nil {
80+
return nil, fmt.Errorf("encrypted data cannot be nil")
81+
}
82+
83+
if len(encrypted.EncapsulatedKey) == 0 {
84+
return nil, fmt.Errorf("encapsulated key cannot be empty")
85+
}
86+
87+
if len(encrypted.Ciphertext) == 0 {
88+
return nil, fmt.Errorf("ciphertext cannot be empty")
89+
}
90+
91+
recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, info)
92+
if err != nil {
93+
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
94+
}
95+
96+
plaintext, err := recipient.Open(nil, encrypted.Ciphertext)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
99+
}
100+
101+
return plaintext, nil
102+
}
103+
104+
// DecryptWithAAD decrypts data that was encrypted with additional authenticated data.
105+
// The aad parameter must match what was used during encryption.
106+
func (d *Decryptor) DecryptWithAAD(encrypted *EncryptedData, aad []byte) ([]byte, error) {
107+
if encrypted == nil {
108+
return nil, fmt.Errorf("encrypted data cannot be nil")
109+
}
110+
111+
if len(encrypted.EncapsulatedKey) == 0 {
112+
return nil, fmt.Errorf("encapsulated key cannot be empty")
113+
}
114+
115+
if len(encrypted.Ciphertext) == 0 {
116+
return nil, fmt.Errorf("ciphertext cannot be empty")
117+
}
118+
119+
recipient, err := hpke.NewRecipient(encrypted.EncapsulatedKey, d.privateKey, d.kdf, d.aead, nil)
120+
if err != nil {
121+
return nil, fmt.Errorf("failed to create HPKE recipient: %w", err)
122+
}
123+
124+
plaintext, err := recipient.Open(aad, encrypted.Ciphertext)
125+
if err != nil {
126+
return nil, fmt.Errorf("failed to open HPKE ciphertext: %w", err)
127+
}
128+
129+
return plaintext, nil
130+
}

internal/hpke/decryptor_test.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package hpke_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/jetstack/preflight/internal/hpke"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNewDecryptor_NilKey(t *testing.T) {
11+
dec, err := hpke.NewDecryptor(nil)
12+
require.Error(t, err)
13+
require.Nil(t, dec)
14+
require.Contains(t, err.Error(), "cannot be nil")
15+
}
16+
17+
func TestEncryptDecrypt_RoundTrip(t *testing.T) {
18+
publicKey, privateKey, err := hpke.GenerateKeyPair()
19+
require.NoError(t, err)
20+
21+
enc, err := hpke.NewEncryptor(publicKey)
22+
require.NoError(t, err)
23+
24+
dec, err := hpke.NewDecryptor(privateKey)
25+
require.NoError(t, err)
26+
27+
originalData := []byte("sensitive data to encrypt and decrypt")
28+
29+
// Encrypt
30+
encrypted, err := enc.Encrypt(originalData)
31+
require.NoError(t, err)
32+
require.NotNil(t, encrypted)
33+
34+
// Decrypt
35+
decrypted, err := dec.Decrypt(encrypted)
36+
require.NoError(t, err)
37+
require.Equal(t, originalData, decrypted)
38+
}
39+
40+
func TestEncryptDecrypt_WithInfo(t *testing.T) {
41+
publicKey, privateKey, err := hpke.GenerateKeyPair()
42+
require.NoError(t, err)
43+
44+
enc, err := hpke.NewEncryptor(publicKey)
45+
require.NoError(t, err)
46+
47+
dec, err := hpke.NewDecryptor(privateKey)
48+
require.NoError(t, err)
49+
50+
originalData := []byte("sensitive data")
51+
info := []byte("application-specific context")
52+
53+
// Encrypt with info
54+
encrypted, err := enc.EncryptWithInfo(originalData, info)
55+
require.NoError(t, err)
56+
57+
// Decrypt with matching info
58+
decrypted, err := dec.DecryptWithInfo(encrypted, info)
59+
require.NoError(t, err)
60+
require.Equal(t, originalData, decrypted)
61+
62+
// Decrypt with wrong info should fail
63+
_, err = dec.DecryptWithInfo(encrypted, []byte("wrong info"))
64+
require.Error(t, err)
65+
66+
// Decrypt without info should fail
67+
_, err = dec.Decrypt(encrypted)
68+
require.Error(t, err)
69+
}
70+
71+
func TestEncryptDecrypt_WithAAD(t *testing.T) {
72+
publicKey, privateKey, err := hpke.GenerateKeyPair()
73+
require.NoError(t, err)
74+
75+
enc, err := hpke.NewEncryptor(publicKey)
76+
require.NoError(t, err)
77+
78+
dec, err := hpke.NewDecryptor(privateKey)
79+
require.NoError(t, err)
80+
81+
originalData := []byte("sensitive data")
82+
aad := []byte("additional authenticated data")
83+
84+
// Encrypt with AAD
85+
encrypted, err := enc.EncryptWithAAD(originalData, aad)
86+
require.NoError(t, err)
87+
88+
// Decrypt with matching AAD
89+
decrypted, err := dec.DecryptWithAAD(encrypted, aad)
90+
require.NoError(t, err)
91+
require.Equal(t, originalData, decrypted)
92+
93+
// Decrypt with wrong AAD should fail
94+
_, err = dec.DecryptWithAAD(encrypted, []byte("wrong aad"))
95+
require.Error(t, err)
96+
97+
// Decrypt without AAD should fail
98+
_, err = dec.Decrypt(encrypted)
99+
require.Error(t, err)
100+
}
101+
102+
func TestDecrypt_WrongKey(t *testing.T) {
103+
publicKey1, _, err := hpke.GenerateKeyPair()
104+
require.NoError(t, err)
105+
106+
_, privateKey2, err := hpke.GenerateKeyPair()
107+
require.NoError(t, err)
108+
109+
enc, err := hpke.NewEncryptor(publicKey1)
110+
require.NoError(t, err)
111+
112+
dec, err := hpke.NewDecryptor(privateKey2)
113+
require.NoError(t, err)
114+
115+
data := []byte("test data")
116+
encrypted, err := enc.Encrypt(data)
117+
require.NoError(t, err)
118+
119+
// Decryption with wrong key should fail
120+
_, err = dec.Decrypt(encrypted)
121+
require.Error(t, err)
122+
}
123+
124+
func TestDecrypt_CorruptedData(t *testing.T) {
125+
publicKey, privateKey, err := hpke.GenerateKeyPair()
126+
require.NoError(t, err)
127+
128+
enc, err := hpke.NewEncryptor(publicKey)
129+
require.NoError(t, err)
130+
131+
dec, err := hpke.NewDecryptor(privateKey)
132+
require.NoError(t, err)
133+
134+
data := []byte("test data")
135+
encrypted, err := enc.Encrypt(data)
136+
require.NoError(t, err)
137+
138+
// Corrupt the ciphertext
139+
encrypted.Ciphertext[0] ^= 0xFF
140+
141+
// Decryption should fail due to authentication failure
142+
_, err = dec.Decrypt(encrypted)
143+
require.Error(t, err)
144+
}

0 commit comments

Comments
 (0)