@ -1,69 +1,100 @@ | |||||
.PHONEY: all test install get_vendor_deps ensure_tools codegen wordlist | |||||
GOTOOLS = \ | GOTOOLS = \ | ||||
github.com/Masterminds/glide \ | github.com/Masterminds/glide \ | ||||
github.com/jteeuwen/go-bindata/go-bindata \ | |||||
github.com/alecthomas/gometalinter | |||||
github.com/jteeuwen/go-bindata/go-bindata | |||||
# gopkg.in/alecthomas/gometalinter.v2 \ | |||||
# | |||||
GOTOOLS_CHECK = glide go-bindata #gometalinter.v2 | |||||
REPO:=github.com/tendermint/go-crypto | |||||
all: check get_vendor_deps build test install | |||||
all: get_vendor_deps metalinter_test test | |||||
check: check_tools | |||||
test: | |||||
go test -p 1 `glide novendor` | |||||
get_vendor_deps: ensure_tools | |||||
######################################## | |||||
### Build | |||||
wordlist: | |||||
# Generating wordlist.go... | |||||
go-bindata -ignore ".*\.go" -o keys/words/wordlist/wordlist.go -pkg "wordlist" keys/words/wordlist/... | |||||
build: wordlist | |||||
# Nothing else to build! | |||||
install: | |||||
# Nothing to install! | |||||
######################################## | |||||
### Tools & dependencies | |||||
check_tools: | |||||
@# https://stackoverflow.com/a/25668869 | |||||
@echo "Found tools: $(foreach tool,$(GOTOOLS_CHECK),\ | |||||
$(if $(shell which $(tool)),$(tool),$(error "No $(tool) in PATH")))" | |||||
get_tools: | |||||
@echo "--> Installing tools" | |||||
go get -u -v $(GOTOOLS) | |||||
#@gometalinter.v2 --install | |||||
update_tools: | |||||
@echo "--> Updating tools" | |||||
@go get -u $(GOTOOLS) | |||||
get_vendor_deps: | |||||
@rm -rf vendor/ | @rm -rf vendor/ | ||||
@echo "--> Running glide install" | @echo "--> Running glide install" | ||||
@glide install | @glide install | ||||
ensure_tools: | |||||
go get $(GOTOOLS) | |||||
wordlist: | |||||
go-bindata -ignore ".*\.go" -o keys/wordlist/wordlist.go -pkg "wordlist" keys/wordlist/... | |||||
prepgen: install | |||||
go install ./vendor/github.com/btcsuite/btcutil/base58 | |||||
go install ./vendor/github.com/stretchr/testify/assert | |||||
go install ./vendor/github.com/stretchr/testify/require | |||||
go install ./vendor/golang.org/x/crypto/bcrypt | |||||
codegen: | |||||
@echo "--> regenerating all interface wrappers" | |||||
@gen | |||||
@echo "Done!" | |||||
metalinter: ensure_tools | |||||
@gometalinter --install | |||||
gometalinter --vendor --deadline=600s --enable-all --disable=lll ./... | |||||
metalinter_test: ensure_tools | |||||
@gometalinter --install | |||||
gometalinter --vendor --deadline=600s --disable-all \ | |||||
######################################## | |||||
### Testing | |||||
test: | |||||
go test -p 1 `glide novendor` | |||||
######################################## | |||||
### Formatting, linting, and vetting | |||||
fmt: | |||||
@go fmt ./... | |||||
metalinter: | |||||
@echo "==> Running linter" | |||||
gometalinter.v2 --vendor --deadline=600s --disable-all \ | |||||
--enable=maligned \ | |||||
--enable=deadcode \ | --enable=deadcode \ | ||||
--enable=gas \ | |||||
--enable=goconst \ | --enable=goconst \ | ||||
--enable=gocyclo \ | |||||
--enable=goimports \ | |||||
--enable=gosimple \ | --enable=gosimple \ | ||||
--enable=ineffassign \ | |||||
--enable=interfacer \ | |||||
--enable=maligned \ | |||||
--enable=ineffassign \ | |||||
--enable=megacheck \ | --enable=megacheck \ | ||||
--enable=misspell \ | |||||
--enable=safesql \ | |||||
--enable=misspell \ | |||||
--enable=staticcheck \ | --enable=staticcheck \ | ||||
--enable=safesql \ | |||||
--enable=structcheck \ | --enable=structcheck \ | ||||
--enable=unconvert \ | |||||
--enable=unconvert \ | |||||
--enable=unused \ | --enable=unused \ | ||||
--enable=vetshadow \ | |||||
--enable=vet \ | |||||
--enable=varcheck \ | --enable=varcheck \ | ||||
--enable=vetshadow \ | |||||
./... | ./... | ||||
#--enable=gas \ | |||||
#--enable=dupl \ | #--enable=dupl \ | ||||
#--enable=errcheck \ | #--enable=errcheck \ | ||||
#--enable=goimports \ | |||||
#--enable=gocyclo \ | |||||
#--enable=golint \ <== comments on anything exported | #--enable=golint \ <== comments on anything exported | ||||
#--enable=gotype \ | #--enable=gotype \ | ||||
#--enable=interfacer \ | |||||
#--enable=unparam \ | #--enable=unparam \ | ||||
#--enable=vet \ | |||||
metalinter_all: | |||||
protoc $(INCLUDE) --lint_out=. types/*.proto | |||||
gometalinter.v2 --vendor --deadline=600s --enable-all --disable=lll ./... | |||||
# To avoid unintended conflicts with file names, always add to .PHONY | |||||
# unless there is a reason not to. | |||||
# https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html | |||||
.PHONEY: check wordlist build install check_tools get_tools update_tools get_vendor_deps test fmt metalinter metalinter_all |
@ -1,25 +0,0 @@ | |||||
/* | |||||
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 |
@ -1,49 +0,0 @@ | |||||
package cryptostore | |||||
import ( | |||||
crypto "github.com/tendermint/go-crypto" | |||||
keys "github.com/tendermint/go-crypto/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.Info, error) { | |||||
secret, info, err := es.store.Get(name) | |||||
if err != nil { | |||||
return crypto.PrivKey{}, info, err | |||||
} | |||||
key, err := es.coder.Decrypt(secret, pass) | |||||
return key, info, err | |||||
} | |||||
func (es encryptedStorage) List() (keys.Infos, 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.Info { | |||||
pub := key.PubKey() | |||||
return keys.Info{ | |||||
Name: name, | |||||
Address: pub.Address(), | |||||
PubKey: pub, | |||||
} | |||||
} |
@ -1,60 +0,0 @@ | |||||
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) { | |||||
if pass == "" { | |||||
return key.Bytes(), nil | |||||
} | |||||
s := secret(pass) | |||||
cipher := crypto.EncryptSymmetric(key.Bytes(), s) | |||||
return cipher, nil | |||||
} | |||||
func (e secretbox) Decrypt(data []byte, pass string) (key crypto.PrivKey, err error) { | |||||
private := data | |||||
if pass != "" { | |||||
s := secret(pass) | |||||
private, err = crypto.DecryptSymmetric(data, s) | |||||
if err != nil { | |||||
return crypto.PrivKey{}, 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) | |||||
} |
@ -1,105 +0,0 @@ | |||||
package cryptostore_test | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
"github.com/stretchr/testify/require" | |||||
cmn "github.com/tendermint/tmlibs/common" | |||||
"github.com/tendermint/go-crypto/keys/cryptostore" | |||||
) | |||||
func TestNoopEncoder(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
noop := cryptostore.Noop | |||||
key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||||
require.NoError(err) | |||||
key2, err := cryptostore.GenSecp256k1.Generate(cmn.RandBytes(16)) | |||||
require.NoError(err) | |||||
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, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||||
require.NoError(err) | |||||
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.True(pk.Empty()) | |||||
// but decoding with the same passphrase gets us our key | |||||
pk, err = enc.Decrypt(b, pass) | |||||
require.Nil(err) | |||||
assert.Equal(key, pk) | |||||
} | |||||
func TestSecretBoxNoPass(t *testing.T) { | |||||
assert, require := assert.New(t), require.New(t) | |||||
enc := cryptostore.SecretBox | |||||
key, rerr := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||||
require.NoError(rerr) | |||||
cases := []struct { | |||||
encode string | |||||
decode string | |||||
valid bool | |||||
}{ | |||||
{"foo", "foo", true}, | |||||
{"foo", "food", false}, | |||||
{"", "", true}, | |||||
{"", "a", false}, | |||||
{"a", "", false}, | |||||
} | |||||
for i, tc := range cases { | |||||
b, err := enc.Encrypt(key, tc.encode) | |||||
require.Nil(err, "%d: %+v", i, err) | |||||
assert.NotEmpty(b, "%d", i) | |||||
pk, err := enc.Decrypt(b, tc.decode) | |||||
if tc.valid { | |||||
require.Nil(err, "%d: %+v", i, err) | |||||
assert.Equal(key, pk, "%d", i) | |||||
} else { | |||||
require.NotNil(err, "%d", i) | |||||
} | |||||
} | |||||
// now let's make sure raw bytes also work... | |||||
b := key.Bytes() | |||||
pk, err := enc.Decrypt(b, "") | |||||
require.Nil(err, "%+v", err) | |||||
assert.Equal(key, pk) | |||||
} |
@ -1,88 +0,0 @@ | |||||
package cryptostore | |||||
import ( | |||||
"github.com/pkg/errors" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
"github.com/tendermint/go-crypto/nano" | |||||
) | |||||
var ( | |||||
// GenEd25519 produces Ed25519 private keys | |||||
GenEd25519 Generator = GenFunc(genEd25519) | |||||
// GenSecp256k1 produces Secp256k1 private keys | |||||
GenSecp256k1 Generator = GenFunc(genSecp256) | |||||
// GenLedgerEd25519 used Ed25519 keys stored on nano ledger s with cosmos app | |||||
GenLedgerEd25519 Generator = GenFunc(genLedgerEd25519) | |||||
) | |||||
// Generator determines the type of private key the keystore creates | |||||
type Generator interface { | |||||
Generate(secret []byte) (crypto.PrivKey, error) | |||||
} | |||||
// GenFunc is a helper to transform a function into a Generator | |||||
type GenFunc func(secret []byte) (crypto.PrivKey, error) | |||||
func (f GenFunc) Generate(secret []byte) (crypto.PrivKey, error) { | |||||
return f(secret) | |||||
} | |||||
func genEd25519(secret []byte) (crypto.PrivKey, error) { | |||||
key := crypto.GenPrivKeyEd25519FromSecret(secret).Wrap() | |||||
return key, nil | |||||
} | |||||
func genSecp256(secret []byte) (crypto.PrivKey, error) { | |||||
key := crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap() | |||||
return key, nil | |||||
} | |||||
// secret is completely ignored for the ledger... | |||||
// just for interface compatibility | |||||
func genLedgerEd25519(secret []byte) (crypto.PrivKey, error) { | |||||
return nano.NewPrivKeyLedgerEd25519Ed25519() | |||||
} | |||||
type genInvalidByte struct { | |||||
typ byte | |||||
} | |||||
func (g genInvalidByte) Generate(secret []byte) (crypto.PrivKey, error) { | |||||
err := errors.Errorf("Cannot generate keys for algorithm: %X", g.typ) | |||||
return crypto.PrivKey{}, err | |||||
} | |||||
type genInvalidAlgo struct { | |||||
algo string | |||||
} | |||||
func (g genInvalidAlgo) Generate(secret []byte) (crypto.PrivKey, error) { | |||||
err := errors.Errorf("Cannot generate keys for algorithm: %s", g.algo) | |||||
return crypto.PrivKey{}, err | |||||
} | |||||
func getGenerator(algo string) Generator { | |||||
switch algo { | |||||
case crypto.NameEd25519: | |||||
return GenEd25519 | |||||
case crypto.NameSecp256k1: | |||||
return GenSecp256k1 | |||||
case nano.NameLedgerEd25519: | |||||
return GenLedgerEd25519 | |||||
default: | |||||
return genInvalidAlgo{algo} | |||||
} | |||||
} | |||||
func getGeneratorByType(typ byte) Generator { | |||||
switch typ { | |||||
case crypto.TypeEd25519: | |||||
return GenEd25519 | |||||
case crypto.TypeSecp256k1: | |||||
return GenSecp256k1 | |||||
case nano.TypeLedgerEd25519: | |||||
return GenLedgerEd25519 | |||||
default: | |||||
return genInvalidByte{typ} | |||||
} | |||||
} |
@ -1,169 +0,0 @@ | |||||
package cryptostore | |||||
import ( | |||||
"strings" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
keys "github.com/tendermint/go-crypto/keys" | |||||
) | |||||
// Manager combines encyption and storage implementation to provide | |||||
// a full-featured key manager | |||||
type Manager struct { | |||||
es encryptedStorage | |||||
codec keys.Codec | |||||
} | |||||
func New(coder Encoder, store keys.Storage, codec keys.Codec) Manager { | |||||
return Manager{ | |||||
es: encryptedStorage{ | |||||
coder: coder, | |||||
store: store, | |||||
}, | |||||
codec: codec, | |||||
} | |||||
} | |||||
// assert Manager satisfies keys.Signer and keys.Manager interfaces | |||||
var _ keys.Signer = Manager{} | |||||
var _ keys.Manager = Manager{} | |||||
// Create adds a new key to the storage engine, returning error if | |||||
// another key already stored under this name | |||||
// | |||||
// algo must be a supported go-crypto algorithm: ed25519, secp256k1 | |||||
func (s Manager) Create(name, passphrase, algo string) (keys.Info, string, error) { | |||||
// 128-bits are the all the randomness we can make use of | |||||
secret := crypto.CRandBytes(16) | |||||
gen := getGenerator(algo) | |||||
key, err := gen.Generate(secret) | |||||
if err != nil { | |||||
return keys.Info{}, "", err | |||||
} | |||||
err = s.es.Put(name, passphrase, key) | |||||
if err != nil { | |||||
return keys.Info{}, "", err | |||||
} | |||||
// we append the type byte to the serialized secret to help with recovery | |||||
// ie [secret] = [secret] + [type] | |||||
typ := key.Bytes()[0] | |||||
secret = append(secret, typ) | |||||
seed, err := s.codec.BytesToWords(secret) | |||||
phrase := strings.Join(seed, " ") | |||||
return info(name, key), phrase, err | |||||
} | |||||
// Recover takes a seed phrase and tries to recover the private key. | |||||
// | |||||
// If the seed phrase is valid, it will create the private key and store | |||||
// it under name, protected by passphrase. | |||||
// | |||||
// Result similar to New(), except it doesn't return the seed again... | |||||
func (s Manager) Recover(name, passphrase, seedphrase string) (keys.Info, error) { | |||||
words := strings.Split(strings.TrimSpace(seedphrase), " ") | |||||
secret, err := s.codec.WordsToBytes(words) | |||||
if err != nil { | |||||
return keys.Info{}, err | |||||
} | |||||
// secret is comprised of the actual secret with the type appended | |||||
// ie [secret] = [secret] + [type] | |||||
l := len(secret) | |||||
secret, typ := secret[:l-1], secret[l-1] | |||||
gen := getGeneratorByType(typ) | |||||
key, err := gen.Generate(secret) | |||||
if err != nil { | |||||
return keys.Info{}, err | |||||
} | |||||
// d00d, it worked! create the bugger.... | |||||
err = s.es.Put(name, passphrase, key) | |||||
return info(name, key), err | |||||
} | |||||
// List loads the keys from the storage and enforces alphabetical order | |||||
func (s Manager) List() (keys.Infos, error) { | |||||
res, err := s.es.List() | |||||
res.Sort() | |||||
return res, err | |||||
} | |||||
// Get returns the public information about one key | |||||
func (s Manager) Get(name string) (keys.Info, 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) | |||||
} |
@ -1,48 +0,0 @@ | |||||
package cryptostore | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
cmn "github.com/tendermint/tmlibs/common" | |||||
keys "github.com/tendermint/go-crypto/keys" | |||||
) | |||||
func TestSortKeys(t *testing.T) { | |||||
assert := assert.New(t) | |||||
gen := func() crypto.PrivKey { | |||||
key, _ := GenEd25519.Generate(cmn.RandBytes(16)) | |||||
return key | |||||
} | |||||
assert.NotEqual(gen(), gen()) | |||||
// alphabetical order is n3, n1, n2 | |||||
n1, n2, n3 := "john", "mike", "alice" | |||||
infos := keys.Infos{ | |||||
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,247 @@ | |||||
package keys | |||||
import ( | |||||
"fmt" | |||||
"strings" | |||||
"github.com/pkg/errors" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
dbm "github.com/tendermint/tmlibs/db" | |||||
"github.com/tendermint/go-crypto/keys/words" | |||||
"github.com/tendermint/go-crypto/nano" | |||||
) | |||||
// XXX Lets use go-crypto/bcrypt and ascii encoding directly in here without | |||||
// further wrappers around a store or DB. | |||||
// Copy functions from: https://github.com/tendermint/mintkey/blob/master/cmd/mintkey/common.go | |||||
// | |||||
// dbKeybase combines encyption and storage implementation to provide | |||||
// a full-featured key manager | |||||
type dbKeybase struct { | |||||
db dbm.DB | |||||
codec words.Codec | |||||
} | |||||
func New(db dbm.DB, codec words.Codec) dbKeybase { | |||||
return dbKeybase{ | |||||
db: db, | |||||
codec: codec, | |||||
} | |||||
} | |||||
var _ Keybase = dbKeybase{} | |||||
// Create generates a new key and persists it storage, encrypted using the passphrase. | |||||
// It returns the generated seedphrase (mnemonic) and the key Info. | |||||
// It returns an error if it fails to generate a key for the given algo type, | |||||
// or if another key is already stored under the same name. | |||||
func (kb dbKeybase) Create(name, passphrase, algo string) (string, Info, error) { | |||||
// NOTE: secret is SHA256 hashed by secp256k1 and ed25519. | |||||
// 16 byte secret corresponds to 12 BIP39 words. | |||||
// XXX: Ledgers use 24 words now - should we ? | |||||
secret := crypto.CRandBytes(16) | |||||
key, err := generate(algo, secret) | |||||
if err != nil { | |||||
return "", Info{}, err | |||||
} | |||||
// encrypt and persist the key | |||||
public := kb.writeKey(key, name, passphrase) | |||||
// return the mnemonic phrase | |||||
words, err := kb.codec.BytesToWords(secret) | |||||
seedphrase := strings.Join(words, " ") | |||||
return seedphrase, public, err | |||||
} | |||||
// Recover converts a seedphrase to a private key and persists it, encrypted with the given passphrase. | |||||
// Functions like Create, but seedphrase is input not output. | |||||
func (kb dbKeybase) Recover(name, passphrase, algo string, seedphrase string) (Info, error) { | |||||
key, err := kb.SeedToPrivKey(algo, seedphrase) | |||||
if err != nil { | |||||
return Info{}, err | |||||
} | |||||
// Valid seedphrase. Encrypt key and persist to disk. | |||||
public := kb.writeKey(key, name, passphrase) | |||||
return public, nil | |||||
} | |||||
// SeedToPrivKey returns the private key corresponding to a seedphrase | |||||
// without persisting the private key. | |||||
// TODO: enable the keybase to just hold these in memory so we can sign without persisting (?) | |||||
func (kb dbKeybase) SeedToPrivKey(algo, seedphrase string) (crypto.PrivKey, error) { | |||||
words := strings.Split(strings.TrimSpace(seedphrase), " ") | |||||
secret, err := kb.codec.WordsToBytes(words) | |||||
if err != nil { | |||||
return crypto.PrivKey{}, err | |||||
} | |||||
key, err := generate(algo, secret) | |||||
if err != nil { | |||||
return crypto.PrivKey{}, err | |||||
} | |||||
return key, nil | |||||
} | |||||
// List returns the keys from storage in alphabetical order. | |||||
func (kb dbKeybase) List() ([]Info, error) { | |||||
var res []Info | |||||
iter := kb.db.Iterator(nil, nil) | |||||
defer iter.Close() | |||||
for ; iter.Valid(); iter.Next() { | |||||
key := iter.Key() | |||||
if isPub(key) { | |||||
info, err := readInfo(iter.Value()) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
res = append(res, info) | |||||
} | |||||
} | |||||
return res, nil | |||||
} | |||||
// Get returns the public information about one key. | |||||
func (kb dbKeybase) Get(name string) (Info, error) { | |||||
bs := kb.db.Get(pubName(name)) | |||||
return readInfo(bs) | |||||
} | |||||
// Sign signs the msg with the named key. | |||||
// It returns an error if the key doesn't exist or the decryption fails. | |||||
// TODO: what if leddger fails ? | |||||
func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signature, pk crypto.PubKey, err error) { | |||||
var key crypto.PrivKey | |||||
armorStr := kb.db.Get(privName(name)) | |||||
key, err = unarmorDecryptPrivKey(string(armorStr), passphrase) | |||||
if err != nil { | |||||
return | |||||
} | |||||
sig = key.Sign(msg) | |||||
pk = key.PubKey() | |||||
return | |||||
} | |||||
// Export decodes the private key with the current password, encrypts | |||||
// it with a secure one-time password and generates an armored private key | |||||
// that can be Imported by another dbKeybase. | |||||
// | |||||
// This is designed to copy from one device to another, or provide backups | |||||
// during version updates. | |||||
func (kb dbKeybase) Export(name, oldpass, transferpass string) ([]byte, error) { | |||||
armorStr := kb.db.Get(privName(name)) | |||||
key, err := unarmorDecryptPrivKey(string(armorStr), oldpass) | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
if transferpass == "" { | |||||
return key.Bytes(), nil | |||||
} | |||||
armorBytes := encryptArmorPrivKey(key, transferpass) | |||||
return []byte(armorBytes), nil | |||||
} | |||||
// 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 (kb dbKeybase) Import(name, newpass, transferpass string, data []byte) (err error) { | |||||
var key crypto.PrivKey | |||||
if transferpass == "" { | |||||
key, err = crypto.PrivKeyFromBytes(data) | |||||
} else { | |||||
key, err = unarmorDecryptPrivKey(string(data), transferpass) | |||||
} | |||||
if err != nil { | |||||
return err | |||||
} | |||||
kb.writeKey(key, name, newpass) | |||||
return nil | |||||
} | |||||
// Delete removes key forever, but we must present the | |||||
// proper passphrase before deleting it (for security). | |||||
func (kb dbKeybase) Delete(name, passphrase string) error { | |||||
// verify we have the proper password before deleting | |||||
bs := kb.db.Get(privName(name)) | |||||
_, err := unarmorDecryptPrivKey(string(bs), passphrase) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
kb.db.DeleteSync(pubName(name)) | |||||
kb.db.DeleteSync(privName(name)) | |||||
return nil | |||||
} | |||||
// Update changes the passphrase with which an already stored key is encrypted. | |||||
// | |||||
// oldpass must be the current passphrase used for encryption, newpass will be | |||||
// the only valid passphrase from this time forward. | |||||
func (kb dbKeybase) Update(name, oldpass, newpass string) error { | |||||
bs := kb.db.Get(privName(name)) | |||||
key, err := unarmorDecryptPrivKey(string(bs), oldpass) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
// Generate the public bytes and the encrypted privkey | |||||
public := info(name, key) | |||||
private := encryptArmorPrivKey(key, newpass) | |||||
// We must delete first, as Putting over an existing name returns an error. | |||||
// Must be done atomically with the write or we could lose the key. | |||||
batch := kb.db.NewBatch() | |||||
batch.Delete(pubName(name)) | |||||
batch.Delete(privName(name)) | |||||
batch.Set(pubName(name), public.bytes()) | |||||
batch.Set(privName(name), []byte(private)) | |||||
batch.Write() | |||||
return nil | |||||
} | |||||
//--------------------------------------------------------------------------------------- | |||||
func (kb dbKeybase) writeKey(priv crypto.PrivKey, name, passphrase string) Info { | |||||
// Generate the public bytes and the encrypted privkey | |||||
public := info(name, priv) | |||||
private := encryptArmorPrivKey(priv, passphrase) | |||||
// Write them both | |||||
kb.db.SetSync(pubName(name), public.bytes()) | |||||
kb.db.SetSync(privName(name), []byte(private)) | |||||
return public | |||||
} | |||||
// TODO: use a `type TypeKeyAlgo string` (?) | |||||
func generate(algo string, secret []byte) (crypto.PrivKey, error) { | |||||
switch algo { | |||||
case crypto.NameEd25519: | |||||
return crypto.GenPrivKeyEd25519FromSecret(secret).Wrap(), nil | |||||
case crypto.NameSecp256k1: | |||||
return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap(), nil | |||||
case nano.NameLedgerEd25519: | |||||
return nano.NewPrivKeyLedgerEd25519() | |||||
default: | |||||
err := errors.Errorf("Cannot generate keys for algorithm: %s", algo) | |||||
return crypto.PrivKey{}, err | |||||
} | |||||
} | |||||
func pubName(name string) []byte { | |||||
return []byte(fmt.Sprintf("%s.pub", name)) | |||||
} | |||||
func privName(name string) []byte { | |||||
return []byte(fmt.Sprintf("%s.priv", name)) | |||||
} | |||||
func isPub(name []byte) bool { | |||||
return strings.HasSuffix(string(name), ".pub") | |||||
} |
@ -0,0 +1,73 @@ | |||||
package keys | |||||
import ( | |||||
"encoding/hex" | |||||
"fmt" | |||||
cmn "github.com/tendermint/tmlibs/common" | |||||
"github.com/tendermint/go-crypto" | |||||
"github.com/tendermint/go-crypto/keys/bcrypt" | |||||
) | |||||
const ( | |||||
blockTypePrivKey = "TENDERMINT PRIVATE KEY" | |||||
) | |||||
func encryptArmorPrivKey(privKey crypto.PrivKey, passphrase string) string { | |||||
saltBytes, encBytes := encryptPrivKey(privKey, passphrase) | |||||
header := map[string]string{ | |||||
"kdf": "bcrypt", | |||||
"salt": fmt.Sprintf("%X", saltBytes), | |||||
} | |||||
armorStr := crypto.EncodeArmor(blockTypePrivKey, header, encBytes) | |||||
return armorStr | |||||
} | |||||
func unarmorDecryptPrivKey(armorStr string, passphrase string) (crypto.PrivKey, error) { | |||||
var privKey crypto.PrivKey | |||||
blockType, header, encBytes, err := crypto.DecodeArmor(armorStr) | |||||
if err != nil { | |||||
return privKey, err | |||||
} | |||||
if blockType != blockTypePrivKey { | |||||
return privKey, fmt.Errorf("Unrecognized armor type: %v", blockType) | |||||
} | |||||
if header["kdf"] != "bcrypt" { | |||||
return privKey, fmt.Errorf("Unrecognized KDF type: %v", header["KDF"]) | |||||
} | |||||
if header["salt"] == "" { | |||||
return privKey, fmt.Errorf("Missing salt bytes") | |||||
} | |||||
saltBytes, err := hex.DecodeString(header["salt"]) | |||||
if err != nil { | |||||
return privKey, fmt.Errorf("Error decoding salt: %v", err.Error()) | |||||
} | |||||
privKey, err = decryptPrivKey(saltBytes, encBytes, passphrase) | |||||
return privKey, err | |||||
} | |||||
func encryptPrivKey(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte) { | |||||
saltBytes = crypto.CRandBytes(16) | |||||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016) | |||||
if err != nil { | |||||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | |||||
} | |||||
key = crypto.Sha256(key) // Get 32 bytes | |||||
privKeyBytes := privKey.Bytes() | |||||
return saltBytes, crypto.EncryptSymmetric(privKeyBytes, key) | |||||
} | |||||
func decryptPrivKey(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { | |||||
key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 12) // TODO parameterize. 12 is good today (2016) | |||||
if err != nil { | |||||
cmn.Exit("Error generating bcrypt key from passphrase: " + err.Error()) | |||||
} | |||||
key = crypto.Sha256(key) // Get 32 bytes | |||||
privKeyBytes, err := crypto.DecryptSymmetric(encBytes, key) | |||||
if err != nil { | |||||
return privKey, err | |||||
} | |||||
privKey, err = crypto.PrivKeyFromBytes(privKeyBytes) | |||||
return privKey, err | |||||
} |
@ -1,177 +0,0 @@ | |||||
/* | |||||
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-crypto/keys" | |||||
) | |||||
const ( | |||||
BlockType = "Tendermint Light Client" | |||||
// PrivExt is the extension for private keys. | |||||
PrivExt = "tlc" | |||||
// PubExt is the extensions for public keys. | |||||
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.MkdirAll(dir, dirPerm) | |||||
if err != nil { | |||||
panic(err) | |||||
} | |||||
return FileStore{dir} | |||||
} | |||||
// assert FileStore satisfies keys.Storage | |||||
var _ keys.Storage = FileStore{} | |||||
// 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.Info) 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 info 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.Info, error) { | |||||
pub, priv := s.nameToPaths(name) | |||||
info, err := readInfo(pub) | |||||
if err != nil { | |||||
return nil, info, err | |||||
} | |||||
key, _, err := read(priv) | |||||
return key, info.Format(), err | |||||
} | |||||
// List parses the key directory for public info and returns a list of | |||||
// Info for all keys located in this directory. | |||||
func (s FileStore) List() (keys.Infos, error) { | |||||
dir, err := os.Open(s.keyDir) | |||||
if err != nil { | |||||
return nil, errors.Wrap(err, "List Keys") | |||||
} | |||||
defer dir.Close() | |||||
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.Info, 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.Format()) | |||||
} | |||||
} | |||||
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.Info) error { | |||||
return write(path, info.Name, info.PubKey.Bytes()) | |||||
} | |||||
func readInfo(path string) (info keys.Info, err error) { | |||||
var data []byte | |||||
data, info.Name, err = read(path) | |||||
if err != nil { | |||||
return | |||||
} | |||||
pk, err := crypto.PubKeyFromBytes(data) | |||||
info.PubKey = pk | |||||
return | |||||
} | |||||
func read(path string) ([]byte, string, error) { | |||||
f, err := os.Open(path) | |||||
if err != nil { | |||||
return nil, "", errors.Wrap(err, "Reading data") | |||||
} | |||||
defer f.Close() | |||||
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") | |||||
} |
@ -1,106 +0,0 @@ | |||||
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-crypto/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.Info{ | |||||
Name: name, | |||||
PubKey: pubkey.Wrap(), | |||||
} | |||||
// 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.Name, i.Name) | |||||
assert.Equal(info.PubKey, i.PubKey) | |||||
assert.NotEmpty(i.Address) | |||||
l, err = store.List() | |||||
require.Nil(err, "%+v", err) | |||||
assert.Equal(1, len(l)) | |||||
assert.Equal(i, 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)) | |||||
defer os.RemoveAll(newDir) | |||||
// now, check with two levels deep.... | |||||
parentDir := path.Join(os.TempDir(), "missing-dir") | |||||
nestedDir := path.Join(parentDir, "lots", "of", "levels", "here") | |||||
_, err = os.Open(parentDir) | |||||
assert.True(os.IsNotExist(err)) | |||||
defer os.RemoveAll(parentDir) | |||||
// create a new storage, and verify it creates the directory with good permissions | |||||
for _, dir := range []string{newDir, nestedDir, newDir} { | |||||
New(dir) | |||||
d, err := os.Open(dir) | |||||
require.Nil(err) | |||||
defer d.Close() | |||||
stat, err := d.Stat() | |||||
require.Nil(err) | |||||
assert.Equal(dirPerm, stat.Mode()&os.ModePerm) | |||||
} | |||||
} |
@ -1,68 +0,0 @@ | |||||
/* | |||||
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-crypto/keys" | |||||
) | |||||
type data struct { | |||||
info keys.Info | |||||
key []byte | |||||
} | |||||
type MemStore map[string]data | |||||
// New creates an instance of file-based key storage with tight permissions | |||||
func New() MemStore { | |||||
return MemStore{} | |||||
} | |||||
// assert MemStore satisfies keys.Storage | |||||
var _ keys.Storage = MemStore{} | |||||
// 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.Info) 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.Info, 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.Format(), err | |||||
} | |||||
// List returns the public info of all keys in the MemStore in unsorted order | |||||
func (s MemStore) List() (keys.Infos, error) { | |||||
res := make([]keys.Info, len(s)) | |||||
i := 0 | |||||
for _, d := range s { | |||||
res[i] = d.info.Format() | |||||
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 | |||||
} |
@ -1,69 +0,0 @@ | |||||
package memstorage | |||||
import ( | |||||
"testing" | |||||
"github.com/stretchr/testify/assert" | |||||
crypto "github.com/tendermint/go-crypto" | |||||
keys "github.com/tendermint/go-crypto/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.Info{ | |||||
Name: name, | |||||
PubKey: 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.Name, i.Name) | |||||
assert.Equal(info.PubKey, i.PubKey) | |||||
assert.NotEmpty(i.Address) | |||||
l, err = store.List() | |||||
assert.Nil(err) | |||||
assert.Equal(1, len(l)) | |||||
assert.Equal(i, 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) | |||||
} |
@ -1,134 +1,48 @@ | |||||
package keys | package keys | ||||
import ( | import ( | ||||
"fmt" | |||||
"sort" | |||||
wire "github.com/tendermint/go-wire" | |||||
crypto "github.com/tendermint/go-crypto" | crypto "github.com/tendermint/go-crypto" | ||||
wire "github.com/tendermint/go-wire" | |||||
data "github.com/tendermint/go-wire/data" | |||||
) | ) | ||||
// 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 Info) error | |||||
Get(name string) (key []byte, info Info, err error) | |||||
List() (Infos, error) | |||||
Delete(name string) error | |||||
} | |||||
// Info is the public information about a key | // Info is the public information about a key | ||||
type Info struct { | type Info struct { | ||||
Name string `json:"name"` | |||||
Address data.Bytes `json:"address"` | |||||
PubKey crypto.PubKey `json:"pubkey"` | |||||
Name string `json:"name"` | |||||
PubKey crypto.PubKey `json:"pubkey"` | |||||
} | } | ||||
func (i *Info) Format() Info { | |||||
if !i.PubKey.Empty() { | |||||
i.Address = i.PubKey.Address() | |||||
} | |||||
return *i | |||||
// Address is a helper function to calculate the address from the pubkey | |||||
func (i Info) Address() []byte { | |||||
return i.PubKey.Address() | |||||
} | } | ||||
// Infos is a wrapper to allows alphabetical sorting of the keys | |||||
type Infos []Info | |||||
func (k Infos) Len() int { return len(k) } | |||||
func (k Infos) Less(i, j int) bool { return k[i].Name < k[j].Name } | |||||
func (k Infos) Swap(i, j int) { k[i], k[j] = k[j], k[i] } | |||||
func (k Infos) Sort() { | |||||
if k != nil { | |||||
sort.Sort(k) | |||||
} | |||||
func (i Info) bytes() []byte { | |||||
return wire.BinaryBytes(i) | |||||
} | } | ||||
// 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) | |||||
func readInfo(bs []byte) (info Info, err error) { | |||||
err = wire.ReadBinaryBytes(bs, &info) | |||||
return | |||||
} | } | ||||
// Signer allows one to use a keystore to sign transactions | |||||
type Signer interface { | |||||
Sign(name, passphrase string, tx Signable) error | |||||
func info(name string, privKey crypto.PrivKey) Info { | |||||
return Info{ | |||||
Name: name, | |||||
PubKey: privKey.PubKey(), | |||||
} | |||||
} | } | ||||
// Manager allows simple CRUD on a keystore, as an aid to signing | |||||
type Manager interface { | |||||
Signer | |||||
// Create also returns a seed phrase for cold-storage | |||||
Create(name, passphrase, algo string) (Info, string, error) | |||||
// Recover takes a seedphrase and loads in the private key | |||||
Recover(name, passphrase, seedphrase string) (Info, error) | |||||
List() (Infos, error) | |||||
// Keybase allows simple CRUD on a keystore, as an aid to signing | |||||
type Keybase interface { | |||||
// Sign some bytes | |||||
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error) | |||||
// Create a new keypair | |||||
Create(name, passphrase, algo string) (seedphrase string, _ Info, _ error) | |||||
// Recover takes a seedphrase and loads in the key | |||||
Recover(name, passphrase, algo, seedphrase string) (Info, error) | |||||
List() ([]Info, error) | |||||
Get(name string) (Info, error) | Get(name string) (Info, error) | ||||
Update(name, oldpass, newpass string) error | Update(name, oldpass, newpass string) error | ||||
Delete(name, passphrase string) error | Delete(name, passphrase string) error | ||||
} | } | ||||
/**** MockSignable allows us to view data ***/ | |||||
// MockSignable lets us wrap arbitrary data with a go-crypto signature | |||||
type MockSignable struct { | |||||
Data []byte | |||||
PubKey crypto.PubKey | |||||
Signature crypto.Signature | |||||
} | |||||
var _ Signable = &MockSignable{} | |||||
// NewMockSignable sets the data to sign | |||||
func NewMockSignable(data []byte) *MockSignable { | |||||
return &MockSignable{Data: data} | |||||
} | |||||
// TxBytes returns the full data with signatures | |||||
func (s *MockSignable) TxBytes() ([]byte, error) { | |||||
return wire.BinaryBytes(s), nil | |||||
} | |||||
// SignBytes returns the original data passed into `NewSig` | |||||
func (s *MockSignable) SignBytes() []byte { | |||||
return s.Data | |||||
} | |||||
// 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 | |||||
func (s *MockSignable) Sign(pubkey crypto.PubKey, sig crypto.Signature) error { | |||||
s.PubKey = pubkey | |||||
s.Signature = sig | |||||
return nil | |||||
} | |||||
// 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 | |||||
func (s *MockSignable) Signers() ([]crypto.PubKey, error) { | |||||
if s.PubKey.Empty() { | |||||
return nil, fmt.Errorf("no signers") | |||||
} | |||||
if !s.PubKey.VerifyBytes(s.SignBytes(), s.Signature) { | |||||
return nil, fmt.Errorf("invalid signature") | |||||
} | |||||
return []crypto.PubKey{s.PubKey}, nil | |||||
} |