Skip to content

Commit 8283b0c

Browse files
committed
Wip encryptor
1 parent b85344b commit 8283b0c

File tree

4 files changed

+332
-9
lines changed

4 files changed

+332
-9
lines changed

keystore/admin.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package keystore
22

33
import (
44
"context"
5+
"crypto/ecdh"
56
"crypto/ecdsa"
67
"crypto/ed25519"
78
"crypto/rand"
@@ -173,6 +174,16 @@ func (ks *keystore) CreateKeys(ctx context.Context, req CreateKeysRequest) (Crea
173174
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
174175
}
175176
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey[:]), publicKey, time.Now(), []byte{})
177+
case EcdhP256:
178+
privateKey, err := ecdh.P256().GenerateKey(rand.Reader)
179+
if err != nil {
180+
return CreateKeysResponse{}, fmt.Errorf("failed to generate EcdhP256 key: %w", err)
181+
}
182+
publicKey, err := publicKeyFromPrivateKey(internal.NewRaw(privateKey.Bytes()), keyReq.KeyType)
183+
if err != nil {
184+
return CreateKeysResponse{}, fmt.Errorf("failed to get public key from private key: %w", err)
185+
}
186+
ksCopy[keyReq.KeyName] = newKey(keyReq.KeyType, internal.NewRaw(privateKey.Bytes()), publicKey, time.Now(), []byte{})
176187
default:
177188
return CreateKeysResponse{}, fmt.Errorf("%w: %s", ErrUnsupportedKeyType, keyReq.KeyType)
178189
}

keystore/encryptor.go

Lines changed: 242 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,26 @@ package keystore
22

33
import (
44
"context"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"crypto/ecdh"
8+
"crypto/rand"
9+
"crypto/sha256"
10+
"encoding/json"
511
"fmt"
12+
"io"
13+
14+
"golang.org/x/crypto/curve25519"
15+
"golang.org/x/crypto/hkdf"
16+
"golang.org/x/crypto/nacl/box"
17+
18+
"github.com/smartcontractkit/chainlink-common/keystore/internal"
619
)
720

821
type EncryptRequest struct {
9-
KeyName string
10-
Data []byte
22+
KeyName string
23+
RemotePubKey []byte
24+
Data []byte
1125
}
1226

1327
type EncryptResponse struct {
@@ -25,13 +39,50 @@ type DecryptResponse struct {
2539

2640
type DeriveSharedSecretRequest struct {
2741
LocalKeyName string
28-
RemotePubKey []byte // Maybe this naming is confusing?
42+
RemotePubKey []byte
2943
}
3044

3145
type DeriveSharedSecretResponse struct {
3246
SharedSecret []byte
3347
}
3448

49+
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]
55+
encVersionV1 byte = 1
56+
57+
algP256HKDFAESGCM = "ecdh-p256+hkdf-sha256+aes-256-gcm"
58+
)
59+
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+
}
68+
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"`
75+
}
76+
77+
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"`
84+
}
85+
3586
// Encryptor is an interfaces for hybrid encryption (key exchange + encryption) operations.
3687
// WARNING: Using the shared secret should only be used directly in
3788
// cases where very custom encryption schemes are needed and you know
@@ -57,15 +108,199 @@ func (UnimplementedEncryptor) DeriveSharedSecret(ctx context.Context, req Derive
57108
return DeriveSharedSecretResponse{}, fmt.Errorf("Encryptor.DeriveSharedSecret: %w", ErrUnimplemented)
58109
}
59110

