@ -0,0 +1,2 @@ | |||
test: | |||
go test ./... |
@ -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 @@ | |||
format = "text" |
@ -1 +0,0 @@ | |||
name: george |
@ -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 | |||
} |