diff --git a/keys/cryptostore/enc_storage.go b/keys/cryptostore/enc_storage.go index daeb220b5..e0c7e59a3 100644 --- a/keys/cryptostore/enc_storage.go +++ b/keys/cryptostore/enc_storage.go @@ -12,21 +12,21 @@ type encryptedStorage struct { } func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error { - secret, err := es.coder.Encrypt(key, pass) + saltBytes, encBytes, err := es.coder.Encrypt(key, pass) if err != nil { return err } ki := info(name, key) - return es.store.Put(name, secret, ki) + return es.store.Put(name, saltBytes, encBytes, ki) } func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.Info, error) { - secret, info, err := es.store.Get(name) + saltBytes, encBytes, info, err := es.store.Get(name) if err != nil { return crypto.PrivKey{}, info, err } - key, err := es.coder.Decrypt(secret, pass) + key, err := es.coder.Decrypt(saltBytes, encBytes, pass) return key, info, err } diff --git a/keys/cryptostore/encoder.go b/keys/cryptostore/encoder.go index 31cbc2e54..251543b60 100644 --- a/keys/cryptostore/encoder.go +++ b/keys/cryptostore/encoder.go @@ -2,7 +2,9 @@ package cryptostore import ( "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" + "github.com/tendermint/go-crypto/bcrypt" ) var ( @@ -16,45 +18,54 @@ var ( // // 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)) + Encrypt(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) + Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) } type secretbox struct{} -func (e secretbox) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { - if pass == "" { - return key.Bytes(), nil +func (e secretbox) Encrypt(privKey crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) { + if passphrase == "" { + return nil, privKey.Bytes(), nil + } + + saltBytes = crypto.CRandBytes(16) + key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 14) // TODO parameterize. 14 is good today (2016) + if err != nil { + return nil, nil, errors.Wrap(err, "Couldn't generate bcrypt key from passphrase.") } - s := secret(pass) - cipher := crypto.EncryptSymmetric(key.Bytes(), s) - return cipher, nil + key = crypto.Sha256(key) // Get 32 bytes + privKeyBytes := privKey.Bytes() + return saltBytes, crypto.EncryptSymmetric(privKeyBytes, key), 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) +func (e secretbox) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { + privKeyBytes := encBytes + // NOTE: Some keys weren't encrypted with a passphrase and hence we have the conditional + if passphrase != "" { + key, err := bcrypt.GenerateFromPassword(saltBytes, []byte(passphrase), 14) // TODO parameterize. 14 is good today (2016) if err != nil { return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") } + key = crypto.Sha256(key) // Get 32 bytes + privKeyBytes, err = crypto.DecryptSymmetric(encBytes, key) + if err != nil { + return crypto.PrivKey{}, errors.Wrap(err, "Invalid Passphrase") + } + } + privKey, err = crypto.PrivKeyFromBytes(privKeyBytes) + if err != nil { + return crypto.PrivKey{}, errors.Wrap(err, "Couldn't get privKey from bytes") } - key, err = crypto.PrivKeyFromBytes(private) - return key, errors.Wrap(err, "Invalid Passphrase") + return privKey, nil } type noop struct{} -func (n noop) Encrypt(key crypto.PrivKey, pass string) ([]byte, error) { - return key.Bytes(), nil +func (n noop) Encrypt(key crypto.PrivKey, passphrase string) (saltBytes []byte, encBytes []byte, err error) { + return []byte{}, key.Bytes(), nil } -func (n noop) Decrypt(data []byte, pass string) (crypto.PrivKey, error) { - return crypto.PrivKeyFromBytes(data) +func (n noop) Decrypt(saltBytes []byte, encBytes []byte, passphrase string) (privKey crypto.PrivKey, err error) { + return crypto.PrivKeyFromBytes(encBytes) } diff --git a/keys/cryptostore/encoder_test.go b/keys/cryptostore/encoder_test.go index f468591f3..ce1118d20 100644 --- a/keys/cryptostore/encoder_test.go +++ b/keys/cryptostore/encoder_test.go @@ -18,22 +18,22 @@ func TestNoopEncoder(t *testing.T) { key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) key2 := cryptostore.GenSecp256k1.Generate(cmn.RandBytes(16)) - b, err := noop.Encrypt(key, "encode") + _, b, err := noop.Encrypt(key, "encode") require.Nil(err) assert.NotEmpty(b) - b2, err := noop.Encrypt(key2, "encode") + _, 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") + pk, err := noop.Decrypt(nil, b, "decode") require.Nil(err) require.NotNil(pk) assert.Equal(key, pk) - pk2, err := noop.Decrypt(b2, "kggugougp") + pk2, err := noop.Decrypt(nil, b2, "kggugougp") require.Nil(err) require.NotNil(pk2) assert.Equal(key2, pk2) @@ -46,17 +46,17 @@ func TestSecretBox(t *testing.T) { key := cryptostore.GenEd25519.Generate(cmn.RandBytes(16)) pass := "some-special-secret" - b, err := enc.Encrypt(key, pass) + s, 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") + pk, err := enc.Decrypt(s, 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) + pk, err = enc.Decrypt(s, b, pass) require.Nil(err) assert.Equal(key, pk) } @@ -80,11 +80,11 @@ func TestSecretBoxNoPass(t *testing.T) { } for i, tc := range cases { - b, err := enc.Encrypt(key, tc.encode) + s, 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) + pk, err := enc.Decrypt(s, b, tc.decode) if tc.valid { require.Nil(err, "%d: %+v", i, err) assert.Equal(key, pk, "%d", i) @@ -95,7 +95,7 @@ func TestSecretBoxNoPass(t *testing.T) { // now let's make sure raw bytes also work... b := key.Bytes() - pk, err := enc.Decrypt(b, "") + pk, err := enc.Decrypt(nil, b, "") require.Nil(err, "%+v", err) assert.Equal(key, pk) } diff --git a/keys/cryptostore/generator.go b/keys/cryptostore/generator.go index 0a2bb55c2..1f162ec08 100644 --- a/keys/cryptostore/generator.go +++ b/keys/cryptostore/generator.go @@ -2,6 +2,7 @@ package cryptostore import ( "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" ) diff --git a/keys/cryptostore/holder.go b/keys/cryptostore/holder.go index 18437a9b5..2fc617b4e 100644 --- a/keys/cryptostore/holder.go +++ b/keys/cryptostore/holder.go @@ -100,7 +100,7 @@ func (s Manager) List() (keys.Infos, error) { // Get returns the public information about one key func (s Manager) Get(name string) (keys.Info, error) { - _, info, err := s.es.store.Get(name) + _, _, info, err := s.es.store.Get(name) return info, err } @@ -124,21 +124,23 @@ func (s Manager) Sign(name, passphrase string, tx keys.Signable) error { // // 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) { +// TODO: How to handle Export with salt? +func (s Manager) Export(name, oldpass, transferpass string) (salt, data []byte, err error) { key, _, err := s.es.Get(name, oldpass) if err != nil { - return nil, err + return nil, nil, err } - res, err := s.es.coder.Encrypt(key, transferpass) - return res, err + salt, data, err = s.es.coder.Encrypt(key, transferpass) + return salt, data, 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 +// If they are valid, it stores the key 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) +// TODO: How to handle Import with salt? +func (s Manager) Import(name, newpass, transferpass string, salt, data []byte) error { + key, err := s.es.coder.Decrypt(salt, data, transferpass) if err != nil { return err } diff --git a/keys/cryptostore/holder_test.go b/keys/cryptostore/holder_test.go index 434966f46..7484b1517 100644 --- a/keys/cryptostore/holder_test.go +++ b/keys/cryptostore/holder_test.go @@ -168,7 +168,7 @@ func TestImportUnencrypted(t *testing.T) { pass := "top-secret" // import raw bytes - err := cstore.Import(name, pass, "", key.Bytes()) + err := cstore.Import(name, pass, "", nil, key.Bytes()) require.Nil(err, "%+v", err) // make sure the address matches @@ -209,15 +209,15 @@ func TestAdvancedKeyManagement(t *testing.T) { assertPassword(assert, cstore, n1, p2, p1) // exporting requires the proper name and passphrase - _, err = cstore.Export(n2, p2, pt) + _, _, err = cstore.Export(n2, p2, pt) assert.NotNil(err) - _, err = cstore.Export(n1, p1, pt) + _, _, err = cstore.Export(n1, p1, pt) assert.NotNil(err) - exported, err := cstore.Export(n1, p2, pt) + salt, 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) + err = cstore.Import(n2, p3, p2, salt, exported) assert.NotNil(err) } diff --git a/keys/cryptostore/storage_test.go b/keys/cryptostore/storage_test.go index 907a19f11..468435143 100644 --- a/keys/cryptostore/storage_test.go +++ b/keys/cryptostore/storage_test.go @@ -5,9 +5,9 @@ import ( "github.com/stretchr/testify/assert" - crypto "github.com/tendermint/go-crypto" cmn "github.com/tendermint/tmlibs/common" + crypto "github.com/tendermint/go-crypto" keys "github.com/tendermint/go-crypto/keys" ) diff --git a/keys/storage.go b/keys/storage.go index 0c25eb8a5..0aba4ebdb 100644 --- a/keys/storage.go +++ b/keys/storage.go @@ -3,8 +3,8 @@ package keys // Storage has many implementation, based on security and sharing requirements // like disk-backed, mem-backed, vault, db, etc. type Storage interface { - Put(name string, key []byte, info Info) error - Get(name string) ([]byte, Info, error) + Put(name string, salt []byte, key []byte, info Info) error + Get(name string) (salt []byte, key []byte, info Info, err error) List() (Infos, error) Delete(name string) error } diff --git a/keys/storage/filestorage/main.go b/keys/storage/filestorage/main.go index 696b200fc..bd29ce41f 100644 --- a/keys/storage/filestorage/main.go +++ b/keys/storage/filestorage/main.go @@ -6,6 +6,7 @@ like standard ssh key storage. package filestorage import ( + "encoding/hex" "fmt" "io/ioutil" "os" @@ -13,19 +14,25 @@ import ( "strings" "github.com/pkg/errors" + crypto "github.com/tendermint/go-crypto" keys "github.com/tendermint/go-crypto/keys" ) const ( + // BlockType is the type of block. BlockType = "Tendermint Light Client" - PrivExt = "tlc" - PubExt = "pub" - keyPerm = os.FileMode(0600) - pubPerm = os.FileMode(0644) - dirPerm = os.FileMode(0700) + // 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) ) +// FileStore is a file-based key storage with tight permissions. type FileStore struct { keyDir string } @@ -36,9 +43,11 @@ type FileStore struct { // 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} } @@ -49,7 +58,7 @@ func (s FileStore) assertStorage() keys.Storage { // 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 { +func (s FileStore) Put(name string, salt, key []byte, info keys.Info) error { pub, priv := s.nameToPaths(name) // write public info @@ -59,31 +68,34 @@ func (s FileStore) Put(name string, key []byte, info keys.Info) error { } // write private info - return write(priv, name, key) + return write(priv, name, salt, 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) { +func (s FileStore) Get(name string) (salt []byte, key []byte, info keys.Info, err error) { pub, priv := s.nameToPaths(name) - info, err := readInfo(pub) + info, err = readInfo(pub) if err != nil { - return nil, info, err + return nil, nil, info, err } - key, _, err := read(priv) - return key, info.Format(), err + salt, key, _, err = read(priv) + return salt, 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) + defer dir.Close() + if err != nil { return nil, errors.Wrap(err, "List Keys") } + names, err := dir.Readdirnames(0) if err != nil { return nil, errors.Wrap(err, "List Keys") @@ -111,61 +123,121 @@ func (s FileStore) List() (keys.Infos, error) { 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()) + return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) } func readInfo(path string) (info keys.Info, err error) { - var data []byte - data, info.Name, err = read(path) + f, err := os.Open(path) + defer f.Close() + if err != nil { - return + return info, errors.Wrap(err, "Reading data") } - pk, err := crypto.PubKeyFromBytes(data) + + d, err := ioutil.ReadAll(f) + if err != nil { + return info, errors.Wrap(err, "Reading data") + } + + block, headers, key, err := crypto.DecodeArmor(string(d)) + if err != nil { + return info, errors.Wrap(err, "Invalid Armor") + } + + if block != BlockType { + return info, errors.Errorf("Unknown key type: %s", block) + } + + pk, _ := crypto.PubKeyFromBytes(key) + info.Name = headers["name"] info.PubKey = pk - return + + return info, nil } -func read(path string) ([]byte, string, error) { +func read(path string) (salt, key []byte, name string, err error) { f, err := os.Open(path) + defer f.Close() + if err != nil { - return nil, "", errors.Wrap(err, "Reading data") + return nil, nil, "", errors.Wrap(err, "Reading data") } + d, err := ioutil.ReadAll(f) if err != nil { - return nil, "", errors.Wrap(err, "Reading data") + return nil, nil, "", errors.Wrap(err, "Reading data") } + block, headers, key, err := crypto.DecodeArmor(string(d)) if err != nil { - return nil, "", errors.Wrap(err, "Invalid Armor") + return nil, nil, "", errors.Wrap(err, "Invalid Armor") } + if block != BlockType { - return nil, "", errors.Errorf("Unknown key type: %s", block) + return nil, nil, "", errors.Errorf("Unknown key type: %s", block) + } + + if headers["kdf"] != "bcrypt" { + return nil, nil, "", errors.Errorf("Unrecognized KDF type: %v", headers["kdf"]) + } + + if headers["salt"] == "" { + return nil, nil, "", errors.Errorf("Missing salt bytes") } - return key, headers["name"], nil + + salt, err = hex.DecodeString(headers["salt"]) + if err != nil { + return nil, nil, "", errors.Errorf("Error decoding salt: %v", err.Error()) + } + + return salt, key, headers["name"], nil } -func write(path, name string, key []byte) error { +func writeInfo(path string, info keys.Info) error { f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm) + defer f.Close() + if err != nil { return errors.Wrap(err, "Writing data") } + + headers := map[string]string{"name": info.Name} + text := crypto.EncodeArmor(BlockType, headers, info.PubKey.Bytes()) + _, err = f.WriteString(text) + + return errors.Wrap(err, "Writing data") +} + +func write(path, name string, salt, key []byte) error { + f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, keyPerm) defer f.Close() - headers := map[string]string{"name": name} + + if err != nil { + return errors.Wrap(err, "Writing data") + } + + headers := map[string]string{ + "name": name, + "kdf": "bcrypt", + "salt": fmt.Sprintf("%X", salt), + } + text := crypto.EncodeArmor(BlockType, headers, key) _, err = f.WriteString(text) + return errors.Wrap(err, "Writing data") } diff --git a/keys/storage/filestorage/main_test.go b/keys/storage/filestorage/main_test.go index 28c950c2c..bed46ab1f 100644 --- a/keys/storage/filestorage/main_test.go +++ b/keys/storage/filestorage/main_test.go @@ -22,6 +22,7 @@ func TestBasicCRUD(t *testing.T) { name := "bar" key := []byte("secret-key-here") + salt := []byte("salt-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() info := keys.Info{ Name: name, @@ -29,7 +30,7 @@ func TestBasicCRUD(t *testing.T) { } // No data: Get and Delete return nothing - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) err = store.Delete(name) assert.NotNil(err) @@ -39,14 +40,14 @@ func TestBasicCRUD(t *testing.T) { assert.Empty(l) // Putting the key in the store must work - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.Nil(err) // But a second time is a failure - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.NotNil(err) // Now, we can get and list properly - k, i, err := store.Get(name) + _, k, i, err := store.Get(name) require.Nil(err, "%+v", err) assert.Equal(key, k) assert.Equal(info.Name, i.Name) @@ -58,7 +59,7 @@ func TestBasicCRUD(t *testing.T) { assert.Equal(i, l[0]) // querying a non-existent key fails - _, _, err = store.Get("badname") + _, _, _, err = store.Get("badname") assert.NotNil(err) // We can only delete once @@ -68,7 +69,7 @@ func TestBasicCRUD(t *testing.T) { assert.NotNil(err) // and then Get and List don't work - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) // List returns empty list l, err = store.List() diff --git a/keys/storage/memstorage/main.go b/keys/storage/memstorage/main.go index 195fa7a17..ddc46afc9 100644 --- a/keys/storage/memstorage/main.go +++ b/keys/storage/memstorage/main.go @@ -7,11 +7,13 @@ package memstorage import ( "github.com/pkg/errors" + keys "github.com/tendermint/go-crypto/keys" ) type data struct { info keys.Info + salt []byte key []byte } @@ -29,22 +31,22 @@ func (s MemStore) assertStorage() keys.Storage { // 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 { +func (s MemStore) Put(name string, salt, 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} + s[name] = data{info, salt, 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 +func (s MemStore) Get(name string) (salt, key []byte, info keys.Info, 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 + + return d.salt, d.key, d.info.Format(), err } // List returns the public info of all keys in the MemStore in unsorted order diff --git a/keys/storage/memstorage/main_test.go b/keys/storage/memstorage/main_test.go index feccb387f..01975df58 100644 --- a/keys/storage/memstorage/main_test.go +++ b/keys/storage/memstorage/main_test.go @@ -14,6 +14,7 @@ func TestBasicCRUD(t *testing.T) { name := "foo" key := []byte("secret-key-here") + salt := []byte("salt-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() info := keys.Info{ Name: name, @@ -21,7 +22,7 @@ func TestBasicCRUD(t *testing.T) { } // No data: Get and Delete return nothing - _, _, err := store.Get(name) + _, _, _, err := store.Get(name) assert.NotNil(err) err = store.Delete(name) assert.NotNil(err) @@ -31,14 +32,14 @@ func TestBasicCRUD(t *testing.T) { assert.Empty(l) // Putting the key in the store must work - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.Nil(err) // But a second time is a failure - err = store.Put(name, key, info) + err = store.Put(name, salt, key, info) assert.NotNil(err) // Now, we can get and list properly - k, i, err := store.Get(name) + _, k, i, err := store.Get(name) assert.Nil(err) assert.Equal(key, k) assert.Equal(info.Name, i.Name) @@ -50,7 +51,7 @@ func TestBasicCRUD(t *testing.T) { assert.Equal(i, l[0]) // querying a non-existent key fails - _, _, err = store.Get("badname") + _, _, _, err = store.Get("badname") assert.NotNil(err) // We can only delete once @@ -60,7 +61,7 @@ func TestBasicCRUD(t *testing.T) { assert.NotNil(err) // and then Get and List don't work - _, _, err = store.Get(name) + _, _, _, err = store.Get(name) assert.NotNil(err) // List returns empty list l, err = store.List()