60-
// TODO: Encryptor implementation.
61111
func (k *keystore) Encrypt(ctx context.Context, req EncryptRequest) (EncryptResponse, error) {
62-
return EncryptResponse{}, nil
112+
k.mu.RLock()
113+
defer k.mu.RUnlock()
114+
115+
key, ok := k.keystore[req.KeyName]
116+
if !ok {
117+
return EncryptResponse{}, fmt.Errorf("key not found: %s", req.KeyName)
118+
}
119+
switch key.keyType {
120+
case X25519:
121+
if len(req.RemotePubKey) != 32 {
122+
return EncryptResponse{}, fmt.Errorf("remote public key must be 32 bytes for X25519")
123+
}
124+
encrypted, err := box.SealAnonymous(nil, req.Data, (*[32]byte)(req.RemotePubKey), rand.Reader)
125+
if err != nil {
126+
return EncryptResponse{}, fmt.Errorf("failed to encrypt data: %w", err)
127+
}
128+
return EncryptResponse{
129+
EncryptedData: encrypted,
130+
}, nil
131+
case EcdhP256:
132+
curve := ecdh.P256()
133+
if len(req.RemotePubKey) == 0 {
134+
return EncryptResponse{}, fmt.Errorf("remote public key required for EcdhP256")
135+
}
136+
recipientPub, err := curve.NewPublicKey(req.RemotePubKey)
137+
if err != nil {
138+
return EncryptResponse{}, fmt.Errorf("invalid P-256 public key: %w", err)
139+
}
140+
// Ephemeral key pair
141+
ephPriv, err := curve.GenerateKey(rand.Reader)
142+
if err != nil {
143+
return EncryptResponse{}, fmt.Errorf("failed to generate ephemeral key: %w", err)
144+
}
145+
shared, err := ephPriv.ECDH(recipientPub)
146+
if err != nil {
147+
return EncryptResponse{}, fmt.Errorf("ecdh failed: %w", err)
148+
}
149+
// Derive AES-256-GCM key
150+
salt := make([]byte, hkdfSaltSize)
151+
if _, err := rand.Read(salt); err != nil {
152+
return EncryptResponse{}, fmt.Errorf("salt generation failed: %w", err)
153+
}
154+
info := []byte("keystore:ecdh-p256:aes-gcm:hkdf-sha256:v1")
155+
aeadKey, err := hkdfAESGCMKey(shared, salt, info, 32)
156+
if err != nil {
157+
return EncryptResponse{}, err
158+
}
159+
block, err := aes.NewCipher(aeadKey)
160+
if err != nil {
161+
return EncryptResponse{}, fmt.Errorf("aes: %w", err)
162+
}
163+
gcm, err := cipher.NewGCM(block)
164+
if err != nil {
165+
return EncryptResponse{}, fmt.Errorf("gcm: %w", err)
166+
}
167+
nonce := make([]byte, aesGCMNonceSize)
168+
if _, err := rand.Read(nonce); err != nil {
169+
return EncryptResponse{}, fmt.Errorf("nonce generation failed: %w", err)
170+
}
171+
ephPub := ephPriv.PublicKey().Bytes()
172+
head := encAAD{
173+
V: encVersionV1,
174+
Alg: algP256HKDFAESGCM,
175+
EPK: ephPub,
176+
Salt: salt,
177+
Nonce: nonce,
178+
}
179+
aadBytes, err := json.Marshal(head)
180+
if err != nil {
181+
return EncryptResponse{}, fmt.Errorf("aad marshal: %w", err)
182+
}
183+
ciphertext := gcm.Seal(nil, nonce, req.Data, aadBytes)
184+
env := encEnvelope{
185+
V: encVersionV1,
186+
Alg: algP256HKDFAESGCM,
187+
EPK: ephPub,
188+
Salt: salt,
189+
Nonce: nonce,
190+
CT: ciphertext,
191+
}
192+
out, err := json.Marshal(env)
193+
if err != nil {
194+
return EncryptResponse{}, fmt.Errorf("envelope marshal: %w", err)
195+
}
196+
return EncryptResponse{EncryptedData: out}, nil
197+
default:
198+
return EncryptResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
199+
}
63200
}
64201

