Skip to content

Commit 074a926

Browse files
authored
Merge branch 'main' into http-action-limits
2 parents 2a2ad9d + eb9ac38 commit 074a926

File tree

3 files changed

+167
-8
lines changed

3 files changed

+167
-8
lines changed

keystore/admin.go

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import (
66
"crypto/ecdsa"
77
"crypto/ed25519"
88
"crypto/rand"
9+
"encoding/json"
910
"fmt"
1011
"maps"
1112
"time"
1213

14+
gethkeystore "github.com/ethereum/go-ethereum/accounts/keystore"
1315
"golang.org/x/crypto/curve25519"
16+
"google.golang.org/protobuf/proto"
1417

1518
"github.com/ethereum/go-ethereum/crypto"
19+
1620
"github.com/smartcontractkit/chainlink-common/keystore/internal"
21+
"github.com/smartcontractkit/chainlink-common/keystore/serialization"
1722
)
1823

1924
var (
@@ -51,9 +56,9 @@ type ImportKeysRequest struct {
5156
}
5257

5358
type ImportKeyRequest struct {
54-
KeyName string
55-
KeyType KeyType
56-
Data []byte
59+
KeyName string
60+
Data []byte
61+
Password string
5762
}
5863

5964
type ImportKeysResponse struct{}
@@ -227,12 +232,90 @@ func (k *keystore) DeleteKeys(ctx context.Context, req DeleteKeysRequest) (Delet
227232
return DeleteKeysResponse{}, nil
228233
}
229234

230-
func (k *keystore) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
235+
func (ks *keystore) ImportKeys(ctx context.Context, req ImportKeysRequest) (ImportKeysResponse, error) {
236+
ks.mu.Lock()
237+
defer ks.mu.Unlock()
238+
239+
ksCopy := maps.Clone(ks.keystore)
240+
for _, keyReq := range req.Keys {
241+
if err := ValidKeyName(keyReq.KeyName); err != nil {
242+
return ImportKeysResponse{}, fmt.Errorf("%w: %s", ErrInvalidKeyName, err)
243+
}
244+
if _, ok := ksCopy[keyReq.KeyName]; ok {
245+
return ImportKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyAlreadyExists, keyReq.KeyName)
246+
}
247+
encData := gethkeystore.CryptoJSON{}
248+
err := json.Unmarshal(keyReq.Data, &encData)
249+
if err != nil {
250+
return ImportKeysResponse{}, fmt.Errorf("key = %s, failed to unmarshal encrypted import data: %w", keyReq.KeyName, err)
251+
}
252+
decData, err := gethkeystore.DecryptDataV3(encData, keyReq.Password)
253+
if err != nil {
254+
return ImportKeysResponse{}, fmt.Errorf("key = %s, failed to decrypt key: %w", keyReq.KeyName, err)
255+
}
256+
keypb := &serialization.Key{}
257+
err = proto.Unmarshal(decData, keypb)
258+
if err != nil {
259+
return ImportKeysResponse{}, fmt.Errorf("key = %s, failed to unmarshal key: %w", keyReq.KeyName, err)
260+
}
261+
pkRaw := internal.NewRaw(keypb.PrivateKey)
262+
keyType := KeyType(keypb.KeyType)
263+
publicKey, err := publicKeyFromPrivateKey(pkRaw, keyType)
264+
if err != nil {
265+
return ImportKeysResponse{}, fmt.Errorf("key = %s, failed to get public key from private key: %w", keyReq.KeyName, err)
266+
}
267+
metadata := keypb.Metadata
268+
// The proto compiler sets empty slices to nil during the serialization (https://github.com/golang/protobuf/issues/1348).
269+
// We set metadata back to empty slice to be consistent with the Create method which initializes it as such.
270+
if metadata == nil {
271+
metadata = []byte{}
272+
}
273+
ksCopy[keyReq.KeyName] = newKey(keyType, pkRaw, publicKey, time.Unix(keypb.CreatedAt, 0), metadata)
274+
}
275+
// Persist it to storage.
276+
if err := ks.save(ctx, ksCopy); err != nil {
277+
return ImportKeysResponse{}, fmt.Errorf("failed to save keystore: %w", err)
278+
}
279+
// If we succeed to save, update the in memory keystore.
280+
ks.keystore = ksCopy
231281
return ImportKeysResponse{}, nil
232282
}
233283

