@@ -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+
2127type EncryptRequest struct {
2228 KeyName string
2329 RemotePubKey []byte
@@ -47,40 +53,28 @@ type DeriveSharedSecretResponse struct {
4753}
4854
4955const (
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
7775type 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.
9792type UnimplementedEncryptor struct {}
9893
9994func (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
202210func (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
268283func (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