diff --git a/Makefile b/Makefile index 7a1f90fd..b15317b2 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,5 @@ test: go test ./... + +install: + go install ./cmd/keys diff --git a/cmd/list.go b/cmd/list.go index e2c8a763..44fefb8d 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -18,7 +18,6 @@ import ( "fmt" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // listCmd represents the list command @@ -28,22 +27,16 @@ var listCmd = &cobra.Command{ Long: `Return a list of all public keys stored by this key manager along with their associated name and address.`, Run: func(cmd *cobra.Command, args []string) { - // TODO: Work your own magic here - fmt.Println("list called") - fmt.Println(viper.Get("format")) + infos, err := manager.List() + if err != nil { + fmt.Println(err.Error()) + return + } + + printInfos(infos) }, } func init() { RootCmd.AddCommand(listCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // listCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - listCmd.Flags().StringP("format", "f", "text", "Format to display (text|json)") } diff --git a/cmd/new.go b/cmd/new.go index 40462e68..38d9762f 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -22,28 +22,46 @@ import ( // newCmd represents the new command var newCmd = &cobra.Command{ - Use: "new", + Use: "new ", Short: "Create a new public/private key pair", Long: `Add a public/private key pair to the key store. The password muts be entered in the terminal and not passed as a command line argument for security.`, - Run: func(cmd *cobra.Command, args []string) { - // TODO: Work your own magic here - fmt.Println("new called") - }, + Run: newPassword, } func init() { RootCmd.AddCommand(newCmd) - - // Here you will define your flags and configuration settings. - - // Cobra supports Persistent Flags which will work for this command - // and all subcommands, e.g.: - // newCmd.PersistentFlags().String("foo", "", "A help for foo") - - // Cobra supports local flags which will only run when this command - // is called directly, e.g.: - // newCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") - +} + +func newPassword(cmd *cobra.Command, args []string) { + if len(args) != 1 || len(args[0]) == 0 { + fmt.Print("You must provide a name for the key") + return + } + name := args[0] + + // TODO: own function??? + pass, err := getPassword("Enter a passphrase:") + if err != nil { + fmt.Println(err.Error()) + return + } + pass2, err := getPassword("Repeat the passphrase:") + if err != nil { + fmt.Println(err.Error()) + return + } + if pass != pass2 { + fmt.Println("Passphrases don't match") + return + } + + info, err := manager.Create(name, pass) + if err != nil { + fmt.Println(err.Error()) + return + } + + printInfo(info) } diff --git a/cmd/root.go b/cmd/root.go index 05fffd48..9d7ae9a9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,16 +17,22 @@ package cmd import ( "fmt" "os" + "path/filepath" "strings" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" + keys "github.com/tendermint/go-keys" + "github.com/tendermint/go-keys/cryptostore" + "github.com/tendermint/go-keys/storage/filestorage" ) var ( rootDir string - format string + output string + keyDir string + manager keys.Manager ) // RootCmd represents the base command when called without any subcommands @@ -53,7 +59,8 @@ func Execute() { func init() { cobra.OnInitialize(initEnv) RootCmd.PersistentFlags().StringP("root", "r", os.ExpandEnv("$HOME/.tlc"), "root directory for config and data (default is TM_ROOT or $HOME/.tlc)") - RootCmd.PersistentFlags().StringP("format", "f", "text", "Output format (text|json)") + RootCmd.PersistentFlags().StringP("output", "o", "text", "Output format (text|json)") + RootCmd.PersistentFlags().StringP("keydir", "", "keys", "Directory to store private keys (subdir of root)") } // initEnv sets to use ENV variables if set. @@ -85,11 +92,25 @@ func bindFlags(cmd *cobra.Command, args []string) error { // validateFlags asserts all RootCmd flags are valid func validateFlags(cmd *cobra.Command) error { - format = viper.GetString("format") - switch format { + // validate output format + output = viper.GetString("output") + switch output { case "text", "json": - return nil default: - return errors.Errorf("Unsupported format: %s", format) + return errors.Errorf("Unsupported output format: %s", output) } + + // store the keys directory + keyDir = viper.GetString("keydir") + if !filepath.IsAbs(keyDir) { + keyDir = filepath.Join(rootDir, keyDir) + } + // and construct the key manager + manager = cryptostore.New( + cryptostore.GenEd25519, // TODO - cli switch??? + cryptostore.SecretBox, + filestorage.New(keyDir), + ) + + return nil } diff --git a/cmd/utils.go b/cmd/utils.go new file mode 100644 index 00000000..bd067f9c --- /dev/null +++ b/cmd/utils.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + + "github.com/bgentry/speakeasy" + "github.com/pkg/errors" + data "github.com/tendermint/go-data" + keys "github.com/tendermint/go-keys" +) + +const PassLength = 10 + +func getPassword(prompt string) (string, error) { + pass, err := speakeasy.Ask(prompt) + if err != nil { + return "", err + } + if len(pass) < PassLength { + return "", errors.Errorf("Password must be at least %d characters", PassLength) + } + return pass, nil +} + +func printInfo(info keys.Info) { + switch output { + case "text": + key, err := data.ToText(info.PubKey) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Printf("%s\t%s\n", info.Name, key) + case "json": + json, err := data.ToJSON(info) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} + +func printInfos(infos keys.Infos) { + switch output { + case "text": + fmt.Println("All keys:") + for _, i := range infos { + printInfo(i) + } + case "json": + json, err := data.ToJSON(infos) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} diff --git a/cryptostore/enc_storage.go b/cryptostore/enc_storage.go index 0a935075..759ca90e 100644 --- a/cryptostore/enc_storage.go +++ b/cryptostore/enc_storage.go @@ -21,7 +21,7 @@ func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error { return es.store.Put(name, secret, ki) } -func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.KeyInfo, error) { +func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.Info, error) { secret, info, err := es.store.Get(name) if err != nil { return nil, info, err @@ -30,7 +30,7 @@ func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.KeyInfo, return key, info, err } -func (es encryptedStorage) List() ([]keys.KeyInfo, error) { +func (es encryptedStorage) List() (keys.Infos, error) { return es.store.List() } @@ -39,8 +39,8 @@ func (es encryptedStorage) Delete(name string) error { } // info hardcodes the encoding of keys -func info(name string, key crypto.PrivKey) keys.KeyInfo { - return keys.KeyInfo{ +func info(name string, key crypto.PrivKey) keys.Info { + return keys.Info{ Name: name, PubKey: crypto.PubKeyS{key.PubKey()}, } diff --git a/cryptostore/holder.go b/cryptostore/holder.go index 4beb6416..29682a87 100644 --- a/cryptostore/holder.go +++ b/cryptostore/holder.go @@ -24,28 +24,28 @@ func (s Manager) assertSigner() keys.Signer { return s } -// exists just to make sure we fulfill the KeyManager interface -func (s Manager) assertKeyManager() keys.KeyManager { +// exists just to make sure we fulfill the Manager interface +func (s Manager) assertKeyManager() keys.Manager { return s } // Create adds a new key to the storage engine, returning error if // another key already stored under this name -func (s Manager) Create(name, passphrase string) error { +func (s Manager) Create(name, passphrase string) (keys.Info, error) { key := s.gen.Generate() - return s.es.Put(name, passphrase, key) + 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.KeyInfos, error) { - k, err := s.es.List() - res := keys.KeyInfos(k) +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.KeyInfo, error) { +func (s Manager) Get(name string) (keys.Info, error) { _, info, err := s.es.store.Get(name) return info, err } diff --git a/cryptostore/holder_test.go b/cryptostore/holder_test.go index fb5ef854..116b92c4 100644 --- a/cryptostore/holder_test.go +++ b/cryptostore/holder_test.go @@ -31,9 +31,10 @@ func TestKeyManagement(t *testing.T) { // create some keys _, err = cstore.Get(n1) assert.NotNil(err) - err = cstore.Create(n1, p1) + i, err := cstore.Create(n1, p1) + require.Equal(n1, i.Name) require.Nil(err) - err = cstore.Create(n2, p2) + _, err = cstore.Create(n2, p2) require.Nil(err) // we can get these keys @@ -159,7 +160,7 @@ func TestAdvancedKeyManagement(t *testing.T) { p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$" // make sure key works with initial password - err := cstore.Create(n1, p1) + _, err := cstore.Create(n1, p1) require.Nil(err, "%+v", err) assertPassword(assert, cstore, n1, p1, p2) diff --git a/cryptostore/storage_test.go b/cryptostore/storage_test.go index 6c6ea5e2..78d107c1 100644 --- a/cryptostore/storage_test.go +++ b/cryptostore/storage_test.go @@ -15,7 +15,7 @@ func TestSortKeys(t *testing.T) { // alphabetical order is n3, n1, n2 n1, n2, n3 := "john", "mike", "alice" - infos := keys.KeyInfos{ + infos := keys.Infos{ info(n1, gen()), info(n2, gen()), info(n3, gen()), diff --git a/keys.toml b/keys.toml index 41f4d528..f9eb95e1 100644 --- a/keys.toml +++ b/keys.toml @@ -1 +1,2 @@ -format = "text" +output = "text" +keydir = ".mykeys" diff --git a/storage.go b/storage.go index 519ce1aa..0c25eb8a 100644 --- a/storage.go +++ b/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 KeyInfo) error - Get(name string) ([]byte, KeyInfo, error) - List() ([]KeyInfo, error) + Put(name string, key []byte, info Info) error + Get(name string) ([]byte, Info, error) + List() (Infos, error) Delete(name string) error } diff --git a/storage/filestorage/main.go b/storage/filestorage/main.go index 554a3654..0396c953 100644 --- a/storage/filestorage/main.go +++ b/storage/filestorage/main.go @@ -49,7 +49,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.KeyInfo) error { +func (s FileStore) Put(name string, key []byte, info keys.Info) error { pub, priv := s.nameToPaths(name) // write public info @@ -62,10 +62,10 @@ func (s FileStore) Put(name string, key []byte, info keys.KeyInfo) error { return write(priv, name, key) } -// Get loads the keyinfo and (encoded) private key from the directory +// 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.KeyInfo, error) { +func (s FileStore) Get(name string) ([]byte, keys.Info, error) { pub, priv := s.nameToPaths(name) info, err := readInfo(pub) @@ -78,8 +78,8 @@ func (s FileStore) Get(name string) ([]byte, keys.KeyInfo, error) { } // List parses the key directory for public info and returns a list of -// KeyInfo for all keys located in this directory. -func (s FileStore) List() ([]keys.KeyInfo, error) { +// 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") @@ -91,7 +91,7 @@ func (s FileStore) List() ([]keys.KeyInfo, error) { // filter names for .pub ending and load them one by one // half the files is a good guess for pre-allocating the slice - infos := make([]keys.KeyInfo, 0, len(names)/2) + infos := make([]keys.Info, 0, len(names)/2) for _, name := range names { if strings.HasSuffix(name, PubExt) { p := path.Join(s.keyDir, name) @@ -124,11 +124,11 @@ func (s FileStore) nameToPaths(name string) (pub, priv string) { return path.Join(s.keyDir, pubName), path.Join(s.keyDir, privName) } -func writeInfo(path string, info keys.KeyInfo) error { +func writeInfo(path string, info keys.Info) error { return write(path, info.Name, info.PubKey.Bytes()) } -func readInfo(path string) (info keys.KeyInfo, err error) { +func readInfo(path string) (info keys.Info, err error) { var data []byte data, info.Name, err = read(path) if err != nil { diff --git a/storage/filestorage/main_test.go b/storage/filestorage/main_test.go index f81ba719..b27fecfb 100644 --- a/storage/filestorage/main_test.go +++ b/storage/filestorage/main_test.go @@ -23,7 +23,7 @@ func TestBasicCRUD(t *testing.T) { name := "bar" key := []byte("secret-key-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() - info := keys.KeyInfo{ + info := keys.Info{ Name: name, PubKey: crypto.PubKeyS{pubkey}, } diff --git a/storage/memstorage/main.go b/storage/memstorage/main.go index e6573eb9..9f671e48 100644 --- a/storage/memstorage/main.go +++ b/storage/memstorage/main.go @@ -11,7 +11,7 @@ import ( ) type data struct { - info keys.KeyInfo + info keys.Info key []byte } @@ -29,7 +29,7 @@ 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.KeyInfo) error { +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) } @@ -38,7 +38,7 @@ func (s MemStore) Put(name string, key []byte, info keys.KeyInfo) error { } // Get returns the key stored under the name, or returns an error if not present -func (s MemStore) Get(name string) ([]byte, keys.KeyInfo, error) { +func (s MemStore) Get(name string) ([]byte, keys.Info, error) { var err error d, ok := s[name] if !ok { @@ -48,8 +48,8 @@ func (s MemStore) Get(name string) ([]byte, keys.KeyInfo, error) { } // List returns the public info of all keys in the MemStore in unsorted order -func (s MemStore) List() ([]keys.KeyInfo, error) { - res := make([]keys.KeyInfo, len(s)) +func (s MemStore) List() (keys.Infos, error) { + res := make([]keys.Info, len(s)) i := 0 for _, d := range s { res[i] = d.info diff --git a/storage/memstorage/main_test.go b/storage/memstorage/main_test.go index 01c015a1..2863bffa 100644 --- a/storage/memstorage/main_test.go +++ b/storage/memstorage/main_test.go @@ -15,7 +15,7 @@ func TestBasicCRUD(t *testing.T) { name := "foo" key := []byte("secret-key-here") pubkey := crypto.GenPrivKeyEd25519().PubKey() - info := keys.KeyInfo{ + info := keys.Info{ Name: name, PubKey: crypto.PubKeyS{pubkey}, } diff --git a/transactions.go b/transactions.go index f22719f7..13f38b41 100644 --- a/transactions.go +++ b/transactions.go @@ -6,19 +6,19 @@ import ( crypto "github.com/tendermint/go-crypto" ) -// KeyInfo is the public information about a key -type KeyInfo struct { +// Info is the public information about a key +type Info struct { Name string PubKey crypto.PubKeyS } -// KeyInfos is a wrapper to allows alphabetical sorting of the keys -type KeyInfos []KeyInfo +// Infos is a wrapper to allows alphabetical sorting of the keys +type Infos []Info -func (k KeyInfos) Len() int { return len(k) } -func (k KeyInfos) Less(i, j int) bool { return k[i].Name < k[j].Name } -func (k KeyInfos) Swap(i, j int) { k[i], k[j] = k[j], k[i] } -func (k KeyInfos) Sort() { +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) } @@ -51,11 +51,11 @@ type Signer interface { Sign(name, passphrase string, tx Signable) error } -// KeyManager allows simple CRUD on a keystore, as an aid to signing -type KeyManager interface { - Create(name, passphrase string) error - List() (KeyInfos, error) - Get(name string) (KeyInfo, error) +// Manager allows simple CRUD on a keystore, as an aid to signing +type Manager interface { + Create(name, passphrase string) (Info, error) + List() (Infos, error) + Get(name string) (Info, error) Update(name, oldpass, newpass string) error Delete(name, passphrase string) error }