Skip to content

Commit aa5dbac

Browse files
committed
add higher level data types, move RSA to subpackage
Signed-off-by: Ashley Davis <ashley.davis@cyberark.com>
1 parent 5baafe8 commit aa5dbac

File tree

7 files changed

+118
-46
lines changed

7 files changed

+118
-46
lines changed

internal/envelope/doc.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
// Package envelope implements RSA envelope encryption, intended to be used to secure sensitive Secret data from a cluster
2-
// being being sent to an external system. This protects against threats such as TLS interception middleware.
1+
// Package envelope provides types and interfaces for envelope encryption.
32
//
4-
// Envelope encryption uses a combination of asymmetric encryption and symmetric encryption; since asymmetric encryption is
5-
// slow and has size limits, we generate a random symmetric key for each encryption operation, use that to encrypt the data,
6-
// then encrypt the symmetric key with the provided RSA public key. The recipient can then use their RSA private key to
7-
// decrypt the symmetric key, then use that to decrypt the data.
3+
// Envelope encryption combines asymmetric and symmetric cryptography to
4+
// efficiently encrypt data. The EncryptedData type holds the result, and
5+
// the Encryptor interface defines the encryption operation.
86
//
9-
// This implementation uses RSA-OAEP with SHA-256 for asymmetric encryption, and AES-256-GCM for symmetric encryption.
7+
// Implementations are available in subpackages:
108
//
11-
// In some documentation, the asymmetric key is called the "key encryption key" (KEK) and the symmetric key is called the "data encryption key" (DEK).
9+
// - internal/envelope/rsa: RSA-OAEP + AES-256-GCM
10+
//
11+
// See subpackage documentation for usage examples.
1212
package envelope

internal/envelope/rsa/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package rsa implements RSA envelope encryption, conforming to the interface in the envelope package.
2+
// It uses RSA-OAEP with SHA-256 for key encryption, and AES-256-GCM for data encryption.
3+
package rsa
Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package envelope
1+
package rsa
22