65202
func (k *keystore) Decrypt(ctx context.Context, req DecryptRequest) (DecryptResponse, error) {
66-
return DecryptResponse{}, nil
203+
k.mu.RLock()
204+
defer k.mu.RUnlock()
205+
206+
key, ok := k.keystore[req.KeyName]
207+
if !ok {
208+
return DecryptResponse{}, fmt.Errorf("key not found: %s", req.KeyName)
209+
}
210+
switch key.keyType {
211+
case X25519:
212+
decrypted, ok := box.OpenAnonymous(nil, req.EncryptedData, (*[32]byte)(key.publicKey), (*[32]byte)(internal.Bytes(key.privateKey)))
213+
if !ok {
214+
return DecryptResponse{}, fmt.Errorf("failed to decrypt data")
215+
}
216+
return DecryptResponse{
217+
Data: decrypted,
218+
}, nil
219+
case EcdhP256:
220+
var env encEnvelope
221+
if err := json.Unmarshal(req.EncryptedData, &env); err != nil {
222+
return DecryptResponse{}, fmt.Errorf("envelope unmarshal: %w", err)
223+
}
224+
if env.V != encVersionV1 || env.Alg != algP256HKDFAESGCM {
225+
return DecryptResponse{}, fmt.Errorf("unsupported envelope version/alg")
226+
}
227+
curve := ecdh.P256()
228+
priv, err := curve.NewPrivateKey(internal.Bytes(key.privateKey))
229+
if err != nil {
230+
return DecryptResponse{}, fmt.Errorf("invalid P-256 private key: %w", err)
231+
}
232+
ephPub, err := curve.NewPublicKey(env.EPK)
233+
if err != nil {
234+
return DecryptResponse{}, fmt.Errorf("invalid P-256 ephemeral public key: %w", err)
235+
}
236+
shared, err := priv.ECDH(ephPub)
237+
if err != nil {
238+
return DecryptResponse{}, fmt.Errorf("ecdh failed: %w", err)
239+
}
240+
info := []byte("keystore:ecdh-p256:aes-gcm:hkdf-sha256:v1")
241+
aeadKey, err := hkdfAESGCMKey(shared, env.Salt, info, 32)
242+
if err != nil {
243+
return DecryptResponse{}, err
244+
}
245+
block, err := aes.NewCipher(aeadKey)
246+
if err != nil {
247+
return DecryptResponse{}, fmt.Errorf("aes: %w", err)
248+
}
249+
gcm, err := cipher.NewGCM(block)
250+
if err != nil {
251+
return DecryptResponse{}, fmt.Errorf("gcm: %w", err)
252+
}
253+
aad := encAAD{V: env.V, Alg: env.Alg, EPK: env.EPK, Salt: env.Salt, Nonce: env.Nonce}
254+
aadBytes, err := json.Marshal(aad)
255+
if err != nil {
256+
return DecryptResponse{}, fmt.Errorf("aad marshal: %w", err)
257+
}
258+
pt, err := gcm.Open(nil, env.Nonce, env.CT, aadBytes)
259+
if err != nil {
260+
return DecryptResponse{}, fmt.Errorf("gcm open: %w", err)
261+
}
262+
return DecryptResponse{Data: pt}, nil
263+
default:
264+
return DecryptResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
265+
}
67266
}
68267

