Import keystore logic from light-client
This commit is contained in:
parent
d979bfc49e
commit
78bb9f9cd8
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
package cryptostore maintains everything needed for doing public-key signing and
|
||||
key management in software, based on the go-crypto library from tendermint.
|
||||
|
||||
It is flexible, and allows the user to provide a key generation algorithm
|
||||
(currently Ed25519 or Secp256k1), an encoder to passphrase-encrypt our keys
|
||||
when storing them (currently SecretBox from NaCl), and a method to persist
|
||||
the keys (currently FileStorage like ssh, or MemStorage for tests).
|
||||
It should be relatively simple to write your own implementation of these
|
||||
interfaces to match your specific security requirements.
|
||||
|
||||
Note that the private keys are never exposed outside the package, and the
|
||||
interface of Manager could be implemented by an HSM in the future for
|
||||
enhanced security. It would require a completely different implementation
|
||||
however.
|
||||
|
||||
This Manager aims to implement Signer and KeyManager interfaces, along
|
||||
with some extensions to allow importing/exporting keys and updating the
|
||||
passphrase.
|
||||
|
||||
Encoder and Generator implementations are currently in this package,
|
||||
keys.Storage implementations exist as subpackages of
|
||||
keys/storage
|
||||
*/
|
||||
package cryptostore
|
|
@ -0,0 +1,47 @@
|
|||
package cryptostore
|
||||
|
||||
import (
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
// encryptedStorage needs passphrase to get private keys
|
||||
type encryptedStorage struct {
|
||||
coder Encoder
|
||||
store keys.Storage
|
||||
}
|
||||
|
||||
func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error {
|
||||
secret, err := es.coder.Encrypt(key, pass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ki := info(name, key)
|
||||
return es.store.Put(name, secret, ki)
|
||||
}
|
||||
|
||||
func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.KeyInfo, error) {
|
||||
secret, info, err := es.store.Get(name)
|
||||
if err != nil {
|
||||
return nil, info, err
|
||||
}
|
||||
key, err := es.coder.Decrypt(secret, pass)
|
||||
return key, info, err
|
||||
}
|
||||
|
||||
func (es encryptedStorage) List() ([]keys.KeyInfo, error) {
|
||||
return es.store.List()
|
||||
}
|
||||
|
||||
func (es encryptedStorage) Delete(name string) error {
|
||||
return es.store.Delete(name)
|
||||
}
|
||||
|
||||
// info hardcodes the encoding of keys
|
||||
func info(name string, key crypto.PrivKey) keys.KeyInfo {
|
||||
return keys.KeyInfo{
|
||||
Name: name,
|
||||
PubKey: crypto.PubKeyS{key.PubKey()},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cryptostore
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
// SecretBox uses the algorithm from NaCL to store secrets securely
|
||||
SecretBox Encoder = secretbox{}
|
||||
// Noop doesn't do any encryption, should only be used in test code
|
||||
Noop Encoder = noop{}
|
||||
)
|
||||
|
||||
// Encoder is used to encrypt any key with a passphrase for storage.
|
||||
//
|
||||
// This should use a well-designed symetric encryption algorithm
|
||||
type Encoder interface {
|
||||
Encrypt(key crypto.PrivKey, pass string) ([]byte, error)
|
||||
Decrypt(data []byte, pass string) (crypto.PrivKey, error)
|
||||
}
|
||||
|
||||
func secret(passphrase string) []byte {
|
||||
// TODO: Sha256(Bcrypt(passphrase))
|
||||
return crypto.Sha256([]byte(passphrase))
|
||||
}
|
||||
|
||||
type secretbox struct{}
|
||||
|
||||
func (e secretbox) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) {
|
||||
s := secret(pass)
|
||||
cipher := crypto.EncryptSymmetric(key.Bytes(), s)
|
||||
return cipher, nil
|
||||
}
|
||||
|
||||
func (e secretbox) Decrypt(data []byte, pass string) (crypto.PrivKey, error) {
|
||||
s := secret(pass)
|
||||
private, err := crypto.DecryptSymmetric(data, s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Invalid Passphrase")
|
||||
}
|
||||
key, err := crypto.PrivKeyFromBytes(private)
|
||||
return key, errors.Wrap(err, "Invalid Passphrase")
|
||||
}
|
||||
|
||||
type noop struct{}
|
||||
|
||||
func (n noop) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) {
|
||||
return key.Bytes(), nil
|
||||
}
|
||||
|
||||
func (n noop) Decrypt(data []byte, pass string) (crypto.PrivKey, error) {
|
||||
return crypto.PrivKeyFromBytes(data)
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package cryptostore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/go-keys/cryptostore"
|
||||
)
|
||||
|
||||
func TestNoopEncoder(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
noop := cryptostore.Noop
|
||||
|
||||
key := cryptostore.GenEd25519.Generate()
|
||||
key2 := cryptostore.GenSecp256k1.Generate()
|
||||
|
||||
b, err := noop.Encrypt(key, "encode")
|
||||
require.Nil(err)
|
||||
assert.NotEmpty(b)
|
||||
|
||||
b2, err := noop.Encrypt(key2, "encode")
|
||||
require.Nil(err)
|
||||
assert.NotEmpty(b2)
|
||||
assert.NotEqual(b, b2)
|
||||
|
||||
// note the decode with a different password works - not secure!
|
||||
pk, err := noop.Decrypt(b, "decode")
|
||||
require.Nil(err)
|
||||
require.NotNil(pk)
|
||||
assert.Equal(key, pk)
|
||||
|
||||
pk2, err := noop.Decrypt(b2, "kggugougp")
|
||||
require.Nil(err)
|
||||
require.NotNil(pk2)
|
||||
assert.Equal(key2, pk2)
|
||||
}
|
||||
|
||||
func TestSecretBox(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
enc := cryptostore.SecretBox
|
||||
|
||||
key := cryptostore.GenEd25519.Generate()
|
||||
pass := "some-special-secret"
|
||||
|
||||
b, err := enc.Encrypt(key, pass)
|
||||
require.Nil(err)
|
||||
assert.NotEmpty(b)
|
||||
|
||||
// decoding with a different pass is an error
|
||||
pk, err := enc.Decrypt(b, "decode")
|
||||
require.NotNil(err)
|
||||
require.Nil(pk)
|
||||
|
||||
// but decoding with the same passphrase gets us our key
|
||||
pk, err = enc.Decrypt(b, pass)
|
||||
require.Nil(err)
|
||||
assert.Equal(key, pk)
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package cryptostore
|
||||
|
||||
import crypto "github.com/tendermint/go-crypto"
|
||||
|
||||
var (
|
||||
// GenEd25519 produces Ed25519 private keys
|
||||
GenEd25519 Generator = GenFunc(genEd25519)
|
||||
// GenSecp256k1 produces Secp256k1 private keys
|
||||
GenSecp256k1 Generator = GenFunc(genSecp256)
|
||||
)
|
||||
|
||||
// Generator determines the type of private key the keystore creates
|
||||
type Generator interface {
|
||||
Generate() crypto.PrivKey
|
||||
}
|
||||
|
||||
// GenFunc is a helper to transform a function into a Generator
|
||||
type GenFunc func() crypto.PrivKey
|
||||
|
||||
func (f GenFunc) Generate() crypto.PrivKey {
|
||||
return f()
|
||||
}
|
||||
|
||||
func genEd25519() crypto.PrivKey {
|
||||
return crypto.GenPrivKeyEd25519()
|
||||
}
|
||||
|
||||
func genSecp256() crypto.PrivKey {
|
||||
return crypto.GenPrivKeySecp256k1()
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
package cryptostore
|
||||
|
||||
import keys "github.com/tendermint/go-keys"
|
||||
|
||||
// Manager combines encyption and storage implementation to provide
|
||||
// a full-featured key manager
|
||||
type Manager struct {
|
||||
gen Generator
|
||||
es encryptedStorage
|
||||
}
|
||||
|
||||
func New(gen Generator, coder Encoder, store keys.Storage) Manager {
|
||||
return Manager{
|
||||
gen: gen,
|
||||
es: encryptedStorage{
|
||||
coder: coder,
|
||||
store: store,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// exists just to make sure we fulfill the Signer interface
|
||||
func (s Manager) assertSigner() keys.Signer {
|
||||
return s
|
||||
}
|
||||
|
||||
// exists just to make sure we fulfill the KeyManager interface
|
||||
func (s Manager) assertKeyManager() keys.KeyManager {
|
||||
return s
|
||||
}
|
||||
|
||||
// Create adds a new key to the storage engine, returning error if
|
||||
// another key already stored under this name
|
||||
func (s Manager) Create(name, passphrase string) error {
|
||||
key := s.gen.Generate()
|
||||
return s.es.Put(name, passphrase, key)
|
||||
}
|
||||
|
||||
// List loads the keys from the storage and enforces alphabetical order
|
||||
func (s Manager) List() (keys.KeyInfos, error) {
|
||||
k, err := s.es.List()
|
||||
res := keys.KeyInfos(k)
|
||||
res.Sort()
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Get returns the public information about one key
|
||||
func (s Manager) Get(name string) (keys.KeyInfo, error) {
|
||||
_, info, err := s.es.store.Get(name)
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Sign will modify the Signable in order to attach a valid signature with
|
||||
// this public key
|
||||
//
|
||||
// If no key for this name, or the passphrase doesn't match, returns an error
|
||||
func (s Manager) Sign(name, passphrase string, tx keys.Signable) error {
|
||||
key, _, err := s.es.Get(name, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sig := key.Sign(tx.SignBytes())
|
||||
pubkey := key.PubKey()
|
||||
return tx.Sign(pubkey, sig)
|
||||
}
|
||||
|
||||
// Export decodes the private key with the current password, encodes
|
||||
// it with a secure one-time password and generates a sequence that can be
|
||||
// Imported by another Manager
|
||||
//
|
||||
// This is designed to copy from one device to another, or provide backups
|
||||
// during version updates.
|
||||
func (s Manager) Export(name, oldpass, transferpass string) ([]byte, error) {
|
||||
key, _, err := s.es.Get(name, oldpass)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.es.coder.Encrypt(key, transferpass)
|
||||
return res, err
|
||||
}
|
||||
|
||||
// Import accepts bytes generated by Export along with the same transferpass
|
||||
// If they are valid, it stores the password under the given name with the
|
||||
// new passphrase.
|
||||
func (s Manager) Import(name, newpass, transferpass string, data []byte) error {
|
||||
key, err := s.es.coder.Decrypt(data, transferpass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.es.Put(name, newpass, key)
|
||||
}
|
||||
|
||||
// Delete removes key forever, but we must present the
|
||||
// proper passphrase before deleting it (for security)
|
||||
func (s Manager) Delete(name, passphrase string) error {
|
||||
// verify we have the proper password before deleting
|
||||
_, _, err := s.es.Get(name, passphrase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.es.Delete(name)
|
||||
}
|
||||
|
||||
// Update changes the passphrase with which a already stored key is encoded.
|
||||
//
|
||||
// oldpass must be the current passphrase used for encoding, newpass will be
|
||||
// the only valid passphrase from this time forward
|
||||
func (s Manager) Update(name, oldpass, newpass string) error {
|
||||
key, _, err := s.es.Get(name, oldpass)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// we must delete first, as Putting over an existing name returns an error
|
||||
s.Delete(name, oldpass)
|
||||
|
||||
return s.es.Put(name, newpass, key)
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
package cryptostore_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tendermint/go-keys/cryptostore"
|
||||
"github.com/tendermint/go-keys/storage/memstorage"
|
||||
)
|
||||
|
||||
// TestKeyManagement makes sure we can manipulate these keys well
|
||||
func TestKeyManagement(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// make the storage with reasonable defaults
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.GenSecp256k1,
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
)
|
||||
|
||||
n1, n2, n3 := "personal", "business", "other"
|
||||
p1, p2 := "1234", "really-secure!@#$"
|
||||
|
||||
// Check empty state
|
||||
l, err := cstore.List()
|
||||
require.Nil(err)
|
||||
assert.Empty(l)
|
||||
|
||||
// create some keys
|
||||
_, err = cstore.Get(n1)
|
||||
assert.NotNil(err)
|
||||
err = cstore.Create(n1, p1)
|
||||
require.Nil(err)
|
||||
err = cstore.Create(n2, p2)
|
||||
require.Nil(err)
|
||||
|
||||
// we can get these keys
|
||||
i2, err := cstore.Get(n2)
|
||||
assert.Nil(err)
|
||||
_, err = cstore.Get(n3)
|
||||
assert.NotNil(err)
|
||||
|
||||
// list shows them in order
|
||||
keys, err := cstore.List()
|
||||
require.Nil(err)
|
||||
require.Equal(2, len(keys))
|
||||
// note these are in alphabetical order
|
||||
assert.Equal(n2, keys[0].Name)
|
||||
assert.Equal(n1, keys[1].Name)
|
||||
assert.Equal(i2.PubKey, keys[0].PubKey)
|
||||
|
||||
// deleting a key removes it
|
||||
err = cstore.Delete("bad name", "foo")
|
||||
require.NotNil(err)
|
||||
err = cstore.Delete(n1, p1)
|
||||
require.Nil(err)
|
||||
keys, err = cstore.List()
|
||||
require.Nil(err)
|
||||
assert.Equal(1, len(keys))
|
||||
_, err = cstore.Get(n1)
|
||||
assert.NotNil(err)
|
||||
|
||||
// make sure that it only signs with the right password
|
||||
// tx := mock.NewSig([]byte("mytransactiondata"))
|
||||
// err = cstore.Sign(n2, p1, tx)
|
||||
// assert.NotNil(err)
|
||||
// err = cstore.Sign(n2, p2, tx)
|
||||
// assert.Nil(err, "%+v", err)
|
||||
// sigs, err := tx.Signers()
|
||||
// assert.Nil(err, "%+v", err)
|
||||
// if assert.Equal(1, len(sigs)) {
|
||||
// assert.Equal(i2.PubKey, sigs[0])
|
||||
// }
|
||||
}
|
||||
|
||||
// TestSignVerify does some detailed checks on how we sign and validate
|
||||
// signatures
|
||||
// func TestSignVerify(t *testing.T) {
|
||||
// assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// // make the storage with reasonable defaults
|
||||
// cstore := cryptostore.New(
|
||||
// cryptostore.GenSecp256k1,
|
||||
// cryptostore.SecretBox,
|
||||
// memstorage.New(),
|
||||
// )
|
||||
|
||||
// n1, n2 := "some dude", "a dudette"
|
||||
// p1, p2 := "1234", "foobar"
|
||||
|
||||
// // create two users and get their info
|
||||
// err := cstore.Create(n1, p1)
|
||||
// require.Nil(err)
|
||||
// i1, err := cstore.Get(n1)
|
||||
// require.Nil(err)
|
||||
|
||||
// err = cstore.Create(n2, p2)
|
||||
// require.Nil(err)
|
||||
// i2, err := cstore.Get(n2)
|
||||
// require.Nil(err)
|
||||
|
||||
// // let's try to sign some messages
|
||||
// d1 := []byte("my first message")
|
||||
// d2 := []byte("some other important info!")
|
||||
|
||||
// // try signing both data with both keys...
|
||||
// s11, err := cstore.Signature(n1, p1, d1)
|
||||
// require.Nil(err)
|
||||
// s12, err := cstore.Signature(n1, p1, d2)
|
||||
// require.Nil(err)
|
||||
// s21, err := cstore.Signature(n2, p2, d1)
|
||||
// require.Nil(err)
|
||||
// s22, err := cstore.Signature(n2, p2, d2)
|
||||
// require.Nil(err)
|
||||
|
||||
// // let's try to validate and make sure it only works when everything is proper
|
||||
// keys := [][]byte{i1.PubKey, i2.PubKey}
|
||||
// data := [][]byte{d1, d2}
|
||||
// sigs := [][]byte{s11, s12, s21, s22}
|
||||
|
||||
// // loop over keys and data
|
||||
// for k := 0; k < 2; k++ {
|
||||
// for d := 0; d < 2; d++ {
|
||||
// // make sure only the proper sig works
|
||||
// good := 2*k + d
|
||||
// for s := 0; s < 4; s++ {
|
||||
// err = cstore.Verify(data[d], sigs[s], keys[k])
|
||||
// if s == good {
|
||||
// assert.Nil(err, "%+v", err)
|
||||
// } else {
|
||||
// assert.NotNil(err)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func assertPassword(assert *assert.Assertions, cstore cryptostore.Manager, name, pass, badpass string) {
|
||||
err := cstore.Update(name, badpass, pass)
|
||||
assert.NotNil(err)
|
||||
err = cstore.Update(name, pass, pass)
|
||||
assert.Nil(err, "%+v", err)
|
||||
}
|
||||
|
||||
// TestAdvancedKeyManagement verifies update, import, export functionality
|
||||
func TestAdvancedKeyManagement(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// make the storage with reasonable defaults
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.GenSecp256k1,
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
)
|
||||
|
||||
n1, n2 := "old-name", "new name"
|
||||
p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$"
|
||||
|
||||
// make sure key works with initial password
|
||||
err := cstore.Create(n1, p1)
|
||||
require.Nil(err, "%+v", err)
|
||||
assertPassword(assert, cstore, n1, p1, p2)
|
||||
|
||||
// update password requires the existing password
|
||||
err = cstore.Update(n1, "jkkgkg", p2)
|
||||
assert.NotNil(err)
|
||||
assertPassword(assert, cstore, n1, p1, p2)
|
||||
|
||||
// then it changes the password when correct
|
||||
err = cstore.Update(n1, p1, p2)
|
||||
assert.Nil(err)
|
||||
// p2 is now the proper one!
|
||||
assertPassword(assert, cstore, n1, p2, p1)
|
||||
|
||||
// exporting requires the proper name and passphrase
|
||||
_, err = cstore.Export(n2, p2, pt)
|
||||
assert.NotNil(err)
|
||||
_, err = cstore.Export(n1, p1, pt)
|
||||
assert.NotNil(err)
|
||||
exported, err := cstore.Export(n1, p2, pt)
|
||||
require.Nil(err, "%+v", err)
|
||||
|
||||
// import fails on bad transfer pass
|
||||
err = cstore.Import(n2, p3, p2, exported)
|
||||
assert.NotNil(err)
|
||||
// import cannot overwrite existing keys
|
||||
err = cstore.Import(n1, p3, pt, exported)
|
||||
assert.NotNil(err)
|
||||
// we can now import under another name
|
||||
err = cstore.Import(n2, p3, pt, exported)
|
||||
require.Nil(err, "%+v", err)
|
||||
|
||||
// make sure both passwords are now properly set (not to the transfer pass)
|
||||
assertPassword(assert, cstore, n1, p2, pt)
|
||||
assertPassword(assert, cstore, n2, p3, pt)
|
||||
}
|
||||
|
||||
// func ExampleStore() {
|
||||
// // Select the encryption and storage for your cryptostore
|
||||
// cstore := cryptostore.New(
|
||||
// cryptostore.GenEd25519,
|
||||
// cryptostore.SecretBox,
|
||||
// // Note: use filestorage.New(dir) for real data
|
||||
// memstorage.New(),
|
||||
// )
|
||||
|
||||
// // Add keys and see they return in alphabetical order
|
||||
// cstore.Create("Bob", "friend")
|
||||
// cstore.Create("Alice", "secret")
|
||||
// cstore.Create("Carl", "mitm")
|
||||
// info, _ := cstore.List()
|
||||
// for _, i := range info {
|
||||
// fmt.Println(i.Name)
|
||||
// }
|
||||
|
||||
// // We need to use passphrase to generate a signature
|
||||
// tx := mock.NewSig([]byte("deadbeef"))
|
||||
// err := cstore.Sign("Bob", "friend", tx)
|
||||
// if err != nil {
|
||||
// fmt.Println("don't accept real passphrase")
|
||||
// }
|
||||
|
||||
// // and we can validate the signature with publically available info
|
||||
// binfo, _ := cstore.Get("Bob")
|
||||
// sigs, err := tx.Signers()
|
||||
// if err != nil {
|
||||
// fmt.Println("badly signed")
|
||||
// } else if bytes.Equal(sigs[0].Bytes(), binfo.PubKey.Bytes()) {
|
||||
// fmt.Println("signed by Bob")
|
||||
// } else {
|
||||
// fmt.Println("signed by someone else")
|
||||
// }
|
||||
|
||||
// // Output:
|
||||
// // Alice
|
||||
// // Bob
|
||||
// // Carl
|
||||
// // signed by Bob
|
||||
// }
|
|
@ -0,0 +1,41 @@
|
|||
package cryptostore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
func TestSortKeys(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
gen := GenEd25519.Generate
|
||||
assert.NotEqual(gen(), gen())
|
||||
|
||||
// alphabetical order is n3, n1, n2
|
||||
n1, n2, n3 := "john", "mike", "alice"
|
||||
infos := keys.KeyInfos{
|
||||
info(n1, gen()),
|
||||
info(n2, gen()),
|
||||
info(n3, gen()),
|
||||
}
|
||||
|
||||
// make sure they are initialized unsorted
|
||||
assert.Equal(n1, infos[0].Name)
|
||||
assert.Equal(n2, infos[1].Name)
|
||||
assert.Equal(n3, infos[2].Name)
|
||||
|
||||
// now they are sorted
|
||||
infos.Sort()
|
||||
assert.Equal(n3, infos[0].Name)
|
||||
assert.Equal(n1, infos[1].Name)
|
||||
assert.Equal(n2, infos[2].Name)
|
||||
|
||||
// make sure info put some real data there...
|
||||
assert.NotEmpty(infos[0].PubKey)
|
||||
assert.NotEmpty(infos[0].PubKey.Address())
|
||||
assert.NotEmpty(infos[1].PubKey)
|
||||
assert.NotEmpty(infos[1].PubKey.Address())
|
||||
assert.NotEqual(infos[0].PubKey, infos[1].PubKey)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package keys
|
||||
|
||||
// Storage has many implementation, based on security and sharing requirements
|
||||
// like disk-backed, mem-backed, vault, db, etc.
|
||||
type Storage interface {
|
||||
Put(name string, key []byte, info KeyInfo) error
|
||||
Get(name string) ([]byte, KeyInfo, error)
|
||||
List() ([]KeyInfo, error)
|
||||
Delete(name string) error
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
package filestorage provides a secure on-disk storage of private keys and
|
||||
metadata. Security is enforced by file and directory permissions, much
|
||||
like standard ssh key storage.
|
||||
*/
|
||||
package filestorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
const (
|
||||
BlockType = "Tendermint Light Client"
|
||||
PrivExt = "tlc"
|
||||
PubExt = "pub"
|
||||
keyPerm = os.FileMode(0600)
|
||||
pubPerm = os.FileMode(0644)
|
||||
dirPerm = os.FileMode(0700)
|
||||
)
|
||||
|
||||
type FileStore struct {
|
||||
keyDir string
|
||||
}
|
||||
|
||||
// New creates an instance of file-based key storage with tight permissions
|
||||
//
|
||||
// dir should be an absolute path of a directory owner by this user. It will
|
||||
// be created if it doesn't exist already.
|
||||
func New(dir string) FileStore {
|
||||
err := os.Mkdir(dir, dirPerm)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
panic(err)
|
||||
}
|
||||
return FileStore{dir}
|
||||
}
|
||||
|
||||
// assertStorage just makes sure we implement the proper Storage interface
|
||||
func (s FileStore) assertStorage() keys.Storage {
|
||||
return s
|
||||
}
|
||||
|
||||
// Put creates two files, one with the public info as json, the other
|
||||
// with the (encoded) private key as gpg ascii-armor style
|
||||
func (s FileStore) Put(name string, key []byte, info keys.KeyInfo) error {
|
||||
pub, priv := s.nameToPaths(name)
|
||||
|
||||
// write public info
|
||||
err := writeInfo(pub, info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write private info
|
||||
return write(priv, name, key)
|
||||
}
|
||||
|
||||
// Get loads the keyinfo and (encoded) private key from the directory
|
||||
// It uses `name` to generate the filename, and returns an error if the
|
||||
// files don't exist or are in the incorrect format
|
||||
func (s FileStore) Get(name string) ([]byte, keys.KeyInfo, error) {
|
||||
pub, priv := s.nameToPaths(name)
|
||||
|
||||
info, err := readInfo(pub)
|
||||
if err != nil {
|
||||
return nil, info, err
|
||||
}
|
||||
|
||||
key, _, err := read(priv)
|
||||
return key, info, err
|
||||
}
|
||||
|
||||
// List parses the key directory for public info and returns a list of
|
||||
// KeyInfo for all keys located in this directory.
|
||||
func (s FileStore) List() ([]keys.KeyInfo, error) {
|
||||
dir, err := os.Open(s.keyDir)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "List Keys")
|
||||
}
|
||||
names, err := dir.Readdirnames(0)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "List Keys")
|
||||
}
|
||||
|
||||
// filter names for .pub ending and load them one by one
|
||||
// half the files is a good guess for pre-allocating the slice
|
||||
infos := make([]keys.KeyInfo, 0, len(names)/2)
|
||||
for _, name := range names {
|
||||
if strings.HasSuffix(name, PubExt) {
|
||||
p := path.Join(s.keyDir, name)
|
||||
info, err := readInfo(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = append(infos, info)
|
||||
}
|
||||
}
|
||||
|
||||
return infos, nil
|
||||
}
|
||||
|
||||
// Delete permanently removes the public and private info for the named key
|
||||
// The calling function should provide some security checks first.
|
||||
func (s FileStore) Delete(name string) error {
|
||||
pub, priv := s.nameToPaths(name)
|
||||
err := os.Remove(priv)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Deleting Private Key")
|
||||
}
|
||||
err = os.Remove(pub)
|
||||
return errors.Wrap(err, "Deleting Public Key")
|
||||
}
|
||||
|
||||
func (s FileStore) nameToPaths(name string) (pub, priv string) {
|
||||
privName := fmt.Sprintf("%s.%s", name, PrivExt)
|
||||
pubName := fmt.Sprintf("%s.%s", name, PubExt)
|
||||
return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName)
|
||||
}
|
||||
|
||||
func writeInfo(path string, info keys.KeyInfo) error {
|
||||
return write(path, info.Name, info.PubKey.Bytes())
|
||||
}
|
||||
|
||||
func readInfo(path string) (info keys.KeyInfo, err error) {
|
||||
var data []byte
|
||||
data, info.Name, err = read(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pk, err := crypto.PubKeyFromBytes(data)
|
||||
info.PubKey = crypto.PubKeyS{pk}
|
||||
return
|
||||
}
|
||||
|
||||
func read(path string) ([]byte, string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "Reading data")
|
||||
}
|
||||
d, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "Reading data")
|
||||
}
|
||||
block, headers, key, err := crypto.DecodeArmor(string(d))
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, "Invalid Armor")
|
||||
}
|
||||
if block != BlockType {
|
||||
return nil, "", errors.Errorf("Unknown key type: %s", block)
|
||||
}
|
||||
return key, headers["name"], nil
|
||||
}
|
||||
|
||||
func write(path, name string, key []byte) error {
|
||||
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Writing data")
|
||||
}
|
||||
defer f.Close()
|
||||
headers := map[string]string{"name": name}
|
||||
text := crypto.EncodeArmor(BlockType, headers, key)
|
||||
_, err = f.WriteString(text)
|
||||
return errors.Wrap(err, "Writing data")
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package filestorage
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
func TestBasicCRUD(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
dir, err := ioutil.TempDir("", "filestorage-test")
|
||||
assert.Nil(err)
|
||||
defer os.RemoveAll(dir)
|
||||
store := New(dir)
|
||||
|
||||
name := "bar"
|
||||
key := []byte("secret-key-here")
|
||||
pubkey := crypto.GenPrivKeyEd25519().PubKey()
|
||||
info := keys.KeyInfo{
|
||||
Name: name,
|
||||
PubKey: crypto.PubKeyS{pubkey},
|
||||
}
|
||||
|
||||
// No data: Get and Delete return nothing
|
||||
_, _, err = store.Get(name)
|
||||
assert.NotNil(err)
|
||||
err = store.Delete(name)
|
||||
assert.NotNil(err)
|
||||
// List returns empty list
|
||||
l, err := store.List()
|
||||
assert.Nil(err)
|
||||
assert.Empty(l)
|
||||
|
||||
// Putting the key in the store must work
|
||||
err = store.Put(name, key, info)
|
||||
assert.Nil(err)
|
||||
// But a second time is a failure
|
||||
err = store.Put(name, key, info)
|
||||
assert.NotNil(err)
|
||||
|
||||
// Now, we can get and list properly
|
||||
k, i, err := store.Get(name)
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(key, k)
|
||||
assert.Equal(info, i)
|
||||
l, err = store.List()
|
||||
require.Nil(err, "%+v", err)
|
||||
assert.Equal(1, len(l))
|
||||
assert.Equal(info, l[0])
|
||||
|
||||
// querying a non-existent key fails
|
||||
_, _, err = store.Get("badname")
|
||||
assert.NotNil(err)
|
||||
|
||||
// We can only delete once
|
||||
err = store.Delete(name)
|
||||
assert.Nil(err)
|
||||
err = store.Delete(name)
|
||||
assert.NotNil(err)
|
||||
|
||||
// and then Get and List don't work
|
||||
_, _, err = store.Get(name)
|
||||
assert.NotNil(err)
|
||||
// List returns empty list
|
||||
l, err = store.List()
|
||||
assert.Nil(err)
|
||||
assert.Empty(l)
|
||||
}
|
||||
|
||||
func TestDirectoryHandling(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// prepare a temp dir and make sure it is not there
|
||||
newDir := path.Join(os.TempDir(), "file-test-dir")
|
||||
_, err := os.Open(newDir)
|
||||
assert.True(os.IsNotExist(err))
|
||||
|
||||
// create a new storage, and verify it creates the directory with good permissions
|
||||
New(newDir)
|
||||
defer os.RemoveAll(newDir)
|
||||
d, err := os.Open(newDir)
|
||||
require.Nil(err)
|
||||
defer d.Close()
|
||||
|
||||
stat, err := d.Stat()
|
||||
require.Nil(err)
|
||||
assert.Equal(dirPerm, stat.Mode()&os.ModePerm)
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
package memstorage provides a simple in-memory key store designed for
|
||||
use in test cases, particularly to isolate them from the filesystem,
|
||||
concurrency, and cleanup issues.
|
||||
*/
|
||||
package memstorage
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
type data struct {
|
||||
info keys.KeyInfo
|
||||
key []byte
|
||||
}
|
||||
|
||||
type MemStore map[string]data
|
||||
|
||||
// New creates an instance of file-based key storage with tight permissions
|
||||
func New() MemStore {
|
||||
return MemStore{}
|
||||
}
|
||||
|
||||
// assertStorage just makes sure we implement the Storage interface
|
||||
func (s MemStore) assertStorage() keys.Storage {
|
||||
return s
|
||||
}
|
||||
|
||||
// Put adds the given key, returns an error if it another key
|
||||
// is already stored under this name
|
||||
func (s MemStore) Put(name string, key []byte, info keys.KeyInfo) error {
|
||||
if _, ok := s[name]; ok {
|
||||
return errors.Errorf("Key named '%s' already exists", name)
|
||||
}
|
||||
s[name] = data{info, key}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the key stored under the name, or returns an error if not present
|
||||
func (s MemStore) Get(name string) ([]byte, keys.KeyInfo, error) {
|
||||
var err error
|
||||
d, ok := s[name]
|
||||
if !ok {
|
||||
err = errors.Errorf("Key named '%s' doesn't exist", name)
|
||||
}
|
||||
return d.key, d.info, err
|
||||
}
|
||||
|
||||
// List returns the public info of all keys in the MemStore in unsorted order
|
||||
func (s MemStore) List() ([]keys.KeyInfo, error) {
|
||||
res := make([]keys.KeyInfo, len(s))
|
||||
i := 0
|
||||
for _, d := range s {
|
||||
res[i] = d.info
|
||||
i++
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Delete removes the named key from the MemStore, raising an error if it
|
||||
// wasn't present yet.
|
||||
func (s MemStore) Delete(name string) error {
|
||||
_, ok := s[name]
|
||||
if !ok {
|
||||
return errors.Errorf("Key named '%s' doesn't exist", name)
|
||||
}
|
||||
delete(s, name)
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package memstorage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
keys "github.com/tendermint/go-keys"
|
||||
)
|
||||
|
||||
func TestBasicCRUD(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
store := New()
|
||||
|
||||
name := "foo"
|
||||
key := []byte("secret-key-here")
|
||||
pubkey := crypto.GenPrivKeyEd25519().PubKey()
|
||||
info := keys.KeyInfo{
|
||||
Name: name,
|
||||
PubKey: crypto.PubKeyS{pubkey},
|
||||
}
|
||||
|
||||
// No data: Get and Delete return nothing
|
||||
_, _, err := store.Get(name)
|
||||
assert.NotNil(err)
|
||||
err = store.Delete(name)
|
||||
assert.NotNil(err)
|
||||
// List returns empty list
|
||||
l, err := store.List()
|
||||
assert.Nil(err)
|
||||
assert.Empty(l)
|
||||
|
||||
// Putting the key in the store must work
|
||||
err = store.Put(name, key, info)
|
||||
assert.Nil(err)
|
||||
// But a second time is a failure
|
||||
err = store.Put(name, key, info)
|
||||
assert.NotNil(err)
|
||||
|
||||
// Now, we can get and list properly
|
||||
k, i, err := store.Get(name)
|
||||
assert.Nil(err)
|
||||
assert.Equal(key, k)
|
||||
assert.Equal(info, i)
|
||||
l, err = store.List()
|
||||
assert.Nil(err)
|
||||
assert.Equal(1, len(l))
|
||||
assert.Equal(info, l[0])
|
||||
|
||||
// querying a non-existent key fails
|
||||
_, _, err = store.Get("badname")
|
||||
assert.NotNil(err)
|
||||
|
||||
// We can only delete once
|
||||
err = store.Delete(name)
|
||||
assert.Nil(err)
|
||||
err = store.Delete(name)
|
||||
assert.NotNil(err)
|
||||
|
||||
// and then Get and List don't work
|
||||
_, _, err = store.Get(name)
|
||||
assert.NotNil(err)
|
||||
// List returns empty list
|
||||
l, err = store.List()
|
||||
assert.Nil(err)
|
||||
assert.Empty(l)
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package keys
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
crypto "github.com/tendermint/go-crypto"
|
||||
)
|
||||
|
||||
// KeyInfo is the public information about a key
|
||||
type KeyInfo struct {
|
||||
Name string
|
||||
PubKey crypto.PubKeyS
|
||||
}
|
||||
|
||||
// KeyInfos is a wrapper to allows alphabetical sorting of the keys
|
||||
type KeyInfos []KeyInfo
|
||||
|
||||
func (k KeyInfos) Len() int { return len(k) }
|
||||
func (k KeyInfos) Less(i, j int) bool { return k[i].Name < k[j].Name }
|
||||
func (k KeyInfos) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
|
||||
func (k KeyInfos) Sort() {
|
||||
if k != nil {
|
||||
sort.Sort(k)
|
||||
}
|
||||
}
|
||||
|
||||
// Signable represents any transaction we wish to send to tendermint core
|
||||
// These methods allow us to sign arbitrary Tx with the KeyStore
|
||||
type Signable interface {
|
||||
// SignBytes is the immutable data, which needs to be signed
|
||||
SignBytes() []byte
|
||||
|
||||
// Sign will add a signature and pubkey.
|
||||
//
|
||||
// Depending on the Signable, one may be able to call this multiple times for multisig
|
||||
// Returns error if called with invalid data or too many times
|
||||
Sign(pubkey crypto.PubKey, sig crypto.Signature) error
|
||||
|
||||
// Signers will return the public key(s) that signed if the signature
|
||||
// is valid, or an error if there is any issue with the signature,
|
||||
// including if there are no signatures
|
||||
Signers() ([]crypto.PubKey, error)
|
||||
|
||||
// TxBytes returns the transaction data as well as all signatures
|
||||
// It should return an error if Sign was never called
|
||||
TxBytes() ([]byte, error)
|
||||
}
|
||||
|
||||
// Signer allows one to use a keystore to sign transactions
|
||||
type Signer interface {
|
||||
Sign(name, passphrase string, tx Signable) error
|
||||
}
|
||||
|
||||
// KeyManager allows simple CRUD on a keystore, as an aid to signing
|
||||
type KeyManager interface {
|
||||
Create(name, passphrase string) error
|
||||
List() (KeyInfos, error)
|
||||
Get(name string) (KeyInfo, error)
|
||||
Update(name, oldpass, newpass string) error
|
||||
Delete(name, passphrase string) error
|
||||
}
|
Loading…
Reference in New Issue