diff --git a/client/commands/keys/new.go b/client/commands/keys/new.go index f1f422a20aaf..46f967a5692a 100644 --- a/client/commands/keys/new.go +++ b/client/commands/keys/new.go @@ -36,7 +36,7 @@ var newCmd = &cobra.Command{ Use: "new [name]", 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 +The password must be entered in the terminal and not passed as a command line argument for security.`, RunE: runNewCmd, } diff --git a/client/rest/handlers.go b/client/rest/handlers.go deleted file mode 100644 index 330e82e2c93a..000000000000 --- a/client/rest/handlers.go +++ /dev/null @@ -1,192 +0,0 @@ -package rest - -import ( - "net/http" - - "github.com/gorilla/mux" - "github.com/pkg/errors" - - keys "github.com/tendermint/go-crypto/keys" - "github.com/tendermint/tmlibs/common" - - sdk "github.com/cosmos/cosmos-sdk" - keycmd "github.com/cosmos/cosmos-sdk/client/commands/keys" -) - -type Keys struct { - algo string - manager keys.Manager -} - -func DefaultKeysManager() keys.Manager { - return keycmd.GetKeyManager() -} - -func NewDefaultKeysManager(algo string) *Keys { - return New(DefaultKeysManager(), algo) -} - -func New(manager keys.Manager, algo string) *Keys { - return &Keys{ - algo: algo, - manager: manager, - } -} - -func (k *Keys) GenerateKey(w http.ResponseWriter, r *http.Request) { - ckReq := &CreateKeyRequest{ - Algo: k.algo, - } - if err := common.ParseRequestAndValidateJSON(r, ckReq); err != nil { - common.WriteError(w, err) - return - } - - key, seed, err := k.manager.Create(ckReq.Name, ckReq.Passphrase, ckReq.Algo) - if err != nil { - common.WriteError(w, err) - return - } - - res := &CreateKeyResponse{Key: key, Seed: seed} - common.WriteSuccess(w, res) -} - -func (k *Keys) GetKey(w http.ResponseWriter, r *http.Request) { - query := mux.Vars(r) - name := query["name"] - key, err := k.manager.Get(name) - if err != nil { - common.WriteError(w, err) - return - } - common.WriteSuccess(w, &key) -} - -func (k *Keys) ListKeys(w http.ResponseWriter, r *http.Request) { - keys, err := k.manager.List() - if err != nil { - common.WriteError(w, err) - return - } - common.WriteSuccess(w, keys) -} - -var ( - errNonMatchingPathAndJSONKeyNames = errors.New("path and json key names don't match") -) - -func (k *Keys) UpdateKey(w http.ResponseWriter, r *http.Request) { - uReq := new(UpdateKeyRequest) - if err := common.ParseRequestAndValidateJSON(r, uReq); err != nil { - common.WriteError(w, err) - return - } - - query := mux.Vars(r) - name := query["name"] - if name != uReq.Name { - common.WriteError(w, errNonMatchingPathAndJSONKeyNames) - return - } - - if err := k.manager.Update(uReq.Name, uReq.OldPass, uReq.NewPass); err != nil { - common.WriteError(w, err) - return - } - - key, err := k.manager.Get(uReq.Name) - if err != nil { - common.WriteError(w, err) - return - } - common.WriteSuccess(w, &key) -} - -func (k *Keys) DeleteKey(w http.ResponseWriter, r *http.Request) { - dReq := new(DeleteKeyRequest) - if err := common.ParseRequestAndValidateJSON(r, dReq); err != nil { - common.WriteError(w, err) - return - } - - query := mux.Vars(r) - name := query["name"] - if name != dReq.Name { - common.WriteError(w, errNonMatchingPathAndJSONKeyNames) - return - } - - if err := k.manager.Delete(dReq.Name, dReq.Passphrase); err != nil { - common.WriteError(w, err) - return - } - - resp := &common.ErrorResponse{Success: true} - common.WriteSuccess(w, resp) -} - -func doPostTx(w http.ResponseWriter, r *http.Request) { - tx := new(sdk.Tx) - if err := common.ParseRequestAndValidateJSON(r, tx); err != nil { - common.WriteError(w, err) - return - } - commit, err := PostTx(*tx) - if err != nil { - common.WriteError(w, err) - return - } - - common.WriteSuccess(w, commit) -} - -func doSign(w http.ResponseWriter, r *http.Request) { - sr := new(SignRequest) - if err := common.ParseRequestAndValidateJSON(r, sr); err != nil { - common.WriteError(w, err) - return - } - - tx := sr.Tx - if err := SignTx(sr.Name, sr.Password, tx); err != nil { - common.WriteError(w, err) - return - } - common.WriteSuccess(w, tx) -} - -// mux.Router registrars - -// RegisterPostTx is a mux.Router handler that exposes POST -// method access to post a transaction to the blockchain. -func RegisterPostTx(r *mux.Router) error { - r.HandleFunc("/tx", doPostTx).Methods("POST") - return nil -} - -// RegisterAllCRUD is a convenience method to register all -// CRUD for keys to allow access by methods and routes: -// POST: /keys -// GET: /keys -// GET: /keys/{name} -// POST, PUT: /keys/{name} -// DELETE: /keys/{name} -func (k *Keys) RegisterAllCRUD(r *mux.Router) error { - r.HandleFunc("/keys", k.GenerateKey).Methods("POST") - r.HandleFunc("/keys", k.ListKeys).Methods("GET") - r.HandleFunc("/keys/{name}", k.GetKey).Methods("GET") - r.HandleFunc("/keys/{name}", k.UpdateKey).Methods("POST", "PUT") - r.HandleFunc("/keys/{name}", k.DeleteKey).Methods("DELETE") - - return nil -} - -// RegisterSignTx is a mux.Router handler that -// exposes POST method access to sign a transaction. -func RegisterSignTx(r *mux.Router) error { - r.HandleFunc("/sign", doSign).Methods("POST") - return nil -} - -// End of mux.Router registrars diff --git a/client/rest/helpers.go b/client/rest/helpers.go deleted file mode 100644 index 5085135e848a..000000000000 --- a/client/rest/helpers.go +++ /dev/null @@ -1,29 +0,0 @@ -package rest - -import ( - "github.com/tendermint/go-crypto/keys" - wire "github.com/tendermint/go-wire" - - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/client/commands" - keycmd "github.com/cosmos/cosmos-sdk/client/commands/keys" -) - -// PostTx is same as a tx -func PostTx(tx sdk.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - packet := wire.BinaryBytes(tx) - // post the bytes - node := commands.GetNode() - return node.BroadcastTxCommit(packet) -} - -// SignTx will modify the tx in-place, adding a signature if possible -func SignTx(name, pass string, tx sdk.Tx) error { - if sign, ok := tx.Unwrap().(keys.Signable); ok { - manager := keycmd.GetKeyManager() - return manager.Sign(name, pass, sign) - } - return nil -} diff --git a/client/rest/keys.go b/client/rest/keys.go new file mode 100644 index 000000000000..4457b44c430c --- /dev/null +++ b/client/rest/keys.go @@ -0,0 +1,190 @@ +package rest + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + + keys "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/tmlibs/common" +) + +const ( + defaultAlgo = "ed25519" // TODO: allow this to come in via requests +) + +var ( + errNonMatchingPathAndJSONKeyNames = errors.New("path and json key names don't match") +) + +// ServiceKeys exposes a REST API service for +// managing keys and signing transactions +type ServiceKeys struct { + manager keys.Manager +} + +// New returns a new instance of the keys service +func NewServiceKeys(manager keys.Manager) *ServiceKeys { + return &ServiceKeys{ + manager: manager, // XXX keycmd.GetKeyManager() + } +} + +func (s *ServiceKeys) Create(w http.ResponseWriter, r *http.Request) { + req := &RequestCreate{ + Algo: defaultAlgo, + } + if err := common.ParseRequestAndValidateJSON(r, req); err != nil { + common.WriteError(w, err) + return + } + + key, seed, err := s.manager.Create(req.Name, req.Passphrase, req.Algo) + if err != nil { + common.WriteError(w, err) + return + } + + res := &ResponseCreate{Key: key, Seed: seed} + common.WriteSuccess(w, res) +} + +func (s *ServiceKeys) Get(w http.ResponseWriter, r *http.Request) { + query := mux.Vars(r) + name := query["name"] + key, err := s.manager.Get(name) + if err != nil { + common.WriteError(w, err) + return + } + common.WriteSuccess(w, &key) +} + +func (s *ServiceKeys) List(w http.ResponseWriter, r *http.Request) { + keys, err := s.manager.List() + if err != nil { + common.WriteError(w, err) + return + } + common.WriteSuccess(w, keys) +} + +func (s *ServiceKeys) Update(w http.ResponseWriter, r *http.Request) { + req := new(RequestUpdate) + if err := common.ParseRequestAndValidateJSON(r, req); err != nil { + common.WriteError(w, err) + return + } + + query := mux.Vars(r) + name := query["name"] + if name != req.Name { + common.WriteError(w, errNonMatchingPathAndJSONKeyNames) + return + } + + if err := s.manager.Update(req.Name, req.OldPass, req.NewPass); err != nil { + common.WriteError(w, err) + return + } + + key, err := s.manager.Get(req.Name) + if err != nil { + common.WriteError(w, err) + return + } + common.WriteSuccess(w, &key) +} + +func (s *ServiceKeys) Delete(w http.ResponseWriter, r *http.Request) { + req := new(RequestDelete) + if err := common.ParseRequestAndValidateJSON(r, req); err != nil { + common.WriteError(w, err) + return + } + + query := mux.Vars(r) + name := query["name"] + if name != req.Name { + common.WriteError(w, errNonMatchingPathAndJSONKeyNames) + return + } + + if err := s.manager.Delete(req.Name, req.Passphrase); err != nil { + common.WriteError(w, err) + return + } + + resp := &common.ErrorResponse{Success: true} + common.WriteSuccess(w, resp) +} + +func (s *ServiceKeys) Recover(w http.ResponseWriter, r *http.Request) { + req := &RequestRecover{ + Algo: defaultAlgo, + } + if err := common.ParseRequestAndValidateJSON(r, req); err != nil { + common.WriteError(w, err) + return + } + + key, err := s.manager.Recover(req.Name, req.Passphrase, req.Seed) + if err != nil { + common.WriteError(w, err) + return + } + + res := &ResponseRecover{Key: key} + common.WriteSuccess(w, res) +} + +func (s *ServiceKeys) SignTx(w http.ResponseWriter, r *http.Request) { + req := new(RequestSign) + if err := common.ParseRequestAndValidateJSON(r, req); err != nil { + common.WriteError(w, err) + return + } + + tx := req.Tx + + var err error + if sign, ok := tx.Unwrap().(keys.Signable); ok { + err = s.manager.Sign(req.Name, req.Password, sign) + } + if err != nil { + common.WriteError(w, err) + return + } + common.WriteSuccess(w, tx) +} + +// mux.Router registrars + +// RegisterCRUD is a convenience method to register all +// CRUD for keys to allow access by methods and routes: +// POST: /keys +// POST: /keys/recover +// GET: /keys +// GET: /keys/{name} +// POST, PUT: /keys/{name} +// DELETE: /keys/{name} +func (s *ServiceKeys) RegisterCRUD(r *mux.Router) error { + r.HandleFunc("/keys", s.Create).Methods("POST") + r.HandleFunc("/keys/recover", s.Recover).Methods("POST") + r.HandleFunc("/keys", s.List).Methods("GET") + r.HandleFunc("/keys/{name}", s.Get).Methods("GET") + r.HandleFunc("/keys/{name}", s.Update).Methods("POST", "PUT") + r.HandleFunc("/keys/{name}", s.Delete).Methods("DELETE") + + return nil +} + +// RegisterSignTx is a mux.Router handler that +// exposes POST method access to sign a transaction. +func (s *ServiceKeys) RegisterSignTx(r *mux.Router) error { + r.HandleFunc("/sign", s.SignTx).Methods("POST") + return nil +} + +// End of mux.Router registrars diff --git a/client/rest/keys_test.go b/client/rest/keys_test.go new file mode 100644 index 000000000000..eeb5f99f9410 --- /dev/null +++ b/client/rest/keys_test.go @@ -0,0 +1,114 @@ +package rest + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-crypto/keys/cryptostore" + "github.com/tendermint/go-crypto/keys/storage/memstorage" +) + +func getKeyManager() keys.Manager { + return cryptostore.New( + cryptostore.SecretBox, + memstorage.New(), + keys.MustLoadCodec("english"), + ) +} + +func equalKeys(t *testing.T, k1, k2 keys.Info, name bool) { + assert.Equal(t, k1.Address, k2.Address) + assert.Equal(t, k1.PubKey, k2.PubKey) +} + +func TestKeysCreateGetRecover(t *testing.T) { + keyMan := getKeyManager() + serviceKeys := NewServiceKeys(keyMan) + + r := mux.NewRouter() + err := serviceKeys.RegisterCRUD(r) + assert.Nil(t, err) + + ts := httptest.NewServer(r) + defer ts.Close() + + var ( + keyInfo keys.Info + + passPhrase string = "abcdefghijklmno" + seedPhrase string + ) + keyInfo.Name = "mykey" + + // create the key + { + reqCreate := RequestCreate{ + Name: keyInfo.Name, + Passphrase: passPhrase, + Algo: defaultAlgo, + } + b, err := json.Marshal(reqCreate) + assert.Nil(t, err) + + resp, err := http.Post(ts.URL+"/keys", "json", bytes.NewBuffer(b)) + assert.Nil(t, err) + assert.Equal(t, resp.StatusCode, 200) + + var resCreate ResponseCreate + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + err = json.Unmarshal(body, &resCreate) + assert.Nil(t, err) + + assert.Equal(t, keyInfo.Name, resCreate.Key.Name) + keyInfo = resCreate.Key + seedPhrase = resCreate.Seed + } + + // get the key and confirm it matches + { + resp, err := http.Get(ts.URL + "/keys/" + keyInfo.Name) + assert.Nil(t, err) + assert.Equal(t, resp.StatusCode, 200) + + var resKeyInfo keys.Info + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + err = json.Unmarshal(body, &resKeyInfo) + assert.Nil(t, err) + + equalKeys(t, keyInfo, resKeyInfo, true) + } + + // recover the key and confirm it matches + { + + reqRecover := RequestRecover{ + Name: "newName", + Passphrase: passPhrase, + Seed: seedPhrase, + Algo: defaultAlgo, + } + b, err := json.Marshal(reqRecover) + assert.Nil(t, err) + + resp, err := http.Post(ts.URL+"/keys/recover", "json", bytes.NewBuffer(b)) + assert.Nil(t, err) + assert.Equal(t, resp.StatusCode, 200) + + var resRecover ResponseRecover + body, err := ioutil.ReadAll(resp.Body) + assert.Nil(t, err) + err = json.Unmarshal(body, &resRecover) + assert.Nil(t, err) + + equalKeys(t, keyInfo, resRecover.Key, false) + } +} diff --git a/client/rest/txs.go b/client/rest/txs.go new file mode 100644 index 000000000000..65da4da4b29b --- /dev/null +++ b/client/rest/txs.go @@ -0,0 +1,54 @@ +package rest + +import ( + "net/http" + + "github.com/gorilla/mux" + + wire "github.com/tendermint/go-wire" + "github.com/tendermint/tmlibs/common" + + sdk "github.com/cosmos/cosmos-sdk" + + rpcclient "github.com/tendermint/tendermint/rpc/client" +) + +// ServiceTxs exposes a REST API service for sendings txs. +// It wraps a Tendermint RPC client. +type ServiceTxs struct { + node rpcclient.Client +} + +func NewServiceTxs(c rpcclient.Client) *ServiceTxs { + return &ServiceTxs{ + node: c, + } +} + +func (s *ServiceTxs) PostTx(w http.ResponseWriter, r *http.Request) { + tx := new(sdk.Tx) + if err := common.ParseRequestAndValidateJSON(r, tx); err != nil { + common.WriteError(w, err) + return + } + + packet := wire.BinaryBytes(*tx) + commit, err := s.node.BroadcastTxCommit(packet) + if err != nil { + common.WriteError(w, err) + return + } + + common.WriteSuccess(w, commit) +} + +// mux.Router registrars + +// RegisterPostTx is a mux.Router handler that exposes POST +// method access to post a transaction to the blockchain. +func (s *ServiceTxs) RegisterPostTx(r *mux.Router) error { + r.HandleFunc("/tx", s.PostTx).Methods("POST") + return nil +} + +// End of mux.Router registrars diff --git a/client/rest/types.go b/client/rest/types.go index 3ae0252ca6d5..a43097bbf8cc 100644 --- a/client/rest/types.go +++ b/client/rest/types.go @@ -1,52 +1,63 @@ package rest import ( - sdk "github.com/cosmos/cosmos-sdk" - "github.com/cosmos/cosmos-sdk/modules/coin" "github.com/tendermint/go-crypto/keys" + + sdk "github.com/cosmos/cosmos-sdk" ) -type CreateKeyRequest struct { +// TODO: consistency between Passphrase and Password !!! + +type RequestCreate struct { + Name string `json:"name,omitempty" validate:"required,min=3,printascii"` + Passphrase string `json:"password,omitempty" validate:"required,min=10"` + + // Algo is the requested algorithm to create the key + Algo string `json:"algo,omitempty"` +} + +type ResponseCreate struct { + Key keys.Info `json:"key,omitempty"` + Seed string `json:"seed_phrase,omitempty"` +} + +//----------------------------------------------------------------------- + +type RequestRecover struct { Name string `json:"name,omitempty" validate:"required,min=3,printascii"` Passphrase string `json:"password,omitempty" validate:"required,min=10"` + Seed string `json:"seed_phrase,omitempty" validate:"required,min=23"` // Algo is the requested algorithm to create the key Algo string `json:"algo,omitempty"` } -type DeleteKeyRequest struct { +type ResponseRecover struct { + Key keys.Info `json:"key,omitempty"` +} + +//----------------------------------------------------------------------- + +type RequestDelete struct { Name string `json:"name,omitempty" validate:"required,min=3,printascii"` Passphrase string `json:"password,omitempty" validate:"required,min=10"` } -type UpdateKeyRequest struct { +//----------------------------------------------------------------------- + +type RequestUpdate struct { Name string `json:"name,omitempty" validate:"required,min=3,printascii"` OldPass string `json:"password,omitempty" validate:"required,min=10"` NewPass string `json:"new_passphrase,omitempty" validate:"required,min=10"` } -type SignRequest struct { +//----------------------------------------------------------------------- + +type RequestSign struct { Name string `json:"name,omitempty" validate:"required,min=3,printascii"` Password string `json:"password,omitempty" validate:"required,min=10"` Tx sdk.Tx `json:"tx" validate:"required"` } -type CreateKeyResponse struct { - Key keys.Info `json:"key,omitempty"` - Seed string `json:"seed_phrase,omitempty"` -} - -// SendInput is the request to send an amount from one actor to another. -// Note: Not using the `validator:""` tags here because SendInput has -// many fields so it would be nice to figure out all the invalid -// inputs and report them back to the caller, in one shot. -type SendInput struct { - Fees *coin.Coin `json:"fees"` - Multi bool `json:"multi,omitempty"` - Sequence uint32 `json:"sequence"` - - To *sdk.Actor `json:"to"` - From *sdk.Actor `json:"from"` - Amount coin.Coins `json:"amount"` -} +//----------------------------------------------------------------------- diff --git a/examples/basecoin/cmd/baseserver/main.go b/examples/basecoin/cmd/baseserver/main.go index c4b4c2a8692a..70dc7f992873 100644 --- a/examples/basecoin/cmd/baseserver/main.go +++ b/examples/basecoin/cmd/baseserver/main.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/commands" rest "github.com/cosmos/cosmos-sdk/client/rest" coinrest "github.com/cosmos/cosmos-sdk/modules/coin/rest" @@ -43,9 +44,14 @@ func init() { func serve(cmd *cobra.Command, args []string) error { router := mux.NewRouter() + rootDir := viper.GetString(cli.HomeFlag) + keyMan := client.GetKeyManager(rootDir) + serviceKeys := rest.NewServiceKeys(keyMan) + serviceTxs := rest.NewServiceTxs(commands.GetNode()) + routeRegistrars := []func(*mux.Router) error{ // rest.Keys handlers - rest.NewDefaultKeysManager(defaultAlgo).RegisterAllCRUD, + serviceKeys.RegisterCRUD, // Coin send handler coinrest.RegisterAll, @@ -54,9 +60,9 @@ func serve(cmd *cobra.Command, args []string) error { rolerest.RegisterCreateRole, // Basecoin sign transactions handler - rest.RegisterSignTx, + serviceKeys.RegisterSignTx, // Basecoin post transaction handler - rest.RegisterPostTx, + serviceTxs.RegisterPostTx, // Nonce query handler noncerest.RegisterQueryNonce,