Skip to content

Commit 0875ec0

Browse files
committed
Add a test and more documentation for encryption
1 parent 8283b0c commit 0875ec0

File tree

4 files changed

+185
-140
lines changed

4 files changed

+185
-140
lines changed

keystore/encryptor.go

Lines changed: 117 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ import (
1818
"github.com/smartcontractkit/chainlink-common/keystore/internal"
1919
)
2020

21+
// Opaque error messages to prevent information leakage
22+
var (
23+
ErrEncryptionFailed = fmt.Errorf("encryption operation failed")
24+
ErrDecryptionFailed = fmt.Errorf("decryption operation failed")
25+
)
26+
2127
type EncryptRequest struct {
2228
KeyName string
2329
RemotePubKey []byte
@@ -47,40 +53,28 @@ type DeriveSharedSecretResponse struct {
4753
}
4854

4955
const (
50-
aesGCMNonceSize = 12
51-
hkdfSaltSize = 16
52-
53-
// ciphertext framing for EcdhP256 + HKDF-SHA256 + AES-GCM
54-
// [1B version=1][2B ephLen][ephPub][1B saltLen][salt][1B nonceLen][nonce][ciphertext]
56+
// 16 byte is the standard NIST recommended salt size for HKDF.
57+
hkdfSaltSize = 16
58+
// 1 byte is the version for the encryption envelope.
5559
encVersionV1 byte = 1
56-
57-
algP256HKDFAESGCM = "ecdh-p256+hkdf-sha256+aes-256-gcm"
5860
)
5961

60-
func hkdfAESGCMKey(sharedSecret, salt, info []byte, keyLen int) ([]byte, error) {
61-
r := hkdf.New(sha256.New, sharedSecret, salt, info)
62-
key := make([]byte, keyLen)
63-
if _, err := io.ReadFull(r, key); err != nil {
64-
return nil, fmt.Errorf("hkdf: %w", err)
65-
}
66-
return key, nil
67-
}
62+
var (
63+
// Domain separation for HKDF-SHA256 based AES-GCM keys.
64+
infoAESGCM = []byte("keystore:ecdh-p256:aes-gcm:hkdf-sha256:v1")
65+
)
6866

69-
type encAAD struct {
70-
V byte `json:"v"`
71-
Alg string `json:"alg"`
72-
EPK []byte `json:"epk"`
73-
Salt []byte `json:"salt"`
74-
Nonce []byte `json:"nonce"`
67+
type encHeader struct {
68+
Version byte `json:"version"`
69+
Alg string `json:"alg"`
70+
EphemeralPublicKey []byte `json:"ephemeral_public_key"`
71+
Salt []byte `json:"salt"`
72+
Nonce []byte `json:"nonce"`
7573
}
7674

7775
type encEnvelope struct {
78-
V byte `json:"v"`
79-
Alg string `json:"alg"`
80-
EPK []byte `json:"epk"`
81-
Salt []byte `json:"salt"`
82-
Nonce []byte `json:"nonce"`
83-
CT []byte `json:"ct"`
76+
encHeader
77+
CipherText []byte `json:"ciphertext"`
8478
}
8579

8680
// Encryptor is an interfaces for hybrid encryption (key exchange + encryption) operations.
@@ -94,6 +88,7 @@ type Encryptor interface {
9488
}
9589

9690
// UnimplementedEncryptor returns ErrUnimplemented for all Encryptor methods.
91+
// Clients should embed this struct to ensure forward compatibility with changes to the Encryptor interface.
9792
type UnimplementedEncryptor struct{}
9893

9994
func (UnimplementedEncryptor) Encrypt(ctx context.Context, req EncryptRequest) (EncryptResponse, error) {
@@ -112,175 +107,202 @@ func (k *keystore) Encrypt(ctx context.Context, req EncryptRequest) (EncryptResp
112107
k.mu.RLock()
113108
defer k.mu.RUnlock()
114109

110+
// Validate request parameters without leaking information
111+
if req.KeyName == "" || len(req.Data) == 0 {
112+
return EncryptResponse{}, ErrEncryptionFailed
113+
}
114+
115115
key, ok := k.keystore[req.KeyName]
116116
if !ok {
117-
return EncryptResponse{}, fmt.Errorf("key not found: %s", req.KeyName)
117+
// Don't leak key existence - return same error as other failures
118+
return EncryptResponse{}, ErrEncryptionFailed
118119
}
120+
119121
switch key.keyType {
120122
case X25519:
121123
if len(req.RemotePubKey) != 32 {
122-
return EncryptResponse{}, fmt.Errorf("remote public key must be 32 bytes for X25519")
124+
return EncryptResponse{}, ErrEncryptionFailed
123125
}
124126
encrypted, err := box.SealAnonymous(nil, req.Data, (*[32]byte)(req.RemotePubKey), rand.Reader)
125127
if err != nil {
126-
return EncryptResponse{}, fmt.Errorf("failed to encrypt data: %w", err)
128+
return EncryptResponse{}, ErrEncryptionFailed
127129
}
128130
return EncryptResponse{
129131
EncryptedData: encrypted,
130132
}, nil
131133
case EcdhP256:
132134
curve := ecdh.P256()
133135
if len(req.RemotePubKey) == 0 {
134-
return EncryptResponse{}, fmt.Errorf("remote public key required for EcdhP256")
136+
return EncryptResponse{}, ErrEncryptionFailed
135137
}
138+
// Remote public key must be on the P256 curve for the shared secret to work.
136139
recipientPub, err := curve.NewPublicKey(req.RemotePubKey)
137140
if err != nil {
138-
return EncryptResponse{}, fmt.Errorf("invalid P-256 public key: %w", err)
141+
return EncryptResponse{}, ErrEncryptionFailed
139142
}
140-
// Ephemeral key pair
143+
// Create an ephemeral keypair on the P256 curve used for encryption.
141144
ephPriv, err := curve.GenerateKey(rand.Reader)
142145
if err != nil {
143-
return EncryptResponse{}, fmt.Errorf("failed to generate ephemeral key: %w", err)
146+
return EncryptResponse{}, ErrEncryptionFailed
144147
}
148+
// The magic here is the the receipient can compute the same
149+
// shared secret because ephPriv*G*recipientPriv = ephPub*G.
150+
// This lets them derive the same ephemeral key used for encryption
151+
// so they can decrypt the ciphertext.
145152
shared, err := ephPriv.ECDH(recipientPub)
146153
if err != nil {
147-
return EncryptResponse{}, fmt.Errorf("ecdh failed: %w", err)
154+
return EncryptResponse{}, ErrEncryptionFailed
148155
}
149156
// Derive AES-256-GCM key
157+
// The reason we do this is so that we can use symmetric encryption (more efficient)
158+
// This is part of any standard hybrid encryption scheme.
159+
// We include random salt to prevent rainbow table attacks (i.e. preventing
160+
// attackers from tracking a mapping of encryption data to plaintext)
150161
salt := make([]byte, hkdfSaltSize)
151162
if _, err := rand.Read(salt); err != nil {
152-
return EncryptResponse{}, fmt.Errorf("salt generation failed: %w", err)
163+
return EncryptResponse{}, ErrEncryptionFailed
153164
}
154-
info := []byte("keystore:ecdh-p256:aes-gcm:hkdf-sha256:v1")
155-
aeadKey, err := hkdfAESGCMKey(shared, salt, info, 32)
165+
derivedKey, err := deriveAESKeyFromSharedSecret(shared, salt, infoAESGCM)
156166
if err != nil {
157-
return EncryptResponse{}, err
167+
return EncryptResponse{}, ErrEncryptionFailed
158168
}
159-
block, err := aes.NewCipher(aeadKey)
169+
block, err := aes.NewCipher(derivedKey)
160170
if err != nil {
161-
return EncryptResponse{}, fmt.Errorf("aes: %w", err)
171+
return EncryptResponse{}, ErrEncryptionFailed
162172
}
163173
gcm, err := cipher.NewGCM(block)
164174
if err != nil {
165-
return EncryptResponse{}, fmt.Errorf("gcm: %w", err)
175+
return EncryptResponse{}, ErrEncryptionFailed
166176
}
167-
nonce := make([]byte, aesGCMNonceSize)
177+
nonce := make([]byte, gcm.NonceSize())
168178
if _, err := rand.Read(nonce); err != nil {
169-
return EncryptResponse{}, fmt.Errorf("nonce generation failed: %w", err)
179+
return EncryptResponse{}, ErrEncryptionFailed
170180
}
171181
ephPub := ephPriv.PublicKey().Bytes()
172-
head := encAAD{
173-
V: encVersionV1,
174-
Alg: algP256HKDFAESGCM,
175-
EPK: ephPub,
176-
Salt: salt,
177-
Nonce: nonce,
182+
head := encHeader{
183+
Version: encVersionV1,
184+
Alg: EcdhP256.String(),
185+
EphemeralPublicKey: ephPub,
186+
Salt: salt,
187+
Nonce: nonce,
178188
}
179189
aadBytes, err := json.Marshal(head)
180190
if err != nil {
181-
return EncryptResponse{}, fmt.Errorf("aad marshal: %w", err)
191+
return EncryptResponse{}, ErrEncryptionFailed
182192
}
193+
// Critical to include the header parameters as additional authenticated data.
194+
// Prevents a MITM from changing the header.
183195
ciphertext := gcm.Seal(nil, nonce, req.Data, aadBytes)
184196
env := encEnvelope{
185-
V: encVersionV1,
186-
Alg: algP256HKDFAESGCM,
187-
EPK: ephPub,
188-
Salt: salt,
189-
Nonce: nonce,
190-
CT: ciphertext,
197+
encHeader: head,
198+
CipherText: ciphertext,
191199
}
192200
out, err := json.Marshal(env)
193201
if err != nil {
194-
return EncryptResponse{}, fmt.Errorf("envelope marshal: %w", err)
202+
return EncryptResponse{}, ErrEncryptionFailed
195203
}
196204
return EncryptResponse{EncryptedData: out}, nil
197205
default:
198-
return EncryptResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
206+
return EncryptResponse{}, ErrEncryptionFailed
199207
}
200208
}
201209

202210
func (k *keystore) Decrypt(ctx context.Context, req DecryptRequest) (DecryptResponse, error) {
203211
k.mu.RLock()
204212
defer k.mu.RUnlock()
205213

214+
// Validate request parameters without leaking information
215+
216+
if req.KeyName == "" || len(req.EncryptedData) == 0 {
217+
return DecryptResponse{}, ErrDecryptionFailed
218+
}
219+
206220
key, ok := k.keystore[req.KeyName]
207221
if !ok {
208-
return DecryptResponse{}, fmt.Errorf("key not found: %s", req.KeyName)
222+
// Don't leak key existence - return same error as other failures
223+
return DecryptResponse{}, ErrDecryptionFailed
209224
}
225+
210226
switch key.keyType {
211227
case X25519:
212228
decrypted, ok := box.OpenAnonymous(nil, req.EncryptedData, (*[32]byte)(key.publicKey), (*[32]byte)(internal.Bytes(key.privateKey)))
213229
if !ok {
214-
return DecryptResponse{}, fmt.Errorf("failed to decrypt data")
230+
return DecryptResponse{}, ErrDecryptionFailed
215231
}
216232
return DecryptResponse{
217233
Data: decrypted,
218234
}, nil
219235
case EcdhP256:
220236
var env encEnvelope
221237
if err := json.Unmarshal(req.EncryptedData, &env); err != nil {
222-
return DecryptResponse{}, fmt.Errorf("envelope unmarshal: %w", err)
238+
return DecryptResponse{}, ErrDecryptionFailed
223239
}
224-
if env.V != encVersionV1 || env.Alg != algP256HKDFAESGCM {
225-
return DecryptResponse{}, fmt.Errorf("unsupported envelope version/alg")
240+
if env.Version != encVersionV1 || env.Alg != string(EcdhP256) {
241+
return DecryptResponse{}, ErrDecryptionFailed
226242
}
227243
curve := ecdh.P256()
228244
priv, err := curve.NewPrivateKey(internal.Bytes(key.privateKey))
229245
if err != nil {
230-
return DecryptResponse{}, fmt.Errorf("invalid P-256 private key: %w", err)
246+
return DecryptResponse{}, ErrDecryptionFailed
231247
}
232-
ephPub, err := curve.NewPublicKey(env.EPK)
248+
ephPub, err := curve.NewPublicKey(env.EphemeralPublicKey)
233249
if err != nil {
234-
return DecryptResponse{}, fmt.Errorf("invalid P-256 ephemeral public key: %w", err)
250+
return DecryptResponse{}, ErrDecryptionFailed
235251
}
236252
shared, err := priv.ECDH(ephPub)
237253
if err != nil {
238-
return DecryptResponse{}, fmt.Errorf("ecdh failed: %w", err)
254+
return DecryptResponse{}, ErrDecryptionFailed
239255
}
240-
info := []byte("keystore:ecdh-p256:aes-gcm:hkdf-sha256:v1")
241-
aeadKey, err := hkdfAESGCMKey(shared, env.Salt, info, 32)
256+
derivedKey, err := deriveAESKeyFromSharedSecret(shared, env.Salt, infoAESGCM)
242257
if err != nil {
243-
return DecryptResponse{}, err
258+
return DecryptResponse{}, ErrDecryptionFailed
244259
}
245-
block, err := aes.NewCipher(aeadKey)
260+
block, err := aes.NewCipher(derivedKey)
246261
if err != nil {
247-
return DecryptResponse{}, fmt.Errorf("aes: %w", err)
262+
return DecryptResponse{}, ErrDecryptionFailed
248263
}
249264
gcm, err := cipher.NewGCM(block)
250265
if err != nil {
251-
return DecryptResponse{}, fmt.Errorf("gcm: %w", err)
266+
return DecryptResponse{}, ErrDecryptionFailed
252267
}
253-
aad := encAAD{V: env.V, Alg: env.Alg, EPK: env.EPK, Salt: env.Salt, Nonce: env.Nonce}
268+
aad := encHeader{Version: env.Version, Alg: env.Alg, EphemeralPublicKey: env.EphemeralPublicKey, Salt: env.Salt, Nonce: env.Nonce}
254269
aadBytes, err := json.Marshal(aad)
255270
if err != nil {
256-
return DecryptResponse{}, fmt.Errorf("aad marshal: %w", err)
271+
return DecryptResponse{}, ErrDecryptionFailed
257272
}
258-
pt, err := gcm.Open(nil, env.Nonce, env.CT, aadBytes)
273+
pt, err := gcm.Open(nil, env.Nonce, env.CipherText, aadBytes)
259274
if err != nil {
260-
return DecryptResponse{}, fmt.Errorf("gcm open: %w", err)
275+
return DecryptResponse{}, ErrDecryptionFailed
261276
}
262277
return DecryptResponse{Data: pt}, nil
263278
default:
264-
return DecryptResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
279+
return DecryptResponse{}, ErrDecryptionFailed
265280
}
266281
}
267282

268283
func (k *keystore) DeriveSharedSecret(ctx context.Context, req DeriveSharedSecretRequest) (DeriveSharedSecretResponse, error) {
269284
k.mu.RLock()
270285
defer k.mu.RUnlock()
271286

287+
// Validate request parameters without leaking information
288+
if req.LocalKeyName == "" || len(req.RemotePubKey) == 0 {
289+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
290+
}
291+
272292
key, ok := k.keystore[req.LocalKeyName]
273293
if !ok {
274-
return DeriveSharedSecretResponse{}, fmt.Errorf("key not found: %s", req.LocalKeyName)
294+
// Don't leak key existence - return same error as other failures
295+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
275296
}
297+
276298
switch key.keyType {
277299
case X25519:
278300
if len(req.RemotePubKey) != 32 {
279-
return DeriveSharedSecretResponse{}, fmt.Errorf("remote public key must be 32 bytes")
301+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
280302
}
281303
sharedSecret, err := curve25519.X25519(internal.Bytes(key.privateKey), req.RemotePubKey)
282304
if err != nil {
283-
return DeriveSharedSecretResponse{}, fmt.Errorf("failed to derive shared secret: %w", err)
305+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
284306
}
285307
return DeriveSharedSecretResponse{
286308
SharedSecret: sharedSecret,
@@ -289,18 +311,27 @@ func (k *keystore) DeriveSharedSecret(ctx context.Context, req DeriveSharedSecre
289311
curve := ecdh.P256()
290312
priv, err := curve.NewPrivateKey(internal.Bytes(key.privateKey))
291313
if err != nil {
292-
return DeriveSharedSecretResponse{}, fmt.Errorf("invalid P-256 private key: %w", err)
314+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
293315
}
294316
remotePub, err := curve.NewPublicKey(req.RemotePubKey)
295317
if err != nil {
296-
return DeriveSharedSecretResponse{}, fmt.Errorf("invalid P-256 public key: %w", err)
318+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
297319
}
298320
shared, err := priv.ECDH(remotePub)
299321
if err != nil {
300-
return DeriveSharedSecretResponse{}, fmt.Errorf("ecdh failed: %w", err)
322+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
301323
}
302324
return DeriveSharedSecretResponse{SharedSecret: shared}, nil
303325
default:
304-
return DeriveSharedSecretResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
326+
return DeriveSharedSecretResponse{}, ErrEncryptionFailed
305327
}
306328
}
329+
330+
func deriveAESKeyFromSharedSecret(sharedSecret []byte, salt []byte, info []byte) ([]byte, error) {
331+
r := hkdf.New(sha256.New, sharedSecret, salt, info)
332+
key := make([]byte, 32)
333+
if _, err := io.ReadFull(r, key); err != nil {
334+
return nil, fmt.Errorf("hkdf: %w", err)
335+
}
336+
return key, nil
337+
}

0 commit comments

Comments
 (0)