69268
func (k *keystore) DeriveSharedSecret(ctx context.Context, req DeriveSharedSecretRequest) (DeriveSharedSecretResponse, error) {
70-
return DeriveSharedSecretResponse{}, nil
269+
k.mu.RLock()
270+
defer k.mu.RUnlock()
271+
272+
key, ok := k.keystore[req.LocalKeyName]
273+
if !ok {
274+
return DeriveSharedSecretResponse{}, fmt.Errorf("key not found: %s", req.LocalKeyName)
275+
}
276+
switch key.keyType {
277+
case X25519:
278+
if len(req.RemotePubKey) != 32 {
279+
return DeriveSharedSecretResponse{}, fmt.Errorf("remote public key must be 32 bytes")
280+
}
281+
sharedSecret, err := curve25519.X25519(internal.Bytes(key.privateKey), req.RemotePubKey)
282+
if err != nil {
283+
return DeriveSharedSecretResponse{}, fmt.Errorf("failed to derive shared secret: %w", err)
284+
}
285+
return DeriveSharedSecretResponse{
286+
SharedSecret: sharedSecret,
287+
}, nil
288+
case EcdhP256:
289+
curve := ecdh.P256()
290+
priv, err := curve.NewPrivateKey(internal.Bytes(key.privateKey))
291+
if err != nil {
292+
return DeriveSharedSecretResponse{}, fmt.Errorf("invalid P-256 private key: %w", err)
293+
}
294+
remotePub, err := curve.NewPublicKey(req.RemotePubKey)
295+
if err != nil {
296+
return DeriveSharedSecretResponse{}, fmt.Errorf("invalid P-256 public key: %w", err)
297+
}
298+
shared, err := priv.ECDH(remotePub)
299+
if err != nil {
300+
return DeriveSharedSecretResponse{}, fmt.Errorf("ecdh failed: %w", err)
301+
}
302+
return DeriveSharedSecretResponse{SharedSecret: shared}, nil
303+
default:
304+
return DeriveSharedSecretResponse{}, fmt.Errorf("unsupported key type: %s", key.keyType)
305+
}
71306
}

keystore/encryptor_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package keystore_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/smartcontractkit/chainlink-common/keystore"
8+
"github.com/smartcontractkit/chainlink-common/keystore/storage"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestEncryptor_X25519(t *testing.T) {
13+
ctx := context.Background()
14+
ks, err := keystore.LoadKeystore(ctx, storage.NewMemoryStorage(), keystore.EncryptionParams{
15+
Password: "test-password",
16+
ScryptParams: keystore.FastScryptParams,
17+
})
18+
require.NoError(t, err)
19+
20+
keys, err := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
21+
Keys: []keystore.CreateKeyRequest{
22+
{KeyName: "A", KeyType: keystore.X25519},
23+
{KeyName: "B", KeyType: keystore.X25519},
24+
},
25+
})
26+
require.NoError(t, err)
27+
encryptResp, err := ks.Encrypt(ctx, keystore.EncryptRequest{
28+
KeyName: "A",
29+
// Encrypt to B
30+
RemotePubKey: keys.Keys[1].KeyInfo.PublicKey,
31+
Data: []byte("hello world"),
32+
})
33+
require.NoError(t, err)
34+
decryptResp, err := ks.Decrypt(ctx, keystore.DecryptRequest{
35+
KeyName: "B",
36+
EncryptedData: encryptResp.EncryptedData,
37+
})
38+
require.NoError(t, err)
39+
require.Equal(t, []byte("hello world"), decryptResp.Data)
40+
}
41+
42+
func TestEncryptor_EcdhP256(t *testing.T) {
43+
ctx := context.Background()
44+
ks, err := keystore.LoadKeystore(ctx, storage.NewMemoryStorage(), keystore.EncryptionParams{
45+
Password: "test-password",
46+
ScryptParams: keystore.FastScryptParams,
47+
})
48+
require.NoError(t, err)
49+
keys, err := ks.CreateKeys(ctx, keystore.CreateKeysRequest{
50+
Keys: []keystore.CreateKeyRequest{
51+
{KeyName: "A", KeyType: keystore.EcdhP256},
52+
{KeyName: "B", KeyType: keystore.EcdhP256},
53+
},
54+
})
55+
require.NoError(t, err)
56+
encryptResp, err := ks.Encrypt(ctx, keystore.EncryptRequest{
57+
KeyName: "A",
58+
RemotePubKey: keys.Keys[1].KeyInfo.PublicKey,
59+
Data: []byte("hello world"),
60+
})
61+
require.NoError(t, err)
62+
decryptResp, err := ks.Decrypt(ctx, keystore.DecryptRequest{
63+
KeyName: "B",
64+
EncryptedData: encryptResp.EncryptedData,
65+
})
66+
require.NoError(t, err)
67+
require.Equal(t, []byte("hello world"), decryptResp.Data)
68+
}

0 commit comments

Comments
 (0)