mirror of
https://github.com/fluencelabs/tendermint
synced 2025-06-28 20:21:47 +00:00
Import keystore logic from light-client
This commit is contained in:
25
cryptostore/docs.go
Normal file
25
cryptostore/docs.go
Normal file
@ -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
|
47
cryptostore/enc_storage.go
Normal file
47
cryptostore/enc_storage.go
Normal file
@ -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()},
|
||||||
|
}
|
||||||
|
}
|
54
cryptostore/encoder.go
Normal file
54
cryptostore/encoder.go
Normal file
@ -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)
|
||||||
|
}
|
59
cryptostore/encoder_test.go
Normal file
59
cryptostore/encoder_test.go
Normal file
@ -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)
|
||||||
|
}
|
30
cryptostore/generator.go
Normal file
30
cryptostore/generator.go
Normal file
@ -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()
|
||||||
|
}
|
120
cryptostore/holder.go
Normal file
120
cryptostore/holder.go
Normal file
@ -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)
|
||||||
|
}
|
241
cryptostore/holder_test.go
Normal file
241
cryptostore/holder_test.go
Normal file
@ -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
|
||||||
|
// }
|
41
cryptostore/storage_test.go
Normal file
41
cryptostore/storage_test.go
Normal file
@ -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)
|
||||||
|
}
|
10
storage.go
Normal file
10
storage.go
Normal file
@ -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
|
||||||
|
}
|
171
storage/filestorage/main.go
Normal file
171
storage/filestorage/main.go
Normal file
@ -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")
|
||||||
|
}
|
95
storage/filestorage/main_test.go
Normal file
95
storage/filestorage/main_test.go
Normal file
@ -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)
|
||||||
|
}
|
70
storage/memstorage/main.go
Normal file
70
storage/memstorage/main.go
Normal file
@ -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
|
||||||
|
}
|
67
storage/memstorage/main_test.go
Normal file
67
storage/memstorage/main_test.go
Normal file
@ -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)
|
||||||
|
}
|
61
transactions.go
Normal file
61
transactions.go
Normal file
@ -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
|
||||||
|
}
|
Reference in New Issue
Block a user