234-
func (k *keystore) ExportKeys(ctx context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
235-
return ExportKeysResponse{}, nil
284+
func (ks *keystore) ExportKeys(_ context.Context, req ExportKeysRequest) (ExportKeysResponse, error) {
285+
ks.mu.RLock()
286+
defer ks.mu.RUnlock()
287+
288+
result := ExportKeysResponse{}
289+
for _, keyReq := range req.Keys {
290+
key, ok := ks.keystore[keyReq.KeyName]
291+
if !ok {
292+
return ExportKeysResponse{}, fmt.Errorf("%w: %s", ErrKeyNotFound, keyReq.KeyName)
293+
}
294+
keypb := &serialization.Key{
295+
Name: keyReq.KeyName,
296+
KeyType: string(key.keyType),
297+
PrivateKey: internal.Bytes(key.privateKey),
298+
CreatedAt: key.createdAt.Unix(),
299+
Metadata: key.metadata,
300+
}
301+
serialized, err := proto.Marshal(keypb)
302+
if err != nil {
303+
return ExportKeysResponse{}, fmt.Errorf("key = %s, failed to marshal key: %w", keyReq.KeyName, err)
304+
}
305+
encData, err := gethkeystore.EncryptDataV3(serialized, []byte(keyReq.Enc.Password), keyReq.Enc.ScryptParams.N, keyReq.Enc.ScryptParams.P)
306+
if err != nil {
307+
return ExportKeysResponse{}, fmt.Errorf("key = %s, failed to encrypt key: %w", keyReq.KeyName, err)
308+
}
309+
encDataBytes, err := json.Marshal(encData)
310+
if err != nil {
311+
return ExportKeysResponse{}, fmt.Errorf("key = %s, failed to marshal encrypted key: %w", keyReq.KeyName, err)
312+
}
313+
result.Keys = append(result.Keys, ExportKeyResponse{
314+
KeyName: keyReq.KeyName,
315+
Data: encDataBytes,
316+
})
317+
}
318+
return result, nil
236319
}
237320

238321
func (ks *keystore) SetMetadata(ctx context.Context, req SetMetadataRequest) (SetMetadataResponse, error) {

keystore/admin_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,79 @@ func TestKeystore_ConcurrentCreateAndRead(t *testing.T) {
210210
require.NoError(t, err)
211211
require.Equal(t, numWriters*keysPerWriter, len(resp.Keys))
212212
}
213+
214+
func TestKeystore_ExportImport(t *testing.T) {
215+
ks1, err := keystore.LoadKeystore(t.Context(), keystore.NewMemoryStorage(), keystore.EncryptionParams{
216+
Password: "ks1",
217+
ScryptParams: keystore.FastScryptParams,
218+
})
219+
ks2, err := keystore.LoadKeystore(t.Context(), keystore.NewMemoryStorage(), keystore.EncryptionParams{
220+
Password: "ks2",
221+
ScryptParams: keystore.FastScryptParams,
222+
})
223+
224+
t.Run("export and import", func(t *testing.T) {
225+
exportParams := keystore.EncryptionParams{
226+
Password: "export-pass",
227+
ScryptParams: keystore.FastScryptParams,
228+
}
229+
_, err = ks1.CreateKeys(t.Context(), keystore.CreateKeysRequest{
230+
Keys: []keystore.CreateKeyRequest{
231+
{KeyName: "key1", KeyType: keystore.Ed25519},
232+
},
233+
})
234+
require.NoError(t, err)
235+
exportResponse, err := ks1.ExportKeys(t.Context(), keystore.ExportKeysRequest{
236+
Keys: []keystore.ExportKeyParam{
237+
{KeyName: "key1", Enc: exportParams},
238+
},
239+
})
240+
require.NoError(t, err)
241+
require.Len(t, exportResponse.Keys, 1)
242+
_, err = ks2.ImportKeys(t.Context(), keystore.ImportKeysRequest{
243+
Keys: []keystore.ImportKeyRequest{
244+
{KeyName: "key1", Password: exportParams.Password, Data: exportResponse.Keys[0].Data},
245+
},
246+
})
247+
require.NoError(t, err)
248+
key1ks1, err := ks1.GetKeys(t.Context(), keystore.GetKeysRequest{KeyNames: []string{"key1"}})
249+
require.NoError(t, err)
250+
key1ks2, err := ks2.GetKeys(t.Context(), keystore.GetKeysRequest{KeyNames: []string{"key1"}})
251+
require.Equal(t, key1ks1, key1ks2)
252+
253+
// We cannot compare private keys directly, so we test that signing with key1 from ks1 and verifying
254+
// with key1 from ks2 works as if the two keys are the same.
255+
testData := []byte("hello world")
256+
signature, err := ks2.Sign(t.Context(), keystore.SignRequest{
257+
KeyName: "key1",
258+
Data: testData,
259+
})
260+
require.NoError(t, err)
261+
verifyResp, err := ks1.Verify(t.Context(), keystore.VerifyRequest{
262+
KeyType: keystore.Ed25519,
263+
PublicKey: key1ks1.Keys[0].KeyInfo.PublicKey,
264+
Data: testData,
265+
Signature: signature.Signature,
266+
})
267+
require.NoError(t, err)
268+
require.True(t, verifyResp.Valid)
269+
})
270+
271+
t.Run("export non-existent key", func(t *testing.T) {
272+
_, err = ks1.ExportKeys(t.Context(), keystore.ExportKeysRequest{
273+
Keys: []keystore.ExportKeyParam{
274+
{KeyName: "key2", Enc: keystore.EncryptionParams{}},
275+
},
276+
})
277+
require.ErrorIs(t, err, keystore.ErrKeyNotFound)
278+
})
279+
280+
t.Run("import existing key", func(t *testing.T) {
281+
_, err = ks2.ImportKeys(t.Context(), keystore.ImportKeysRequest{
282+
Keys: []keystore.ImportKeyRequest{
283+
{KeyName: "key1", Password: "", Data: []byte{}},
284+
},
285+
})
286+
require.ErrorIs(t, err, keystore.ErrKeyAlreadyExists)
287+
})
288+
}

keystore/keystore.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ func newKey(keyType KeyType, privateKey internal.Raw, publicKey []byte, createdA
143143
}
144144
}
145145

146-
// EncryptionParams controls password-based encryption cost.
147-
// N and P are scrypt parameters; higher values increase CPU/memory cost.
146+
// EncryptionParams controls password-based encryption.
148147
// Password is the secret used to derive the encryption key.
148+
// ScryptParams control CPU/memory cost.
149149
type EncryptionParams struct {
150150
Password string
151151
ScryptParams ScryptParams

0 commit comments

Comments
 (0)