Develop pre wirepull/1782/head
@ -1,69 +1,100 @@ | |||
.PHONEY: all test install get_vendor_deps ensure_tools codegen wordlist | |||
GOTOOLS = \ | |||
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/ | |||
@echo "--> Running 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=gas \ | |||
--enable=goconst \ | |||
--enable=gocyclo \ | |||
--enable=goimports \ | |||
--enable=gosimple \ | |||
--enable=ineffassign \ | |||
--enable=interfacer \ | |||
--enable=maligned \ | |||
--enable=ineffassign \ | |||
--enable=megacheck \ | |||
--enable=misspell \ | |||
--enable=safesql \ | |||
--enable=misspell \ | |||
--enable=staticcheck \ | |||
--enable=safesql \ | |||
--enable=structcheck \ | |||
--enable=unconvert \ | |||
--enable=unconvert \ | |||
--enable=unused \ | |||
--enable=vetshadow \ | |||
--enable=vet \ | |||
--enable=varcheck \ | |||
--enable=vetshadow \ | |||
./... | |||
#--enable=gas \ | |||
#--enable=dupl \ | |||
#--enable=errcheck \ | |||
#--enable=goimports \ | |||
#--enable=gocyclo \ | |||
#--enable=golint \ <== comments on anything exported | |||
#--enable=gotype \ | |||
#--enable=interfacer \ | |||
#--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,378 +0,0 @@ | |||
package cryptostore_test | |||
import ( | |||
"bytes" | |||
"fmt" | |||
"os" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
crypto "github.com/tendermint/go-crypto" | |||
"github.com/tendermint/go-crypto/keys" | |||
"github.com/tendermint/go-crypto/keys/cryptostore" | |||
"github.com/tendermint/go-crypto/keys/storage/memstorage" | |||
"github.com/tendermint/go-crypto/nano" | |||
) | |||
// 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.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
algo := crypto.NameEd25519 | |||
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) | |||
i, _, err := cstore.Create(n1, p1, algo) | |||
require.Equal(n1, i.Name) | |||
require.Nil(err) | |||
_, _, err = cstore.Create(n2, p2, algo) | |||
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.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
algo := crypto.NameSecp256k1 | |||
n1, n2 := "some dude", "a dudette" | |||
p1, p2 := "1234", "foobar" | |||
// create two users and get their info | |||
i1, _, err := cstore.Create(n1, p1, algo) | |||
require.Nil(err) | |||
i2, _, err := cstore.Create(n2, p2, algo) | |||
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 := keys.NewMockSignable(d1) | |||
err = cstore.Sign(n1, p1, s11) | |||
require.Nil(err) | |||
s12 := keys.NewMockSignable(d2) | |||
err = cstore.Sign(n1, p1, s12) | |||
require.Nil(err) | |||
s21 := keys.NewMockSignable(d1) | |||
err = cstore.Sign(n2, p2, s21) | |||
require.Nil(err) | |||
s22 := keys.NewMockSignable(d2) | |||
err = cstore.Sign(n2, p2, s22) | |||
require.Nil(err) | |||
// let's try to validate and make sure it only works when everything is proper | |||
cases := []struct { | |||
key crypto.PubKey | |||
data []byte | |||
sig crypto.Signature | |||
valid bool | |||
}{ | |||
// proper matches | |||
{i1.PubKey, d1, s11.Signature, true}, | |||
// change data, pubkey, or signature leads to fail | |||
{i1.PubKey, d2, s11.Signature, false}, | |||
{i2.PubKey, d1, s11.Signature, false}, | |||
{i1.PubKey, d1, s21.Signature, false}, | |||
// make sure other successes | |||
{i1.PubKey, d2, s12.Signature, true}, | |||
{i2.PubKey, d1, s21.Signature, true}, | |||
{i2.PubKey, d2, s22.Signature, true}, | |||
} | |||
for i, tc := range cases { | |||
valid := tc.key.VerifyBytes(tc.data, tc.sig) | |||
assert.Equal(tc.valid, valid, "%d", i) | |||
} | |||
} | |||
// TestSignWithLedger makes sure we have ledger compatibility with | |||
// the crypto store. | |||
// | |||
// This test will only succeed with a ledger attached to the computer | |||
// and the cosmos app open | |||
func TestSignWithLedger(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
if os.Getenv("WITH_LEDGER") == "" { | |||
t.Skip("Set WITH_LEDGER to run code on real ledger") | |||
} | |||
// make the storage with reasonable defaults | |||
cstore := cryptostore.New( | |||
cryptostore.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
n := "nano-s" | |||
p := "hard2hack" | |||
// create a nano user | |||
c, _, err := cstore.Create(n, p, nano.NameLedgerEd25519) | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(c.Name, n) | |||
_, ok := c.PubKey.Unwrap().(nano.PubKeyLedgerEd25519) | |||
require.True(ok) | |||
// make sure we can get it back | |||
info, err := cstore.Get(n) | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(info.Name, n) | |||
key := info.PubKey | |||
require.False(key.Empty()) | |||
require.True(key.Equals(c.PubKey)) | |||
// let's try to sign some messages | |||
d1 := []byte("welcome to cosmos") | |||
d2 := []byte("please turn on the app") | |||
// try signing both data with the ledger... | |||
s1 := keys.NewMockSignable(d1) | |||
err = cstore.Sign(n, p, s1) | |||
require.Nil(err) | |||
s2 := keys.NewMockSignable(d2) | |||
err = cstore.Sign(n, p, s2) | |||
require.Nil(err) | |||
// now, let's check those signatures work | |||
assert.True(key.VerifyBytes(d1, s1.Signature)) | |||
assert.True(key.VerifyBytes(d2, s2.Signature)) | |||
// and mismatched signatures don't | |||
assert.False(key.VerifyBytes(d1, s2.Signature)) | |||
} | |||
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) | |||
} | |||
// TestImportUnencrypted tests accepting raw priv keys bytes as input | |||
func TestImportUnencrypted(t *testing.T) { | |||
require := require.New(t) | |||
// make the storage with reasonable defaults | |||
cstore := cryptostore.New( | |||
cryptostore.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
key, err := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) | |||
require.NoError(err) | |||
addr := key.PubKey().Address() | |||
name := "john" | |||
pass := "top-secret" | |||
// import raw bytes | |||
err = cstore.Import(name, pass, "", key.Bytes()) | |||
require.Nil(err, "%+v", err) | |||
// make sure the address matches | |||
info, err := cstore.Get(name) | |||
require.Nil(err, "%+v", err) | |||
require.EqualValues(addr, info.Address) | |||
} | |||
// 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.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
algo := crypto.NameSecp256k1 | |||
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, algo) | |||
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) | |||
} | |||
// TestSeedPhrase verifies restoring from a seed phrase | |||
func TestSeedPhrase(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
// make the storage with reasonable defaults | |||
cstore := cryptostore.New( | |||
cryptostore.SecretBox, | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
algo := crypto.NameEd25519 | |||
n1, n2 := "lost-key", "found-again" | |||
p1, p2 := "1234", "foobar" | |||
// make sure key works with initial password | |||
info, seed, err := cstore.Create(n1, p1, algo) | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(n1, info.Name) | |||
assert.NotEmpty(seed) | |||
// now, let us delete this key | |||
err = cstore.Delete(n1, p1) | |||
require.Nil(err, "%+v", err) | |||
_, err = cstore.Get(n1) | |||
require.NotNil(err) | |||
// let us re-create it from the seed-phrase | |||
newInfo, err := cstore.Recover(n2, p2, seed) | |||
require.Nil(err, "%+v", err) | |||
assert.Equal(n2, newInfo.Name) | |||
assert.Equal(info.Address, newInfo.Address) | |||
assert.Equal(info.PubKey, newInfo.PubKey) | |||
} | |||
func ExampleNew() { | |||
// Select the encryption and storage for your cryptostore | |||
cstore := cryptostore.New( | |||
cryptostore.SecretBox, | |||
// Note: use filestorage.New(dir) for real data | |||
memstorage.New(), | |||
keys.MustLoadCodec("english"), | |||
) | |||
ed := crypto.NameEd25519 | |||
sec := crypto.NameSecp256k1 | |||
// Add keys and see they return in alphabetical order | |||
bob, _, err := cstore.Create("Bob", "friend", ed) | |||
if err != nil { | |||
// this should never happen | |||
fmt.Println(err) | |||
} else { | |||
// return info here just like in List | |||
fmt.Println(bob.Name) | |||
} | |||
cstore.Create("Alice", "secret", sec) | |||
cstore.Create("Carl", "mitm", ed) | |||
info, _ := cstore.List() | |||
for _, i := range info { | |||
fmt.Println(i.Name) | |||
} | |||
// We need to use passphrase to generate a signature | |||
tx := keys.NewMockSignable([]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") | |||
if !binfo.PubKey.Equals(bob.PubKey) { | |||
fmt.Println("Get and Create return different keys") | |||
} | |||
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: | |||
// Bob | |||
// Alice | |||
// Bob | |||
// Carl | |||
// signed by Bob | |||
} |
@ -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,202 @@ | |||
package keys | |||
import ( | |||
"fmt" | |||
"strings" | |||
"github.com/pkg/errors" | |||
crypto "github.com/tendermint/go-crypto" | |||
"github.com/tendermint/go-crypto/keys/words" | |||
dbm "github.com/tendermint/tmlibs/db" | |||
) | |||
// 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 to 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 string, algo CryptoAlgo) (Info, string, 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) | |||
priv, err := generate(algo, secret) | |||
if err != nil { | |||
return Info{}, "", err | |||
} | |||
// encrypt and persist the key | |||
info := kb.writeKey(priv, name, passphrase) | |||
// we append the type byte to the serialized secret to help with | |||
// recovery | |||
// ie [secret] = [type] + [secret] | |||
typ := cryptoAlgoToByte(algo) | |||
secret = append([]byte{typ}, secret...) | |||
// return the mnemonic phrase | |||
words, err := kb.codec.BytesToWords(secret) | |||
seed := strings.Join(words, " ") | |||
return info, seed, 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, seedphrase string) (Info, error) { | |||
words := strings.Split(strings.TrimSpace(seedphrase), " ") | |||
secret, err := kb.codec.WordsToBytes(words) | |||
if err != nil { | |||
return Info{}, err | |||
} | |||
// secret is comprised of the actual secret with the type | |||
// appended. | |||
// ie [secret] = [type] + [secret] | |||
typ, secret := secret[0], secret[1:] | |||
algo := byteToCryptoAlgo(typ) | |||
priv, err := generate(algo, secret) | |||
if err != nil { | |||
return Info{}, err | |||
} | |||
// encrypt and persist key. | |||
public := kb.writeKey(priv, name, passphrase) | |||
return public, err | |||
} | |||
// 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() | |||
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(infoKey(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. | |||
func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signature, pub crypto.PubKey, err error) { | |||
info, err := kb.Get(name) | |||
if err != nil { | |||
return | |||
} | |||
priv, err := unarmorDecryptPrivKey(info.PrivKeyArmor, passphrase) | |||
if err != nil { | |||
return | |||
} | |||
sig = priv.Sign(msg) | |||
pub = priv.PubKey() | |||
return | |||
} | |||
func (kb dbKeybase) Export(name string) (armor string, err error) { | |||
bz := kb.db.Get(infoKey(name)) | |||
if bz == nil { | |||
return "", errors.New("No key to export with name " + name) | |||
} | |||
return armorInfoBytes(bz), nil | |||
} | |||
func (kb dbKeybase) Import(name string, armor string) (err error) { | |||
bz := kb.db.Get(infoKey(name)) | |||
if len(bz) > 0 { | |||
return errors.New("Cannot overwrite data for name " + name) | |||
} | |||
infoBytes, err := unarmorInfoBytes(armor) | |||
if err != nil { | |||
return | |||
} | |||
kb.db.Set(infoKey(name), infoBytes) | |||
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 | |||
info, err := kb.Get(name) | |||
if err != nil { | |||
return err | |||
} | |||
_, err = unarmorDecryptPrivKey(info.PrivKeyArmor, passphrase) | |||
if err != nil { | |||
return err | |||
} | |||
kb.db.DeleteSync(infoKey(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 { | |||
info, err := kb.Get(name) | |||
if err != nil { | |||
return err | |||
} | |||
key, err := unarmorDecryptPrivKey(info.PrivKeyArmor, oldpass) | |||
if err != nil { | |||
return err | |||
} | |||
kb.writeKey(key, name, newpass) | |||
return nil | |||
} | |||
func (kb dbKeybase) writeKey(priv crypto.PrivKey, name, passphrase string) Info { | |||
// generate the encrypted privkey | |||
privArmor := encryptArmorPrivKey(priv, passphrase) | |||
// make Info | |||
info := newInfo(name, priv.PubKey(), privArmor) | |||
// write them both | |||
kb.db.SetSync(infoKey(name), info.bytes()) | |||
return info | |||
} | |||
func generate(algo CryptoAlgo, secret []byte) (crypto.PrivKey, error) { | |||
switch algo { | |||
case AlgoEd25519: | |||
return crypto.GenPrivKeyEd25519FromSecret(secret).Wrap(), nil | |||
case AlgoSecp256k1: | |||
return crypto.GenPrivKeySecp256k1FromSecret(secret).Wrap(), nil | |||
default: | |||
err := errors.Errorf("Cannot generate keys for algorithm: %s", algo) | |||
return crypto.PrivKey{}, err | |||
} | |||
} | |||
func infoKey(name string) []byte { | |||
return []byte(fmt.Sprintf("%s.info", name)) | |||
} |
@ -0,0 +1,380 @@ | |||
package keys_test | |||
import ( | |||
"fmt" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
dbm "github.com/tendermint/tmlibs/db" | |||
"github.com/tendermint/go-crypto" | |||
"github.com/tendermint/go-crypto/keys" | |||
"github.com/tendermint/go-crypto/keys/words" | |||
) | |||
// TestKeyManagement makes sure we can manipulate these keys well | |||
func TestKeyManagement(t *testing.T) { | |||
// make the storage with reasonable defaults | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
algo := keys.AlgoEd25519 | |||
n1, n2, n3 := "personal", "business", "other" | |||
p1, p2 := "1234", "really-secure!@#$" | |||
// Check empty state | |||
l, err := cstore.List() | |||
require.Nil(t, err) | |||
assert.Empty(t, l) | |||
// create some keys | |||
_, err = cstore.Get(n1) | |||
assert.NotNil(t, err) | |||
i, _, err := cstore.Create(n1, p1, algo) | |||
require.Equal(t, n1, i.Name) | |||
require.Nil(t, err) | |||
_, _, err = cstore.Create(n2, p2, algo) | |||
require.Nil(t, err) | |||
// we can get these keys | |||
i2, err := cstore.Get(n2) | |||
assert.Nil(t, err) | |||
_, err = cstore.Get(n3) | |||
assert.NotNil(t, err) | |||
// list shows them in order | |||
keyS, err := cstore.List() | |||
require.Nil(t, err) | |||
require.Equal(t, 2, len(keyS)) | |||
// note these are in alphabetical order | |||
assert.Equal(t, n2, keyS[0].Name) | |||
assert.Equal(t, n1, keyS[1].Name) | |||
assert.Equal(t, i2.PubKey, keyS[0].PubKey) | |||
// deleting a key removes it | |||
err = cstore.Delete("bad name", "foo") | |||
require.NotNil(t, err) | |||
err = cstore.Delete(n1, p1) | |||
require.Nil(t, err) | |||
keyS, err = cstore.List() | |||
require.Nil(t, err) | |||
assert.Equal(t, 1, len(keyS)) | |||
_, err = cstore.Get(n1) | |||
assert.NotNil(t, err) | |||
// make sure that it only signs with the right password | |||
// tx := mock.NewSig([]byte("mytransactiondata")) | |||
// err = cstore.Sign(n2, p1, tx) | |||
// assert.NotNil(t, err) | |||
// err = cstore.Sign(n2, p2, tx) | |||
// assert.Nil(t, err, "%+v", err) | |||
// sigs, err := tx.Signers() | |||
// assert.Nil(t, err, "%+v", err) | |||
// if assert.Equal(t, 1, len(sigs)) { | |||
// assert.Equal(t, i2.PubKey, sigs[0]) | |||
// } | |||
} | |||
// TestSignVerify does some detailed checks on how we sign and validate | |||
// signatures | |||
func TestSignVerify(t *testing.T) { | |||
// make the storage with reasonable defaults | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
algo := keys.AlgoSecp256k1 | |||
n1, n2 := "some dude", "a dudette" | |||
p1, p2 := "1234", "foobar" | |||
// create two users and get their info | |||
i1, _, err := cstore.Create(n1, p1, algo) | |||
require.Nil(t, err) | |||
i2, _, err := cstore.Create(n2, p2, algo) | |||
require.Nil(t, 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, pub1, err := cstore.Sign(n1, p1, d1) | |||
require.Nil(t, err) | |||
require.Equal(t, i1.PubKey, pub1) | |||
s12, pub1, err := cstore.Sign(n1, p1, d2) | |||
require.Nil(t, err) | |||
require.Equal(t, i1.PubKey, pub1) | |||
s21, pub2, err := cstore.Sign(n2, p2, d1) | |||
require.Nil(t, err) | |||
require.Equal(t, i2.PubKey, pub2) | |||
s22, pub2, err := cstore.Sign(n2, p2, d2) | |||
require.Nil(t, err) | |||
require.Equal(t, i2.PubKey, pub2) | |||
// let's try to validate and make sure it only works when everything is proper | |||
cases := []struct { | |||
key crypto.PubKey | |||
data []byte | |||
sig crypto.Signature | |||
valid bool | |||
}{ | |||
// proper matches | |||
{i1.PubKey, d1, s11, true}, | |||
// change data, pubkey, or signature leads to fail | |||
{i1.PubKey, d2, s11, false}, | |||
{i2.PubKey, d1, s11, false}, | |||
{i1.PubKey, d1, s21, false}, | |||
// make sure other successes | |||
{i1.PubKey, d2, s12, true}, | |||
{i2.PubKey, d1, s21, true}, | |||
{i2.PubKey, d2, s22, true}, | |||
} | |||
for i, tc := range cases { | |||
valid := tc.key.VerifyBytes(tc.data, tc.sig) | |||
assert.Equal(t, tc.valid, valid, "%d", i) | |||
} | |||
} | |||
/* | |||
// TestSignWithLedger makes sure we have ledger compatibility with | |||
// the crypto store. | |||
// | |||
// This test will only succeed with a ledger attached to the computer | |||
// and the cosmos app open | |||
func TestSignWithLedger(t *testing.T) { | |||
if os.Getenv("WITH_LEDGER") == "" { | |||
t.Skip("Set WITH_LEDGER to run code on real ledger") | |||
} | |||
// make the storage with reasonable defaults | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
n := "nano-s" | |||
p := "hard2hack" | |||
// create a nano user | |||
c, _, err := cstore.Create(n, p, nano.KeyLedgerEd25519) | |||
require.Nil(t, err, "%+v", err) | |||
assert.Equal(t, c.Key, n) | |||
_, ok := c.PubKey.Unwrap().(nano.PubKeyLedgerEd25519) | |||
require.True(t, ok) | |||
// make sure we can get it back | |||
info, err := cstore.Get(n) | |||
require.Nil(t, err, "%+v", err) | |||
assert.Equal(t, info.Key, n) | |||
key := info.PubKey | |||
require.False(t ,key.Empty()) | |||
require.True(t, key.Equals(c.PubKey)) | |||
// let's try to sign some messages | |||
d1 := []byte("welcome to cosmos") | |||
d2 := []byte("please turn on the app") | |||
// try signing both data with the ledger... | |||
s1, pub, err := cstore.Sign(n, p, d1) | |||
require.Nil(t, err) | |||
require.Equal(t, info.PubKey, pub) | |||
s2, pub, err := cstore.Sign(n, p, d2) | |||
require.Nil(t, err) | |||
require.Equal(t, info.PubKey, pub) | |||
// now, let's check those signatures work | |||
assert.True(t, key.VerifyBytes(d1, s1)) | |||
assert.True(t, key.VerifyBytes(d2, s2)) | |||
// and mismatched signatures don't | |||
assert.False(t, key.VerifyBytes(d1, s2)) | |||
} | |||
*/ | |||
func assertPassword(t *testing.T, cstore keys.Keybase, name, pass, badpass string) { | |||
err := cstore.Update(name, badpass, pass) | |||
assert.NotNil(t, err) | |||
err = cstore.Update(name, pass, pass) | |||
assert.Nil(t, err, "%+v", err) | |||
} | |||
// TestExportImport tests exporting and importing keys. | |||
func TestExportImport(t *testing.T) { | |||
// make the storage with reasonable defaults | |||
db := dbm.NewMemDB() | |||
cstore := keys.New( | |||
db, | |||
words.MustLoadCodec("english"), | |||
) | |||
info, _, err := cstore.Create("john", "passphrase", keys.AlgoEd25519) | |||
assert.Nil(t, err) | |||
assert.Equal(t, info.Name, "john") | |||
addr := info.PubKey.Address() | |||
john, err := cstore.Get("john") | |||
assert.Nil(t, err) | |||
assert.Equal(t, john.Name, "john") | |||
assert.Equal(t, john.PubKey.Address(), addr) | |||
armor, err := cstore.Export("john") | |||
assert.Nil(t, err) | |||
err = cstore.Import("john2", armor) | |||
assert.Nil(t, err) | |||
john2, err := cstore.Get("john2") | |||
assert.Nil(t, err) | |||
assert.Equal(t, john.PubKey.Address(), addr) | |||
assert.Equal(t, john.Name, "john") | |||
assert.Equal(t, john, john2) | |||
} | |||
// TestAdvancedKeyManagement verifies update, import, export functionality | |||
func TestAdvancedKeyManagement(t *testing.T) { | |||
// make the storage with reasonable defaults | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
algo := keys.AlgoSecp256k1 | |||
n1, n2 := "old-name", "new name" | |||
p1, p2 := "1234", "foobar" | |||
// make sure key works with initial password | |||
_, _, err := cstore.Create(n1, p1, algo) | |||
require.Nil(t, err, "%+v", err) | |||
assertPassword(t, cstore, n1, p1, p2) | |||
// update password requires the existing password | |||
err = cstore.Update(n1, "jkkgkg", p2) | |||
assert.NotNil(t, err) | |||
assertPassword(t, cstore, n1, p1, p2) | |||
// then it changes the password when correct | |||
err = cstore.Update(n1, p1, p2) | |||
assert.Nil(t, err) | |||
// p2 is now the proper one! | |||
assertPassword(t, cstore, n1, p2, p1) | |||
// exporting requires the proper name and passphrase | |||
_, err = cstore.Export(n1 + ".notreal") | |||
assert.NotNil(t, err) | |||
_, err = cstore.Export(" " + n1) | |||
assert.NotNil(t, err) | |||
_, err = cstore.Export(n1 + " ") | |||
assert.NotNil(t, err) | |||
_, err = cstore.Export("") | |||
assert.NotNil(t, err) | |||
exported, err := cstore.Export(n1) | |||
require.Nil(t, err, "%+v", err) | |||
// import succeeds | |||
err = cstore.Import(n2, exported) | |||
assert.Nil(t, err) | |||
// second import fails | |||
err = cstore.Import(n2, exported) | |||
assert.NotNil(t, err) | |||
} | |||
// TestSeedPhrase verifies restoring from a seed phrase | |||
func TestSeedPhrase(t *testing.T) { | |||
// make the storage with reasonable defaults | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
algo := keys.AlgoEd25519 | |||
n1, n2 := "lost-key", "found-again" | |||
p1, p2 := "1234", "foobar" | |||
// make sure key works with initial password | |||
info, seed, err := cstore.Create(n1, p1, algo) | |||
require.Nil(t, err, "%+v", err) | |||
assert.Equal(t, n1, info.Name) | |||
assert.NotEmpty(t, seed) | |||
// now, let us delete this key | |||
err = cstore.Delete(n1, p1) | |||
require.Nil(t, err, "%+v", err) | |||
_, err = cstore.Get(n1) | |||
require.NotNil(t, err) | |||
// let us re-create it from the seed-phrase | |||
newInfo, err := cstore.Recover(n2, p2, seed) | |||
require.Nil(t, err, "%+v", err) | |||
assert.Equal(t, n2, newInfo.Name) | |||
assert.Equal(t, info.Address(), newInfo.Address()) | |||
assert.Equal(t, info.PubKey, newInfo.PubKey) | |||
} | |||
func ExampleNew() { | |||
// Select the encryption and storage for your cryptostore | |||
cstore := keys.New( | |||
dbm.NewMemDB(), | |||
words.MustLoadCodec("english"), | |||
) | |||
ed := keys.AlgoEd25519 | |||
sec := keys.AlgoSecp256k1 | |||
// Add keys and see they return in alphabetical order | |||
bob, _, err := cstore.Create("Bob", "friend", ed) | |||
if err != nil { | |||
// this should never happen | |||
fmt.Println(err) | |||
} else { | |||
// return info here just like in List | |||
fmt.Println(bob.Name) | |||
} | |||
cstore.Create("Alice", "secret", sec) | |||
cstore.Create("Carl", "mitm", ed) | |||
info, _ := cstore.List() | |||
for _, i := range info { | |||
fmt.Println(i.Name) | |||
} | |||
// We need to use passphrase to generate a signature | |||
tx := []byte("deadbeef") | |||
sig, pub, 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") | |||
if !binfo.PubKey.Equals(bob.PubKey) { | |||
fmt.Println("Get and Create return different keys") | |||
} | |||
if pub.Equals(binfo.PubKey) { | |||
fmt.Println("signed by Bob") | |||
} | |||
if !pub.VerifyBytes(tx, sig) { | |||
fmt.Println("invalid signature") | |||
} | |||
// Output: | |||
// Bob | |||
// Alice | |||
// Bob | |||
// Carl | |||
// signed by Bob | |||
} |
@ -0,0 +1,32 @@ | |||
package keys | |||
import "fmt" | |||
type CryptoAlgo string | |||
const ( | |||
AlgoEd25519 = CryptoAlgo("ed25519") | |||
AlgoSecp256k1 = CryptoAlgo("secp256k1") | |||
) | |||
func cryptoAlgoToByte(key CryptoAlgo) byte { | |||
switch key { | |||
case AlgoEd25519: | |||
return 0x01 | |||
case AlgoSecp256k1: | |||
return 0x02 | |||
default: | |||
panic(fmt.Sprintf("Unexpected type key %v", key)) | |||
} | |||
} | |||
func byteToCryptoAlgo(b byte) CryptoAlgo { | |||
switch b { | |||
case 0x01: | |||
return AlgoEd25519 | |||
case 0x02: | |||
return AlgoSecp256k1 | |||
default: | |||
panic(fmt.Sprintf("Unexpected type byte %X", b)) | |||
} | |||
} |
@ -0,0 +1,99 @@ | |||
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" | |||
blockTypeKeyInfo = "TENDERMINT KEY INFO" | |||
) | |||
func armorInfoBytes(bz []byte) string { | |||
header := map[string]string{ | |||
"type": "Info", | |||
"version": "0.0.0", | |||
} | |||
armorStr := crypto.EncodeArmor(blockTypeKeyInfo, header, bz) | |||
return armorStr | |||
} | |||
func unarmorInfoBytes(armorStr string) (bz []byte, err error) { | |||
blockType, header, bz, err := crypto.DecodeArmor(armorStr) | |||
if err != nil { | |||
return | |||
} | |||
if blockType != blockTypeKeyInfo { | |||
err = fmt.Errorf("Unrecognized armor type: %v", blockType) | |||
return | |||
} | |||
if header["version"] != "0.0.0" { | |||
err = fmt.Errorf("Unrecognized version: %v", header["version"]) | |||
return | |||
} | |||
return | |||
} | |||
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,56 @@ | |||
package keys | |||
import ( | |||
"fmt" | |||
"sort" | |||
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 | |||
type Info struct { | |||
Name string `json:"name"` | |||
Address data.Bytes `json:"address"` | |||
PubKey crypto.PubKey `json:"pubkey"` | |||
} | |||
func (i *Info) Format() Info { | |||
if !i.PubKey.Empty() { | |||
i.Address = i.PubKey.Address() | |||
} | |||
return *i | |||
} | |||
// 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) | |||
} | |||
} | |||
// 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 | |||
} | |||
// 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 string, algo CryptoAlgo) (info Info, seed string, err error) | |||
// Recover takes a seedphrase and loads in the key | |||
Recover(name, passphrase, seedphrase string) (info Info, erro error) | |||
List() ([]Info, error) | |||
Get(name string) (Info, error) | |||
Update(name, oldpass, newpass 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 | |||
Import(name string, armor string) (err error) | |||
Export(name string) (armor string, err error) | |||
} | |||
var _ Signable = &MockSignable{} | |||
// NewMockSignable sets the data to sign | |||
func NewMockSignable(data []byte) *MockSignable { | |||
return &MockSignable{Data: data} | |||
// Info is the public information about a key | |||
type Info struct { | |||
Name string `json:"name"` | |||
PubKey crypto.PubKey `json:"pubkey"` | |||
PrivKeyArmor string `json:"privkey.armor"` | |||
} | |||
// TxBytes returns the full data with signatures | |||
func (s *MockSignable) TxBytes() ([]byte, error) { | |||
return wire.BinaryBytes(s), nil | |||
func newInfo(name string, pub crypto.PubKey, privArmor string) Info { | |||
return Info{ | |||
Name: name, | |||
PubKey: pub, | |||
PrivKeyArmor: privArmor, | |||
} | |||
} | |||
// SignBytes returns the original data passed into `NewSig` | |||
func (s *MockSignable) SignBytes() []byte { | |||
return s.Data | |||
// Address is a helper function to calculate the address from the pubkey | |||
func (i Info) Address() []byte { | |||
return i.PubKey.Address() | |||
} | |||
// 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 | |||
func (i Info) bytes() []byte { | |||
bz, err := wire.MarshalBinary(i) | |||
if err != nil { | |||
panic(err) | |||
} | |||
return bz | |||
} | |||
// 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 | |||
func readInfo(bz []byte) (info Info, err error) { | |||
err = wire.UnmarshalBinary(bz, &info) | |||
return | |||
} |
@ -1,3 +1,3 @@ | |||
package crypto | |||
const Version = "0.4.1" | |||
const Version = "0.5.0" |