33
import (
44
"crypto/aes"
@@ -7,6 +7,8 @@ import (
77
"crypto/rsa"
88
"crypto/sha256"
99
"fmt"
10+
11+
"github.com/jetstack/preflight/internal/envelope"
1012
)
1113

1214
const (
@@ -17,17 +19,24 @@ const (
1719
// minRSAKeySize is the minimum RSA key size in bits; we'd expect that keys will be larger but 2048 is a sane floor
1820
// to enforce to ensure that a weak key can't accidentally be used
1921
minRSAKeySize = 2048
22+
23+
// keyAlgorithmIdentifier is set in EncryptedData to identify the key wrapping algorithm used in this package
24+
keyAlgorithmIdentifier = "RSA-OAEP-SHA256"
2025
)
2126

27+
// Compile-time check that Encryptor implements envelope.Encryptor
28+
var _ envelope.Encryptor = (*Encryptor)(nil)
29+
2230
// Encryptor provides envelope encryption using RSA for key wrapping
2331
// and AES-256-GCM for data encryption.
2432
type Encryptor struct {
33+
keyID string
2534
rsaPublicKey *rsa.PublicKey
2635
}
2736

2837
// NewEncryptor creates a new Encryptor with the provided RSA public key.
2938
// The RSA key must be at least minRSAKeySize bits
30-
func NewEncryptor(publicKey *rsa.PublicKey) (*Encryptor, error) {
39+
func NewEncryptor(keyID string, publicKey *rsa.PublicKey) (*Encryptor, error) {
3140
if publicKey == nil {
3241
return nil, fmt.Errorf("RSA public key cannot be nil")
3342
}
@@ -38,15 +47,20 @@ func NewEncryptor(publicKey *rsa.PublicKey) (*Encryptor, error) {
3847
return nil, fmt.Errorf("RSA key size must be at least %d bits, got %d bits", minRSAKeySize, keySize)
3948
}
4049

50+
if len(keyID) == 0 {
51+
return nil, fmt.Errorf("keyID cannot be empty")
52+
}
53+
4154
return &Encryptor{
55+
keyID: keyID,
4256
rsaPublicKey: publicKey,
4357
}, nil
4458
}
4559

4660
// Encrypt performs envelope encryption on the provided data.
4761
// It generates a random AES-256 key, encrypts the data with AES-256-GCM,
4862
// then encrypts the AES key with RSA-OAEP-SHA256.
49-
func (e *Encryptor) Encrypt(data []byte) (*EncryptedData, error) {
63+
func (e *Encryptor) Encrypt(data []byte) (*envelope.EncryptedData, error) {
5064
if len(data) == 0 {
5165
return nil, fmt.Errorf("data to encrypt cannot be empty")
5266
}
@@ -74,7 +88,9 @@ func (e *Encryptor) Encrypt(data []byte) (*EncryptedData, error) {
7488
return nil, fmt.Errorf("failed to create GCM cipher: %w", err)
7589
}
7690

77-
encryptedData := &EncryptedData{
91+
encryptedData := &envelope.EncryptedData{
92+
KeyID: e.keyID,
93+
KeyAlgorithm: keyAlgorithmIdentifier,
7894
EncryptedKey: nil,
7995
EncryptedData: nil,
8096
Nonce: make([]byte, gcm.NonceSize()),
Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,36 @@
1-
package envelope_test
1+
package rsa
22

33
import (
44
"crypto/rand"
55
"crypto/rsa"
6+
"sync"
67
"testing"
78

89
"github.com/stretchr/testify/require"
10+
)
11+
12+
const testKeyID = "test-key-id"
913

10-
"github.com/jetstack/preflight/internal/envelope"
14+
var (
15+
testKeyOnce sync.Once
16+
internalTestKey *rsa.PrivateKey
1117
)
1218

19+
// testKey generates and returns a singleton RSA private key for testing purposes,
20+
// to avoid needing to generate a new key for each test.
21+
func testKey() *rsa.PrivateKey {
22+
testKeyOnce.Do(func() {
23+
key, err := rsa.GenerateKey(rand.Reader, minRSAKeySize)
24+
if err != nil {
25+
panic("failed to generate test RSA key: " + err.Error())
26+
}
27+
28+
internalTestKey = key
29+
})
30+
31+
return internalTestKey
32+
}
33+
1334
func TestNewEncryptor_ValidKeys(t *testing.T) {
1435
tests := []struct {
1536
name string
@@ -25,7 +46,7 @@ func TestNewEncryptor_ValidKeys(t *testing.T) {
2546
key, err := rsa.GenerateKey(rand.Reader, tt.keySize)
2647
require.NoError(t, err)
2748

28-
enc, err := envelope.NewEncryptor(&key.PublicKey)
49+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
2950
require.NoError(t, err)
3051
require.NotNil(t, enc)
3152
})
@@ -36,24 +57,32 @@ func TestNewEncryptor_RejectsSmallKeys(t *testing.T) {
3657
key, err := rsa.GenerateKey(rand.Reader, 1024)
3758
require.NoError(t, err)
3859

39-
enc, err := envelope.NewEncryptor(&key.PublicKey)
60+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
4061
require.Error(t, err)
4162
require.Nil(t, enc)
4263
require.Contains(t, err.Error(), "must be at least 2048 bits")
4364
}
4465

4566
func TestNewEncryptor_NilKey(t *testing.T) {
46-
enc, err := envelope.NewEncryptor(nil)
67+
enc, err := NewEncryptor(testKeyID, nil)
4768
require.Error(t, err)
4869
require.Nil(t, enc)
4970
require.Contains(t, err.Error(), "cannot be nil")
5071
}
5172

73+
func TestNewEncryptor_EmptyKeyID(t *testing.T) {
74+
key := testKey()
75+
76+
enc, err := NewEncryptor("", &key.PublicKey)
77+
require.Error(t, err)
78+
require.Nil(t, enc)
79+
require.Contains(t, err.Error(), "keyID cannot be empty")
80+
}
81+
5282
func TestEncrypt_VariousDataSizes(t *testing.T) {
53-
key, err := rsa.GenerateKey(rand.Reader, 2048)
54-
require.NoError(t, err)
83+
key := testKey()
5584

56-
enc, err := envelope.NewEncryptor(&key.PublicKey)
85+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
5786
require.NoError(t, err)
5887

5988
tests := []struct {
@@ -80,6 +109,10 @@ func TestEncrypt_VariousDataSizes(t *testing.T) {
80109
require.NotEmpty(t, result.EncryptedData)
81110
require.NotEmpty(t, result.Nonce)
82111

112+
// Verify KeyID and KeyAlgorithm are set correctly
113+
require.Equal(t, testKeyID, result.KeyID)
114+
require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm)
115+
83116
// Verify nonce is correct size (12 bytes for GCM)
84117
require.Len(t, result.Nonce, 12)
85118

@@ -90,10 +123,9 @@ func TestEncrypt_VariousDataSizes(t *testing.T) {
90123
}
91124

92125
func TestEncrypt_EmptyData(t *testing.T) {
93-
key, err := rsa.GenerateKey(rand.Reader, 2048)
94-
require.NoError(t, err)
126+
key := testKey()
95127

96-
enc, err := envelope.NewEncryptor(&key.PublicKey)
128+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
97129
require.NoError(t, err)
98130

99131
result, err := enc.Encrypt([]byte{})
@@ -103,10 +135,9 @@ func TestEncrypt_EmptyData(t *testing.T) {
103135
}
104136

105137
func TestEncrypt_NonDeterministic(t *testing.T) {
106-
key, err := rsa.GenerateKey(rand.Reader, 2048)
107-
require.NoError(t, err)
138+
key := testKey()
108139

109-
enc, err := envelope.NewEncryptor(&key.PublicKey)
140+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
110141
require.NoError(t, err)
111142

112143
data := []byte("test data for encryption")
@@ -118,6 +149,12 @@ func TestEncrypt_NonDeterministic(t *testing.T) {
118149
result2, err := enc.Encrypt(data)
119150
require.NoError(t, err)
120151

152+
// Verify KeyID and KeyAlgorithm are set correctly in both results
153+
require.Equal(t, testKeyID, result1.KeyID)
154+
require.Equal(t, keyAlgorithmIdentifier, result1.KeyAlgorithm)
155+
require.Equal(t, testKeyID, result2.KeyID)
156+
require.Equal(t, keyAlgorithmIdentifier, result2.KeyAlgorithm)
157+
121158
// Nonces should be different (random)
122159
require.NotEqual(t, result1.Nonce, result2.Nonce)
123160

@@ -129,10 +166,9 @@ func TestEncrypt_NonDeterministic(t *testing.T) {
129166
}
130167

131168
func TestEncrypt_AllFieldsPopulated(t *testing.T) {
132-
key, err := rsa.GenerateKey(rand.Reader, 2048)
133-
require.NoError(t, err)
169+
key := testKey()
134170

135-
enc, err := envelope.NewEncryptor(&key.PublicKey)
171+
enc, err := NewEncryptor(testKeyID, &key.PublicKey)
136172
require.NoError(t, err)
137173

138174
data := []byte("test data")
@@ -144,6 +180,10 @@ func TestEncrypt_AllFieldsPopulated(t *testing.T) {
144180
require.NotEmpty(t, result.EncryptedData, "EncryptedData should be populated")
145181
require.NotEmpty(t, result.Nonce, "Nonce should be populated")
146182

183+
// Verify KeyID and KeyAlgorithm are set correctly
184+
require.Equal(t, testKeyID, result.KeyID, "KeyID should match the encryptor's keyID")
185+
require.Equal(t, keyAlgorithmIdentifier, result.KeyAlgorithm, "KeyAlgorithm should be the value of keyAlgorithmIdentifier")
186+
147187
// Verify encrypted key size is appropriate for RSA 2048
148188
require.Equal(t, 256, len(result.EncryptedKey), "EncryptedKey should be 256 bytes for RSA 2048")
149189
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package envelope
1+
package rsa
22

33
import (
44
"crypto/rsa"
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package envelope_test
1+
package rsa_test
22

33
import (
44
"crypto/ecdsa"
@@ -13,7 +13,7 @@ import (
1313

1414
"github.com/stretchr/testify/require"
1515

16-
"github.com/jetstack/preflight/internal/envelope"
16+
internalrsa "github.com/jetstack/preflight/internal/envelope/rsa"
1717
)
1818

1919
func generateTestKeyPEM(t *testing.T, keySize int, pemType string) []byte {
@@ -49,7 +49,7 @@ func generateTestKeyPEM(t *testing.T, keySize int, pemType string) []byte {
4949
func TestLoadPublicKeyFromPEM_PKIX(t *testing.T) {
5050
pemBytes := generateTestKeyPEM(t, 2048, "PUBLIC KEY")
5151

52-
key, err := envelope.LoadPublicKeyFromPEM(pemBytes)
52+
key, err := internalrsa.LoadPublicKeyFromPEM(pemBytes)
5353
require.NoError(t, err)
5454
require.NotNil(t, key)
5555
require.Equal(t, 2048, key.N.BitLen())
@@ -58,7 +58,7 @@ func TestLoadPublicKeyFromPEM_PKIX(t *testing.T) {
5858
func TestLoadPublicKeyFromPEM_PKCS1(t *testing.T) {
5959
pemBytes := generateTestKeyPEM(t, 2048, "RSA PUBLIC KEY")
6060

61-
key, err := envelope.LoadPublicKeyFromPEM(pemBytes)
61+
key, err := internalrsa.LoadPublicKeyFromPEM(pemBytes)
6262
require.NoError(t, err)
6363
require.NotNil(t, key)
6464
require.Equal(t, 2048, key.N.BitLen())
@@ -67,7 +67,7 @@ func TestLoadPublicKeyFromPEM_PKCS1(t *testing.T) {
6767
func TestLoadPublicKeyFromPEM_InvalidPEM(t *testing.T) {
6868
invalidPEM := []byte("this is not a valid PEM")
6969

70-
key, err := envelope.LoadPublicKeyFromPEM(invalidPEM)
70+
key, err := internalrsa.LoadPublicKeyFromPEM(invalidPEM)
7171
require.Error(t, err)
7272
require.Nil(t, key)
7373
require.Contains(t, err.Error(), "failed to decode PEM block")
@@ -84,7 +84,7 @@ func TestLoadPublicKeyFromPEM_WrongPEMType(t *testing.T) {
8484
Bytes: privateKeyBytes,
8585
})
8686

87-
key, err := envelope.LoadPublicKeyFromPEM(pemBytes)
87+
key, err := internalrsa.LoadPublicKeyFromPEM(pemBytes)
8888
require.Error(t, err)
8989
require.Nil(t, key)
9090
require.Contains(t, err.Error(), "unsupported PEM block type")
@@ -104,7 +104,7 @@ func TestLoadPublicKeyFromPEM_NonRSAKey(t *testing.T) {
104104
Bytes: publicKeyBytes,
105105
})
106106

107-
key, err := envelope.LoadPublicKeyFromPEM(pemBytes)
107+
key, err := internalrsa.LoadPublicKeyFromPEM(pemBytes)
108108
require.Error(t, err)
109109
require.Nil(t, key)
110110
require.Contains(t, err.Error(), "not an RSA public key")
@@ -118,14 +118,14 @@ func TestLoadPublicKeyFromPEMFile_ValidFile(t *testing.T) {
118118
err := os.WriteFile(keyPath, pemBytes, 0600)
119119
require.NoError(t, err)
120120

121-
key, err := envelope.LoadPublicKeyFromPEMFile(keyPath)
121+
key, err := internalrsa.LoadPublicKeyFromPEMFile(keyPath)
122122
require.NoError(t, err)
123123
require.NotNil(t, key)
124124
require.Equal(t, 2048, key.N.BitLen())
125125
}
126126

127127
func TestLoadPublicKeyFromPEMFile_MissingFile(t *testing.T) {
128-
key, err := envelope.LoadPublicKeyFromPEMFile("/nonexistent/path/key.pem")
128+
key, err := internalrsa.LoadPublicKeyFromPEMFile("/nonexistent/path/key.pem")
129129
require.Error(t, err)
130130
require.Nil(t, key)
131131
require.Contains(t, err.Error(), "failed to read PEM file")
@@ -138,7 +138,7 @@ func TestLoadPublicKeyFromPEMFile_InvalidContent(t *testing.T) {
138138
err := os.WriteFile(keyPath, []byte("not a valid PEM"), 0600)
139139
require.NoError(t, err)
140140

141-
key, err := envelope.LoadPublicKeyFromPEMFile(keyPath)
141+
key, err := internalrsa.LoadPublicKeyFromPEMFile(keyPath)
142142
require.Error(t, err)
143143
require.Nil(t, key)
144144
}

internal/envelope/types.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,31 @@
11
package envelope
22

33
// EncryptedData contains the result of envelope encryption.
4-
// It includes the encrypted data, the encrypted AES key which was used for encrypting the original data,
5-
// and the nonce needed for AES-GCM decryption.
4+
// It includes the encrypted data, the encrypted symmetric key which was used for encrypting the original data,
5+
// and the nonce needed for the symmetric decryption.
66
type EncryptedData struct {
7-
// EncryptedKey is the AES-256 key encrypted with RSA-OAEP-SHA256.
8-
// This is ciphertext and should only be decryptable by the holder of the corresponding RSA private key.
7+
// KeyID is the identifier of the asymmetric key used to encrypt the AES key.
8+
KeyID string `json:"key_id"`
9+
10+
// KeyAlgorithm is the algorithm of the asymmetric key used to encrypt the AES key.
11+
KeyAlgorithm string `json:"key_algorithm"`
12+
13+
// EncryptedKey is an encrypted AES-256-GCM symmetric key, used to encrypt EncryptedData.
14+
// This is ciphertext and should only be decryptable by the holder of the private key.
915
EncryptedKey []byte `json:"encrypted_key"`
1016

11-
// EncryptedData is the actual data encrypted using AES-256-GCM.
12-
// This is ciphertext and requires the AES key (after RSA decryption) and nonce for decryption.
17+
// EncryptedData is the actual data encrypted using the AES-256-GCM in EncryptedKey.
18+
// This is ciphertext and requires the decrypted AES key and nonce for decryption.
1319
EncryptedData []byte `json:"encrypted_data"`
1420

1521
// Nonce is the 12-byte nonce used for AES-GCM encryption.
1622
// This is intentionally plaintext.
1723
Nonce []byte `json:"nonce"`
1824
}
25+
26+
// Encryptor performs envelope encryption on arbitrary data.
27+
type Encryptor interface {
28+
// Encrypt encrypts data using envelope encryption, returning the resulting data along
29+
// with identifiers of the asymmetric key used to encrypt the AES key.
30+
Encrypt(data []byte) (*EncryptedData, error)
31+
}

0 commit comments

Comments
 (0)