Expose new and list via cli

This commit is contained in:
Ethan Frey
2017-02-28 18:52:52 +01:00
parent 78bb9f9cd8
commit 506ff7d85a
16 changed files with 177 additions and 84 deletions

View File

@ -1,2 +1,5 @@
test: test:
go test ./... go test ./...
install:
go install ./cmd/keys

View File

@ -18,7 +18,6 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
) )
// listCmd represents the list command // 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 Long: `Return a list of all public keys stored by this key manager
along with their associated name and address.`, along with their associated name and address.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// TODO: Work your own magic here infos, err := manager.List()
fmt.Println("list called") if err != nil {
fmt.Println(viper.Get("format")) fmt.Println(err.Error())
return
}
printInfos(infos)
}, },
} }
func init() { func init() {
RootCmd.AddCommand(listCmd) 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)")
} }

View File

@ -22,28 +22,46 @@ import (
// newCmd represents the new command // newCmd represents the new command
var newCmd = &cobra.Command{ var newCmd = &cobra.Command{
Use: "new", Use: "new <name>",
Short: "Create a new public/private key pair", Short: "Create a new public/private key pair",
Long: `Add a public/private key pair to the key store. Long: `Add a public/private key pair to the key store.
The password muts be entered in the terminal and not The password muts be entered in the terminal and not
passed as a command line argument for security.`, passed as a command line argument for security.`,
Run: func(cmd *cobra.Command, args []string) { Run: newPassword,
// TODO: Work your own magic here
fmt.Println("new called")
},
} }
func init() { func init() {
RootCmd.AddCommand(newCmd) RootCmd.AddCommand(newCmd)
}
// Here you will define your flags and configuration settings.
func newPassword(cmd *cobra.Command, args []string) {
// Cobra supports Persistent Flags which will work for this command if len(args) != 1 || len(args[0]) == 0 {
// and all subcommands, e.g.: fmt.Print("You must provide a name for the key")
// newCmd.PersistentFlags().String("foo", "", "A help for foo") return
}
// Cobra supports local flags which will only run when this command name := args[0]
// is called directly, e.g.:
// newCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") // 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)
} }

View File

@ -17,16 +17,22 @@ package cmd
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
keys "github.com/tendermint/go-keys"
"github.com/tendermint/go-keys/cryptostore"
"github.com/tendermint/go-keys/storage/filestorage"
) )
var ( var (
rootDir string rootDir string
format string output string
keyDir string
manager keys.Manager
) )
// RootCmd represents the base command when called without any subcommands // RootCmd represents the base command when called without any subcommands
@ -53,7 +59,8 @@ func Execute() {
func init() { func init() {
cobra.OnInitialize(initEnv) 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("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. // 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 // validateFlags asserts all RootCmd flags are valid
func validateFlags(cmd *cobra.Command) error { func validateFlags(cmd *cobra.Command) error {
format = viper.GetString("format") // validate output format
switch format { output = viper.GetString("output")
switch output {
case "text", "json": case "text", "json":
return nil
default: 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
} }

56
cmd/utils.go Normal file
View File

@ -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))
}
}

View File

@ -21,7 +21,7 @@ func (es encryptedStorage) Put(name, pass string, key crypto.PrivKey) error {
return es.store.Put(name, secret, ki) 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) secret, info, err := es.store.Get(name)
if err != nil { if err != nil {
return nil, info, err return nil, info, err
@ -30,7 +30,7 @@ func (es encryptedStorage) Get(name, pass string) (crypto.PrivKey, keys.KeyInfo,
return key, info, err return key, info, err
} }
func (es encryptedStorage) List() ([]keys.KeyInfo, error) { func (es encryptedStorage) List() (keys.Infos, error) {
return es.store.List() return es.store.List()
} }
@ -39,8 +39,8 @@ func (es encryptedStorage) Delete(name string) error {
} }
// info hardcodes the encoding of keys // info hardcodes the encoding of keys
func info(name string, key crypto.PrivKey) keys.KeyInfo { func info(name string, key crypto.PrivKey) keys.Info {
return keys.KeyInfo{ return keys.Info{
Name: name, Name: name,
PubKey: crypto.PubKeyS{key.PubKey()}, PubKey: crypto.PubKeyS{key.PubKey()},
} }

View File

@ -24,28 +24,28 @@ func (s Manager) assertSigner() keys.Signer {
return s return s
} }
// exists just to make sure we fulfill the KeyManager interface // exists just to make sure we fulfill the Manager interface
func (s Manager) assertKeyManager() keys.KeyManager { func (s Manager) assertKeyManager() keys.Manager {
return s return s
} }
// Create adds a new key to the storage engine, returning error if // Create adds a new key to the storage engine, returning error if
// another key already stored under this name // 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() 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 // List loads the keys from the storage and enforces alphabetical order
func (s Manager) List() (keys.KeyInfos, error) { func (s Manager) List() (keys.Infos, error) {
k, err := s.es.List() res, err := s.es.List()
res := keys.KeyInfos(k)
res.Sort() res.Sort()
return res, err return res, err
} }
// Get returns the public information about one key // 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) _, info, err := s.es.store.Get(name)
return info, err return info, err
} }

View File

@ -31,9 +31,10 @@ func TestKeyManagement(t *testing.T) {
// create some keys // create some keys
_, err = cstore.Get(n1) _, err = cstore.Get(n1)
assert.NotNil(err) assert.NotNil(err)
err = cstore.Create(n1, p1) i, err := cstore.Create(n1, p1)
require.Equal(n1, i.Name)
require.Nil(err) require.Nil(err)
err = cstore.Create(n2, p2) _, err = cstore.Create(n2, p2)
require.Nil(err) require.Nil(err)
// we can get these keys // we can get these keys
@ -159,7 +160,7 @@ func TestAdvancedKeyManagement(t *testing.T) {
p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$" p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$"
// make sure key works with initial password // make sure key works with initial password
err := cstore.Create(n1, p1) _, err := cstore.Create(n1, p1)
require.Nil(err, "%+v", err) require.Nil(err, "%+v", err)
assertPassword(assert, cstore, n1, p1, p2) assertPassword(assert, cstore, n1, p1, p2)

View File

@ -15,7 +15,7 @@ func TestSortKeys(t *testing.T) {
// alphabetical order is n3, n1, n2 // alphabetical order is n3, n1, n2
n1, n2, n3 := "john", "mike", "alice" n1, n2, n3 := "john", "mike", "alice"
infos := keys.KeyInfos{ infos := keys.Infos{
info(n1, gen()), info(n1, gen()),
info(n2, gen()), info(n2, gen()),
info(n3, gen()), info(n3, gen()),

View File

@ -1 +1,2 @@
format = "text" output = "text"
keydir = ".mykeys"

View File

@ -3,8 +3,8 @@ package keys
// Storage has many implementation, based on security and sharing requirements // Storage has many implementation, based on security and sharing requirements
// like disk-backed, mem-backed, vault, db, etc. // like disk-backed, mem-backed, vault, db, etc.
type Storage interface { type Storage interface {
Put(name string, key []byte, info KeyInfo) error Put(name string, key []byte, info Info) error
Get(name string) ([]byte, KeyInfo, error) Get(name string) ([]byte, Info, error)
List() ([]KeyInfo, error) List() (Infos, error)
Delete(name string) error Delete(name string) error
} }

View File

@ -49,7 +49,7 @@ func (s FileStore) assertStorage() keys.Storage {
// Put creates two files, one with the public info as json, the other // Put creates two files, one with the public info as json, the other
// with the (encoded) private key as gpg ascii-armor style // 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) pub, priv := s.nameToPaths(name)
// write public info // write public info
@ -62,10 +62,10 @@ func (s FileStore) Put(name string, key []byte, info keys.KeyInfo) error {
return write(priv, name, key) 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 // It uses `name` to generate the filename, and returns an error if the
// files don't exist or are in the incorrect format // 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) pub, priv := s.nameToPaths(name)
info, err := readInfo(pub) 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 // List parses the key directory for public info and returns a list of
// KeyInfo for all keys located in this directory. // Info for all keys located in this directory.
func (s FileStore) List() ([]keys.KeyInfo, error) { func (s FileStore) List() (keys.Infos, error) {
dir, err := os.Open(s.keyDir) dir, err := os.Open(s.keyDir)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "List Keys") 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 // filter names for .pub ending and load them one by one
// half the files is a good guess for pre-allocating the slice // 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 { for _, name := range names {
if strings.HasSuffix(name, PubExt) { if strings.HasSuffix(name, PubExt) {
p := path.Join(s.keyDir, name) 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) 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()) 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 var data []byte
data, info.Name, err = read(path) data, info.Name, err = read(path)
if err != nil { if err != nil {

View File

@ -23,7 +23,7 @@ func TestBasicCRUD(t *testing.T) {
name := "bar" name := "bar"
key := []byte("secret-key-here") key := []byte("secret-key-here")
pubkey := crypto.GenPrivKeyEd25519().PubKey() pubkey := crypto.GenPrivKeyEd25519().PubKey()
info := keys.KeyInfo{ info := keys.Info{
Name: name, Name: name,
PubKey: crypto.PubKeyS{pubkey}, PubKey: crypto.PubKeyS{pubkey},
} }

View File

@ -11,7 +11,7 @@ import (
) )
type data struct { type data struct {
info keys.KeyInfo info keys.Info
key []byte key []byte
} }
@ -29,7 +29,7 @@ func (s MemStore) assertStorage() keys.Storage {
// Put adds the given key, returns an error if it another key // Put adds the given key, returns an error if it another key
// is already stored under this name // 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 { if _, ok := s[name]; ok {
return errors.Errorf("Key named '%s' already exists", name) 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 // 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 var err error
d, ok := s[name] d, ok := s[name]
if !ok { 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 // List returns the public info of all keys in the MemStore in unsorted order
func (s MemStore) List() ([]keys.KeyInfo, error) { func (s MemStore) List() (keys.Infos, error) {
res := make([]keys.KeyInfo, len(s)) res := make([]keys.Info, len(s))
i := 0 i := 0
for _, d := range s { for _, d := range s {
res[i] = d.info res[i] = d.info

View File

@ -15,7 +15,7 @@ func TestBasicCRUD(t *testing.T) {
name := "foo" name := "foo"
key := []byte("secret-key-here") key := []byte("secret-key-here")
pubkey := crypto.GenPrivKeyEd25519().PubKey() pubkey := crypto.GenPrivKeyEd25519().PubKey()
info := keys.KeyInfo{ info := keys.Info{
Name: name, Name: name,
PubKey: crypto.PubKeyS{pubkey}, PubKey: crypto.PubKeyS{pubkey},
} }

View File

@ -6,19 +6,19 @@ import (
crypto "github.com/tendermint/go-crypto" crypto "github.com/tendermint/go-crypto"
) )
// KeyInfo is the public information about a key // Info is the public information about a key
type KeyInfo struct { type Info struct {
Name string Name string
PubKey crypto.PubKeyS PubKey crypto.PubKeyS
} }
// KeyInfos is a wrapper to allows alphabetical sorting of the keys // Infos is a wrapper to allows alphabetical sorting of the keys
type KeyInfos []KeyInfo type Infos []Info
func (k KeyInfos) Len() int { return len(k) } func (k Infos) Len() int { return len(k) }
func (k KeyInfos) Less(i, j int) bool { return k[i].Name < k[j].Name } func (k Infos) 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 Infos) Swap(i, j int) { k[i], k[j] = k[j], k[i] }
func (k KeyInfos) Sort() { func (k Infos) Sort() {
if k != nil { if k != nil {
sort.Sort(k) sort.Sort(k)
} }
@ -51,11 +51,11 @@ type Signer interface {
Sign(name, passphrase string, tx Signable) error Sign(name, passphrase string, tx Signable) error
} }
// KeyManager allows simple CRUD on a keystore, as an aid to signing // Manager allows simple CRUD on a keystore, as an aid to signing
type KeyManager interface { type Manager interface {
Create(name, passphrase string) error Create(name, passphrase string) (Info, error)
List() (KeyInfos, error) List() (Infos, error)
Get(name string) (KeyInfo, error) Get(name string) (Info, error)
Update(name, oldpass, newpass string) error Update(name, oldpass, newpass string) error
Delete(name, passphrase string) error Delete(name, passphrase string) error
} }