diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..7a1f90fd4 --- /dev/null +++ b/Makefile @@ -0,0 +1,2 @@ +test: + go test ./... diff --git a/cryptostore/docs.go b/cryptostore/docs.go new file mode 100644 index 000000000..5c66fba2d --- /dev/null +++ b/cryptostore/docs.go @@ -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 diff --git a/cryptostore/enc_storage.go b/cryptostore/enc_storage.go new file mode 100644 index 000000000..0a935075f --- /dev/null +++ b/cryptostore/enc_storage.go @@ -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()}, + } +} diff --git a/cryptostore/encoder.go b/cryptostore/encoder.go new file mode 100644 index 000000000..03bc1e4e8 --- /dev/null +++ b/cryptostore/encoder.go @@ -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) +} diff --git a/cryptostore/encoder_test.go b/cryptostore/encoder_test.go new file mode 100644 index 000000000..8b72509ed --- /dev/null +++ b/cryptostore/encoder_test.go @@ -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) +} diff --git a/cryptostore/generator.go b/cryptostore/generator.go new file mode 100644 index 000000000..c6661fb34 --- /dev/null +++ b/cryptostore/generator.go @@ -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() +} diff --git a/cryptostore/holder.go b/cryptostore/holder.go new file mode 100644 index 000000000..4beb64160 --- /dev/null +++ b/cryptostore/holder.go @@ -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) +} diff --git a/cryptostore/holder_test.go b/cryptostore/holder_test.go new file mode 100644 index 000000000..fb5ef854f --- /dev/null +++ b/cryptostore/holder_test.go @@ -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 +// } diff --git a/cryptostore/storage_test.go b/cryptostore/storage_test.go new file mode 100644 index 000000000..6c6ea5e2c --- /dev/null +++ b/cryptostore/storage_test.go @@ -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) +} diff --git a/keys.toml b/keys.toml new file mode 100644 index 000000000..41f4d5286 --- /dev/null +++ b/keys.toml @@ -0,0 +1 @@ +format = "text" diff --git a/keys.yaml b/keys.yaml deleted file mode 100644 index 812adb72a..000000000 --- a/keys.yaml +++ /dev/null @@ -1 +0,0 @@ -name: george diff --git a/storage.go b/storage.go new file mode 100644 index 000000000..519ce1aa8 --- /dev/null +++ b/storage.go @@ -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 +} diff --git a/storage/filestorage/main.go b/storage/filestorage/main.go new file mode 100644 index 000000000..554a36540 --- /dev/null +++ b/storage/filestorage/main.go @@ -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") +} diff --git a/storage/filestorage/main_test.go b/storage/filestorage/main_test.go new file mode 100644 index 000000000..f81ba7198 --- /dev/null +++ b/storage/filestorage/main_test.go @@ -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) +} diff --git a/storage/memstorage/main.go b/storage/memstorage/main.go new file mode 100644 index 000000000..e6573eb9f --- /dev/null +++ b/storage/memstorage/main.go @@ -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 +} diff --git a/storage/memstorage/main_test.go b/storage/memstorage/main_test.go new file mode 100644 index 000000000..01c015a15 --- /dev/null +++ b/storage/memstorage/main_test.go @@ -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) +} diff --git a/transactions.go b/transactions.go new file mode 100644 index 000000000..f22719f7e --- /dev/null +++ b/transactions.go @@ -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 +}