mirror of
https://github.com/fluencelabs/tendermint
synced 2025-07-04 15:11:38 +00:00
Merge pull request #19 from tendermint/feature/cli-improvements
cli improvements
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
vendor
|
vendor
|
||||||
|
shunit2
|
||||||
|
25
Makefile
25
Makefile
@ -1,20 +1,32 @@
|
|||||||
.PHONEY: all docs test install get_vendor_deps ensure_tools codegen
|
.PHONEY: all docs test install get_vendor_deps ensure_tools codegen wordlist
|
||||||
|
|
||||||
GOTOOLS = \
|
GOTOOLS = \
|
||||||
github.com/Masterminds/glide
|
github.com/Masterminds/glide \
|
||||||
|
github.com/jteeuwen/go-bindata/go-bindata
|
||||||
REPO:=github.com/tendermint/go-crypto
|
REPO:=github.com/tendermint/go-crypto
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
@go get github.com/davecheney/godoc2md
|
@go get github.com/davecheney/godoc2md
|
||||||
godoc2md $(REPO) > README.md
|
godoc2md $(REPO) > README.md
|
||||||
|
|
||||||
all: install test
|
all: get_vendor_deps install test
|
||||||
|
|
||||||
install:
|
install:
|
||||||
go install ./cmd/keys
|
go install ./cmd/keys
|
||||||
|
|
||||||
test:
|
test: test_unit test_cli
|
||||||
|
|
||||||
|
test_unit:
|
||||||
go test `glide novendor`
|
go test `glide novendor`
|
||||||
|
#go run tests/tendermint/*.go
|
||||||
|
|
||||||
|
test_cli: tests/shunit2
|
||||||
|
# sudo apt-get install jq
|
||||||
|
@./tests/keys.sh
|
||||||
|
|
||||||
|
tests/shunit2:
|
||||||
|
wget "https://raw.githubusercontent.com/kward/shunit2/master/source/2.1/src/shunit2" \
|
||||||
|
-q -O tests/shunit2
|
||||||
|
|
||||||
get_vendor_deps: ensure_tools
|
get_vendor_deps: ensure_tools
|
||||||
@rm -rf vendor/
|
@rm -rf vendor/
|
||||||
@ -24,13 +36,16 @@ get_vendor_deps: ensure_tools
|
|||||||
ensure_tools:
|
ensure_tools:
|
||||||
go get $(GOTOOLS)
|
go get $(GOTOOLS)
|
||||||
|
|
||||||
|
wordlist:
|
||||||
|
go-bindata -ignore ".*\.go" -o keys/wordlist/wordlist.go -pkg "wordlist" keys/wordlist/...
|
||||||
|
|
||||||
prepgen: install
|
prepgen: install
|
||||||
go install ./vendor/github.com/btcsuite/btcutil/base58
|
go install ./vendor/github.com/btcsuite/btcutil/base58
|
||||||
go install ./vendor/github.com/stretchr/testify/assert
|
go install ./vendor/github.com/stretchr/testify/assert
|
||||||
go install ./vendor/github.com/stretchr/testify/require
|
go install ./vendor/github.com/stretchr/testify/require
|
||||||
go install ./vendor/golang.org/x/crypto/bcrypt
|
go install ./vendor/golang.org/x/crypto/bcrypt
|
||||||
|
|
||||||
codegen:
|
codegen:
|
||||||
@echo "--> regenerating all interface wrappers"
|
@echo "--> regenerating all interface wrappers"
|
||||||
@gen
|
@gen
|
||||||
@echo "Done!"
|
@echo "Done!"
|
||||||
|
@ -18,4 +18,4 @@ dependencies:
|
|||||||
test:
|
test:
|
||||||
override:
|
override:
|
||||||
- "go version"
|
- "go version"
|
||||||
- "cd $PROJECT_PATH && make get_vendor_deps && make test"
|
- "cd $PROJECT_PATH && make all"
|
||||||
|
49
cmd/delete.go
Normal file
49
cmd/delete.go
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright © 2017 Ethan Frey
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// deleteCmd represents the delete command
|
||||||
|
var deleteCmd = &cobra.Command{
|
||||||
|
Use: "delete [name]",
|
||||||
|
Short: "DANGER: Delete a private key from your system",
|
||||||
|
RunE: runDeleteCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDeleteCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) != 1 || len(args[0]) == 0 {
|
||||||
|
return errors.New("You must provide a name for the key")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
oldpass, err := getPassword("DANGER - enter password to permanently delete key:")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = GetKeyManager().Delete(name, oldpass)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Password deleted forever (uh oh!)")
|
||||||
|
return nil
|
||||||
|
}
|
28
cmd/get.go
28
cmd/get.go
@ -22,23 +22,21 @@ import (
|
|||||||
|
|
||||||
// getCmd represents the get command
|
// getCmd represents the get command
|
||||||
var getCmd = &cobra.Command{
|
var getCmd = &cobra.Command{
|
||||||
Use: "get <name>",
|
Use: "get [name]",
|
||||||
Short: "Get details of one key",
|
Short: "Get details of one key",
|
||||||
Long: `Return public details of one local key.`,
|
Long: `Return public details of one local key.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: runGetCmd,
|
||||||
if len(args) != 1 || len(args[0]) == 0 {
|
|
||||||
return errors.New("You must provide a name for the key")
|
|
||||||
}
|
|
||||||
name := args[0]
|
|
||||||
|
|
||||||
info, err := GetKeyManager().Get(name)
|
|
||||||
if err == nil {
|
|
||||||
printInfo(info)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func runGetCmd(cmd *cobra.Command, args []string) error {
|
||||||
RootCmd.AddCommand(getCmd)
|
if len(args) != 1 || len(args[0]) == 0 {
|
||||||
|
return errors.New("You must provide a name for the key")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
info, err := GetKeyManager().Get(name)
|
||||||
|
if err == nil {
|
||||||
|
printInfo(info)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// for demos, we enable the key server, probably don't want this
|
||||||
|
// in most binaries we embed the key management into
|
||||||
|
cmd.RegisterServer()
|
||||||
root := cli.PrepareMainCmd(cmd.RootCmd, "TM", os.ExpandEnv("$HOME/.tlc"))
|
root := cli.PrepareMainCmd(cmd.RootCmd, "TM", os.ExpandEnv("$HOME/.tlc"))
|
||||||
root.Execute()
|
root.Execute()
|
||||||
}
|
}
|
||||||
|
16
cmd/list.go
16
cmd/list.go
@ -22,15 +22,13 @@ var listCmd = &cobra.Command{
|
|||||||
Short: "List all keys",
|
Short: "List all keys",
|
||||||
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.`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: runListCmd,
|
||||||
infos, err := GetKeyManager().List()
|
|
||||||
if err == nil {
|
|
||||||
printInfos(infos)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func runListCmd(cmd *cobra.Command, args []string) error {
|
||||||
RootCmd.AddCommand(listCmd)
|
infos, err := GetKeyManager().List()
|
||||||
|
if err == nil {
|
||||||
|
printInfos(infos)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
54
cmd/new.go
54
cmd/new.go
@ -15,42 +15,80 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/tendermint/go-crypto/keys"
|
||||||
|
"github.com/tendermint/go-wire/data"
|
||||||
|
"github.com/tendermint/tmlibs/cli"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagType = "type"
|
||||||
|
flagNoBackup = "no-backup"
|
||||||
|
)
|
||||||
|
|
||||||
// newCmd represents the new command
|
// newCmd represents the new command
|
||||||
var newCmd = &cobra.Command{
|
var newCmd = &cobra.Command{
|
||||||
Use: "new <name>",
|
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.`,
|
||||||
RunE: newPassword,
|
RunE: runNewCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(newCmd)
|
newCmd.Flags().StringP(flagType, "t", "ed25519", "Type of key (ed25519|secp256k1)")
|
||||||
newCmd.Flags().StringP("type", "t", "ed25519", "Type of key (ed25519|secp256k1)")
|
newCmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPassword(cmd *cobra.Command, args []string) error {
|
func runNewCmd(cmd *cobra.Command, args []string) error {
|
||||||
if len(args) != 1 || len(args[0]) == 0 {
|
if len(args) != 1 || len(args[0]) == 0 {
|
||||||
return errors.New("You must provide a name for the key")
|
return errors.New("You must provide a name for the key")
|
||||||
}
|
}
|
||||||
name := args[0]
|
name := args[0]
|
||||||
algo := viper.GetString("type")
|
algo := viper.GetString(flagType)
|
||||||
|
|
||||||
pass, err := getCheckPassword("Enter a passphrase:", "Repeat the passphrase:")
|
pass, err := getCheckPassword("Enter a passphrase:", "Repeat the passphrase:")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := GetKeyManager().Create(name, pass, algo)
|
info, seed, err := GetKeyManager().Create(name, pass, algo)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
printInfo(info)
|
printCreate(info, seed)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NewOutput struct {
|
||||||
|
Key keys.Info `json:"key"`
|
||||||
|
Seed string `json:"seed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func printCreate(info keys.Info, seed string) {
|
||||||
|
switch viper.Get(cli.OutputFlag) {
|
||||||
|
case "text":
|
||||||
|
printInfo(info)
|
||||||
|
// print seed unless requested not to.
|
||||||
|
if !viper.GetBool(flagNoBackup) {
|
||||||
|
fmt.Println("**Important** write this seed phrase in a safe place.")
|
||||||
|
fmt.Println("It is the only way to recover your account if you ever forget your password.\n")
|
||||||
|
fmt.Println(seed)
|
||||||
|
}
|
||||||
|
case "json":
|
||||||
|
out := NewOutput{Key: info}
|
||||||
|
if !viper.GetBool(flagNoBackup) {
|
||||||
|
out.Seed = seed
|
||||||
|
}
|
||||||
|
json, err := data.ToJSON(out)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // really shouldn't happen...
|
||||||
|
}
|
||||||
|
fmt.Println(string(json))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
53
cmd/recover.go
Normal file
53
cmd/recover.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright © 2017 Ethan Frey
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// recoverCmd represents the recover command
|
||||||
|
var recoverCmd = &cobra.Command{
|
||||||
|
Use: "recover [name]",
|
||||||
|
Short: "Change the password for a private key",
|
||||||
|
RunE: runRecoverCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRecoverCmd(cmd *cobra.Command, args []string) error {
|
||||||
|
if len(args) != 1 || len(args[0]) == 0 {
|
||||||
|
return errors.New("You must provide a name for the key")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
pass, err := getPassword("Enter the new passphrase:")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// not really a password... huh?
|
||||||
|
seed, err := getSeed("Enter your recovery seed phrase:")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := GetKeyManager().Recover(name, pass, seed)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
printInfo(info)
|
||||||
|
return nil
|
||||||
|
}
|
30
cmd/root.go
30
cmd/root.go
@ -15,14 +15,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
|
||||||
keys "github.com/tendermint/go-crypto/keys"
|
keys "github.com/tendermint/go-crypto/keys"
|
||||||
"github.com/tendermint/go-crypto/keys/cryptostore"
|
|
||||||
"github.com/tendermint/go-crypto/keys/storage/filestorage"
|
|
||||||
"github.com/tendermint/tmlibs/cli"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const KeySubdir = "keys"
|
const KeySubdir = "keys"
|
||||||
@ -42,17 +36,15 @@ used by light-clients, full nodes, or any other application that
|
|||||||
needs to sign with a private key.`,
|
needs to sign with a private key.`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyManager initializes a key manager based on the configuration
|
func init() {
|
||||||
func GetKeyManager() keys.Manager {
|
RootCmd.AddCommand(getCmd)
|
||||||
if manager == nil {
|
RootCmd.AddCommand(listCmd)
|
||||||
// store the keys directory
|
RootCmd.AddCommand(newCmd)
|
||||||
rootDir := viper.GetString(cli.HomeFlag)
|
RootCmd.AddCommand(updateCmd)
|
||||||
keyDir := filepath.Join(rootDir, KeySubdir)
|
RootCmd.AddCommand(deleteCmd)
|
||||||
// and construct the key manager
|
RootCmd.AddCommand(recoverCmd)
|
||||||
manager = cryptostore.New(
|
}
|
||||||
cryptostore.SecretBox,
|
|
||||||
filestorage.New(keyDir),
|
func RegisterServer() {
|
||||||
)
|
RootCmd.AddCommand(serveCmd)
|
||||||
}
|
|
||||||
return manager
|
|
||||||
}
|
}
|
||||||
|
22
cmd/serve.go
22
cmd/serve.go
@ -28,6 +28,11 @@ import (
|
|||||||
"github.com/tendermint/go-crypto/keys/server"
|
"github.com/tendermint/go-crypto/keys/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
flagPort = "port"
|
||||||
|
flagSocket = "socket"
|
||||||
|
)
|
||||||
|
|
||||||
// serveCmd represents the serve command
|
// serveCmd represents the serve command
|
||||||
var serveCmd = &cobra.Command{
|
var serveCmd = &cobra.Command{
|
||||||
Use: "serve",
|
Use: "serve",
|
||||||
@ -36,27 +41,26 @@ var serveCmd = &cobra.Command{
|
|||||||
private keys much more in depth than the cli can perform.
|
private keys much more in depth than the cli can perform.
|
||||||
In particular, this will allow you to sign transactions with
|
In particular, this will allow you to sign transactions with
|
||||||
the private keys in the store.`,
|
the private keys in the store.`,
|
||||||
RunE: serveHTTP,
|
RunE: runServeCmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RootCmd.AddCommand(serveCmd)
|
serveCmd.Flags().IntP(flagPort, "p", 8118, "TCP Port for listen for http server")
|
||||||
serveCmd.Flags().IntP("port", "p", 8118, "TCP Port for listen for http server")
|
serveCmd.Flags().StringP(flagSocket, "s", "", "UNIX socket for more secure http server")
|
||||||
serveCmd.Flags().StringP("socket", "s", "", "UNIX socket for more secure http server")
|
serveCmd.Flags().StringP(flagType, "t", "ed25519", "Default key type (ed25519|secp256k1)")
|
||||||
serveCmd.Flags().StringP("type", "t", "ed25519", "Default key type (ed25519|secp256k1)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveHTTP(cmd *cobra.Command, args []string) error {
|
func runServeCmd(cmd *cobra.Command, args []string) error {
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
var err error
|
var err error
|
||||||
socket := viper.GetString("socket")
|
socket := viper.GetString(flagSocket)
|
||||||
if socket != "" {
|
if socket != "" {
|
||||||
l, err = createSocket(socket)
|
l, err = createSocket(socket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Cannot create socket")
|
return errors.Wrap(err, "Cannot create socket")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
port := viper.GetInt("port")
|
port := viper.GetInt(flagPort)
|
||||||
l, err = net.Listen("tcp", fmt.Sprintf(":%d", port))
|
l, err = net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Errorf("Cannot listen on port %d", port)
|
return errors.Errorf("Cannot listen on port %d", port)
|
||||||
@ -64,7 +68,7 @@ func serveHTTP(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
ks := server.New(GetKeyManager(), viper.GetString("type"))
|
ks := server.New(GetKeyManager(), viper.GetString(flagType))
|
||||||
ks.Register(router)
|
ks.Register(router)
|
||||||
|
|
||||||
// only set cors for tcp listener
|
// only set cors for tcp listener
|
||||||
|
@ -24,17 +24,12 @@ import (
|
|||||||
|
|
||||||
// updateCmd represents the update command
|
// updateCmd represents the update command
|
||||||
var updateCmd = &cobra.Command{
|
var updateCmd = &cobra.Command{
|
||||||
Use: "update <name>",
|
Use: "update [name]",
|
||||||
Short: "Change the password for a private key",
|
Short: "Change the password for a private key",
|
||||||
Long: `Change the password for a private key.`,
|
RunE: runUpdateCmd,
|
||||||
RunE: updatePassword,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func runUpdateCmd(cmd *cobra.Command, args []string) error {
|
||||||
RootCmd.AddCommand(updateCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updatePassword(cmd *cobra.Command, args []string) error {
|
|
||||||
if len(args) != 1 || len(args[0]) == 0 {
|
if len(args) != 1 || len(args[0]) == 0 {
|
||||||
return errors.New("You must provide a name for the key")
|
return errors.New("You must provide a name for the key")
|
||||||
}
|
}
|
||||||
|
78
cmd/utils.go
78
cmd/utils.go
@ -1,30 +1,96 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bgentry/speakeasy"
|
"github.com/bgentry/speakeasy"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
keys "github.com/tendermint/go-crypto/keys"
|
|
||||||
data "github.com/tendermint/go-wire/data"
|
data "github.com/tendermint/go-wire/data"
|
||||||
"github.com/tendermint/tmlibs/cli"
|
"github.com/tendermint/tmlibs/cli"
|
||||||
|
|
||||||
|
keys "github.com/tendermint/go-crypto/keys"
|
||||||
|
"github.com/tendermint/go-crypto/keys/cryptostore"
|
||||||
|
"github.com/tendermint/go-crypto/keys/storage/filestorage"
|
||||||
)
|
)
|
||||||
|
|
||||||
const PassLength = 10
|
const MinPassLength = 10
|
||||||
|
|
||||||
func getPassword(prompt string) (string, error) {
|
// GetKeyManager initializes a key manager based on the configuration
|
||||||
pass, err := speakeasy.Ask(prompt)
|
func GetKeyManager() keys.Manager {
|
||||||
|
if manager == nil {
|
||||||
|
// store the keys directory
|
||||||
|
rootDir := viper.GetString(cli.HomeFlag)
|
||||||
|
keyDir := filepath.Join(rootDir, KeySubdir)
|
||||||
|
|
||||||
|
// TODO: smarter loading??? with language and fallback?
|
||||||
|
codec := keys.MustLoadCodec("english")
|
||||||
|
|
||||||
|
// and construct the key manager
|
||||||
|
manager = cryptostore.New(
|
||||||
|
cryptostore.SecretBox,
|
||||||
|
filestorage.New(keyDir),
|
||||||
|
codec,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we read from non-tty, we just need to init the buffer reader once,
|
||||||
|
// in case we try to read multiple passwords (eg. update)
|
||||||
|
var buf *bufio.Reader
|
||||||
|
|
||||||
|
func inputIsTty() bool {
|
||||||
|
return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stdinPassword() (string, error) {
|
||||||
|
if buf == nil {
|
||||||
|
buf = bufio.NewReader(os.Stdin)
|
||||||
|
}
|
||||||
|
pass, err := buf.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if len(pass) < PassLength {
|
return strings.TrimSpace(pass), nil
|
||||||
return "", errors.Errorf("Password must be at least %d characters", PassLength)
|
}
|
||||||
|
|
||||||
|
func getPassword(prompt string) (pass string, err error) {
|
||||||
|
if inputIsTty() {
|
||||||
|
pass, err = speakeasy.Ask(prompt)
|
||||||
|
} else {
|
||||||
|
pass, err = stdinPassword()
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(pass) < MinPassLength {
|
||||||
|
return "", errors.Errorf("Password must be at least %d characters", MinPassLength)
|
||||||
}
|
}
|
||||||
return pass, nil
|
return pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSeed(prompt string) (seed string, err error) {
|
||||||
|
if inputIsTty() {
|
||||||
|
fmt.Println(prompt)
|
||||||
|
}
|
||||||
|
seed, err = stdinPassword()
|
||||||
|
seed = strings.TrimSpace(seed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func getCheckPassword(prompt, prompt2 string) (string, error) {
|
func getCheckPassword(prompt, prompt2 string) (string, error) {
|
||||||
|
// simple read on no-tty
|
||||||
|
if !inputIsTty() {
|
||||||
|
return getPassword(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: own function???
|
// TODO: own function???
|
||||||
pass, err := getPassword(prompt)
|
pass, err := getPassword(prompt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
15
glide.lock
generated
15
glide.lock
generated
@ -1,5 +1,5 @@
|
|||||||
hash: 3bcee9fbccf29d21217b24b6a83ec51e1514f37b2ae5d8718cf6c5df80f4fb2c
|
hash: 3bcee9fbccf29d21217b24b6a83ec51e1514f37b2ae5d8718cf6c5df80f4fb2c
|
||||||
updated: 2017-05-15T09:40:53.073691731-04:00
|
updated: 2017-06-19T17:16:58.037568333+02:00
|
||||||
imports:
|
imports:
|
||||||
- name: github.com/bgentry/speakeasy
|
- name: github.com/bgentry/speakeasy
|
||||||
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
|
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
|
||||||
@ -17,8 +17,6 @@ imports:
|
|||||||
- hdkeychain
|
- hdkeychain
|
||||||
- name: github.com/btcsuite/fastsha256
|
- name: github.com/btcsuite/fastsha256
|
||||||
version: 637e656429416087660c84436a2a035d69d54e2e
|
version: 637e656429416087660c84436a2a035d69d54e2e
|
||||||
- name: github.com/clipperhouse/typewriter
|
|
||||||
version: c1a48da378ebb7db1db9f35981b5cc24bf2e5b85
|
|
||||||
- name: github.com/fsnotify/fsnotify
|
- name: github.com/fsnotify/fsnotify
|
||||||
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
|
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
|
||||||
- name: github.com/go-kit/kit
|
- name: github.com/go-kit/kit
|
||||||
@ -60,6 +58,8 @@ imports:
|
|||||||
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
|
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
|
||||||
- name: github.com/magiconair/properties
|
- name: github.com/magiconair/properties
|
||||||
version: 51463bfca2576e06c62a8504b5c0f06d61312647
|
version: 51463bfca2576e06c62a8504b5c0f06d61312647
|
||||||
|
- name: github.com/mattn/go-isatty
|
||||||
|
version: 9622e0cc9d8f9be434ca605520ff9a16808fee47
|
||||||
- name: github.com/mitchellh/mapstructure
|
- name: github.com/mitchellh/mapstructure
|
||||||
version: cc8532a8e9a55ea36402aa21efdf403a60d34096
|
version: cc8532a8e9a55ea36402aa21efdf403a60d34096
|
||||||
- name: github.com/pelletier/go-buffruneio
|
- name: github.com/pelletier/go-buffruneio
|
||||||
@ -88,12 +88,12 @@ imports:
|
|||||||
- edwards25519
|
- edwards25519
|
||||||
- extra25519
|
- extra25519
|
||||||
- name: github.com/tendermint/go-wire
|
- name: github.com/tendermint/go-wire
|
||||||
version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74
|
version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb
|
||||||
subpackages:
|
subpackages:
|
||||||
- data
|
- data
|
||||||
- data/base58
|
- data/base58
|
||||||
- name: github.com/tendermint/tmlibs
|
- name: github.com/tendermint/tmlibs
|
||||||
version: 8f5a175ff4c869fedde710615a11f5745ff69bf3
|
version: bd9d0d1637dadf1330e167189d5e5031aadcda6f
|
||||||
subpackages:
|
subpackages:
|
||||||
- cli
|
- cli
|
||||||
- common
|
- common
|
||||||
@ -119,11 +119,6 @@ imports:
|
|||||||
subpackages:
|
subpackages:
|
||||||
- transform
|
- transform
|
||||||
- unicode/norm
|
- unicode/norm
|
||||||
- name: golang.org/x/tools
|
|
||||||
version: 144c6642b5d832d6c44a53dad6ee61665dd432ce
|
|
||||||
subpackages:
|
|
||||||
- go/ast/astutil
|
|
||||||
- imports
|
|
||||||
- name: gopkg.in/go-playground/validator.v9
|
- name: gopkg.in/go-playground/validator.v9
|
||||||
version: 6d8c18553ea1ac493d049edd6f102f52e618f085
|
version: 6d8c18553ea1ac493d049edd6f102f52e618f085
|
||||||
- name: gopkg.in/yaml.v2
|
- name: gopkg.in/yaml.v2
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
GOTOOLS = \
|
|
||||||
github.com/mitchellh/gox \
|
|
||||||
github.com/Masterminds/glide
|
|
||||||
|
|
||||||
.PHONEY: all test install get_vendor_deps ensure_tools
|
|
||||||
|
|
||||||
all: install test
|
|
||||||
|
|
||||||
test:
|
|
||||||
go test `glide novendor`
|
|
||||||
|
|
||||||
install:
|
|
||||||
go install ./cmd/keys
|
|
||||||
|
|
||||||
get_vendor_deps: ensure_tools
|
|
||||||
@rm -rf vendor/
|
|
||||||
@echo "--> Running glide install"
|
|
||||||
@glide install
|
|
||||||
|
|
||||||
ensure_tools:
|
|
||||||
go get $(GOTOOLS)
|
|
||||||
|
|
@ -1,19 +1,26 @@
|
|||||||
package cryptostore
|
package cryptostore
|
||||||
|
|
||||||
import keys "github.com/tendermint/go-crypto/keys"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
keys "github.com/tendermint/go-crypto/keys"
|
||||||
|
)
|
||||||
|
|
||||||
// Manager combines encyption and storage implementation to provide
|
// Manager combines encyption and storage implementation to provide
|
||||||
// a full-featured key manager
|
// a full-featured key manager
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
es encryptedStorage
|
es encryptedStorage
|
||||||
|
codec keys.Codec
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(coder Encoder, store keys.Storage) Manager {
|
func New(coder Encoder, store keys.Storage, codec keys.Codec) Manager {
|
||||||
return Manager{
|
return Manager{
|
||||||
es: encryptedStorage{
|
es: encryptedStorage{
|
||||||
coder: coder,
|
coder: coder,
|
||||||
store: store,
|
store: store,
|
||||||
},
|
},
|
||||||
|
codec: codec,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,14 +37,41 @@ func (s Manager) assertKeyManager() keys.Manager {
|
|||||||
// 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
|
||||||
//
|
//
|
||||||
// algo must be a supported go-crypto algorithm:
|
// algo must be a supported go-crypto algorithm: ed25519, secp256k1
|
||||||
//
|
func (s Manager) Create(name, passphrase, algo string) (keys.Info, string, error) {
|
||||||
func (s Manager) Create(name, passphrase, algo string) (keys.Info, error) {
|
|
||||||
gen, err := getGenerator(algo)
|
gen, err := getGenerator(algo)
|
||||||
|
if err != nil {
|
||||||
|
return keys.Info{}, "", err
|
||||||
|
}
|
||||||
|
key := gen.Generate()
|
||||||
|
err = s.es.Put(name, passphrase, key)
|
||||||
|
if err != nil {
|
||||||
|
return keys.Info{}, "", err
|
||||||
|
}
|
||||||
|
seed, err := s.codec.BytesToWords(key.Bytes())
|
||||||
|
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), " ")
|
||||||
|
data, err := s.codec.WordsToBytes(words)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return keys.Info{}, err
|
return keys.Info{}, err
|
||||||
}
|
}
|
||||||
key := gen.Generate()
|
|
||||||
|
key, err := crypto.PrivKeyFromBytes(data)
|
||||||
|
if err != nil {
|
||||||
|
return keys.Info{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// d00d, it worked! create the bugger....
|
||||||
err = s.es.Put(name, passphrase, key)
|
err = s.es.Put(name, passphrase, key)
|
||||||
return info(name, key), err
|
return info(name, key), err
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
crypto "github.com/tendermint/go-crypto"
|
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/cryptostore"
|
||||||
"github.com/tendermint/go-crypto/keys/storage/memstorage"
|
"github.com/tendermint/go-crypto/keys/storage/memstorage"
|
||||||
)
|
)
|
||||||
@ -18,6 +19,7 @@ func TestKeyManagement(t *testing.T) {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
|
|
||||||
algo := crypto.NameEd25519
|
algo := crypto.NameEd25519
|
||||||
@ -32,10 +34,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)
|
||||||
i, err := cstore.Create(n1, p1, algo)
|
i, _, err := cstore.Create(n1, p1, algo)
|
||||||
require.Equal(n1, i.Name)
|
require.Equal(n1, i.Name)
|
||||||
require.Nil(err)
|
require.Nil(err)
|
||||||
_, err = cstore.Create(n2, p2, algo)
|
_, _, err = cstore.Create(n2, p2, algo)
|
||||||
require.Nil(err)
|
require.Nil(err)
|
||||||
|
|
||||||
// we can get these keys
|
// we can get these keys
|
||||||
@ -154,6 +156,7 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
|
|
||||||
algo := crypto.NameSecp256k1
|
algo := crypto.NameSecp256k1
|
||||||
@ -161,7 +164,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, algo)
|
_, _, err := cstore.Create(n1, p1, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
assertPassword(assert, cstore, n1, p1, p2)
|
assertPassword(assert, cstore, n1, p1, p2)
|
||||||
|
|
||||||
@ -199,6 +202,41 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
|||||||
assertPassword(assert, cstore, n2, p3, pt)
|
assertPassword(assert, cstore, n2, p3, pt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 ExampleStore() {
|
// func ExampleStore() {
|
||||||
// // Select the encryption and storage for your cryptostore
|
// // Select the encryption and storage for your cryptostore
|
||||||
// cstore := cryptostore.New(
|
// cstore := cryptostore.New(
|
||||||
|
141
keys/ecc.go
Normal file
141
keys/ecc.go
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"hash/crc64"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ECC is used for anything that calculates an error-correcting code
|
||||||
|
type ECC interface {
|
||||||
|
// AddECC calculates an error-correcting code for the input
|
||||||
|
// returns an output with the code appended
|
||||||
|
AddECC([]byte) []byte
|
||||||
|
|
||||||
|
// CheckECC verifies if the ECC is proper on the input and returns
|
||||||
|
// the data with the code removed, or an error
|
||||||
|
CheckECC([]byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoECC is a no-op placeholder, kind of useless... except for tests
|
||||||
|
type NoECC struct{}
|
||||||
|
|
||||||
|
var _ ECC = NoECC{}
|
||||||
|
|
||||||
|
func (_ NoECC) AddECC(input []byte) []byte { return input }
|
||||||
|
func (_ NoECC) CheckECC(input []byte) ([]byte, error) { return input, nil }
|
||||||
|
|
||||||
|
// CRC32 does the ieee crc32 polynomial check
|
||||||
|
type CRC32 struct {
|
||||||
|
Poly uint32
|
||||||
|
table *crc32.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ECC = &CRC32{}
|
||||||
|
|
||||||
|
func NewIEEECRC32() *CRC32 {
|
||||||
|
return &CRC32{Poly: crc32.IEEE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCastagnoliCRC32() *CRC32 {
|
||||||
|
return &CRC32{Poly: crc32.Castagnoli}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewKoopmanCRC32() *CRC32 {
|
||||||
|
return &CRC32{Poly: crc32.Koopman}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC32) AddECC(input []byte) []byte {
|
||||||
|
table := c.getTable()
|
||||||
|
|
||||||
|
// get crc and convert to some bytes...
|
||||||
|
crc := crc32.Checksum(input, table)
|
||||||
|
check := make([]byte, crc32.Size)
|
||||||
|
binary.BigEndian.PutUint32(check, crc)
|
||||||
|
|
||||||
|
// append it to the input
|
||||||
|
output := append(input, check...)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC32) CheckECC(input []byte) ([]byte, error) {
|
||||||
|
table := c.getTable()
|
||||||
|
|
||||||
|
if len(input) <= crc32.Size {
|
||||||
|
return nil, errors.New("input too short, no checksum present")
|
||||||
|
}
|
||||||
|
cut := len(input) - crc32.Size
|
||||||
|
data, check := input[:cut], input[cut:]
|
||||||
|
crc := binary.BigEndian.Uint32(check)
|
||||||
|
calc := crc32.Checksum(data, table)
|
||||||
|
if crc != calc {
|
||||||
|
return nil, errors.New("Checksum does not match")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC32) getTable() *crc32.Table {
|
||||||
|
if c.table == nil {
|
||||||
|
if c.Poly == 0 {
|
||||||
|
c.Poly = crc32.IEEE
|
||||||
|
}
|
||||||
|
c.table = crc32.MakeTable(c.Poly)
|
||||||
|
}
|
||||||
|
return c.table
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRC64 does the ieee crc64 polynomial check
|
||||||
|
type CRC64 struct {
|
||||||
|
Poly uint64
|
||||||
|
table *crc64.Table
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ECC = &CRC64{}
|
||||||
|
|
||||||
|
func NewISOCRC64() *CRC64 {
|
||||||
|
return &CRC64{Poly: crc64.ISO}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewECMACRC64() *CRC64 {
|
||||||
|
return &CRC64{Poly: crc64.ECMA}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC64) AddECC(input []byte) []byte {
|
||||||
|
table := c.getTable()
|
||||||
|
|
||||||
|
// get crc and convert to some bytes...
|
||||||
|
crc := crc64.Checksum(input, table)
|
||||||
|
check := make([]byte, crc64.Size)
|
||||||
|
binary.BigEndian.PutUint64(check, crc)
|
||||||
|
|
||||||
|
// append it to the input
|
||||||
|
output := append(input, check...)
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC64) CheckECC(input []byte) ([]byte, error) {
|
||||||
|
table := c.getTable()
|
||||||
|
|
||||||
|
if len(input) <= crc64.Size {
|
||||||
|
return nil, errors.New("input too short, no checksum present")
|
||||||
|
}
|
||||||
|
cut := len(input) - crc64.Size
|
||||||
|
data, check := input[:cut], input[cut:]
|
||||||
|
crc := binary.BigEndian.Uint64(check)
|
||||||
|
calc := crc64.Checksum(data, table)
|
||||||
|
if crc != calc {
|
||||||
|
return nil, errors.New("Checksum does not match")
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CRC64) getTable() *crc64.Table {
|
||||||
|
if c.table == nil {
|
||||||
|
if c.Poly == 0 {
|
||||||
|
c.Poly = crc64.ISO
|
||||||
|
}
|
||||||
|
c.table = crc64.MakeTable(c.Poly)
|
||||||
|
}
|
||||||
|
return c.table
|
||||||
|
}
|
65
keys/ecc_test.go
Normal file
65
keys/ecc_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
cmn "github.com/tendermint/tmlibs/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestECCPasses makes sure that the AddECC/CheckECC methods are symetric
|
||||||
|
func TestECCPasses(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
checks := []ECC{
|
||||||
|
NoECC{},
|
||||||
|
NewIEEECRC32(),
|
||||||
|
NewCastagnoliCRC32(),
|
||||||
|
NewKoopmanCRC32(),
|
||||||
|
NewISOCRC64(),
|
||||||
|
NewECMACRC64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
for i := 0; i < 2000; i++ {
|
||||||
|
numBytes := cmn.RandInt()%60 + 1
|
||||||
|
data := cmn.RandBytes(numBytes)
|
||||||
|
|
||||||
|
checked := check.AddECC(data)
|
||||||
|
res, err := check.CheckECC(checked)
|
||||||
|
if assert.Nil(err, "%#v: %+v", check, err) {
|
||||||
|
assert.Equal(data, res, "%v", check)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestECCFails makes sure random data will (usually) fail the checksum
|
||||||
|
func TestECCFails(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
checks := []ECC{
|
||||||
|
NewIEEECRC32(),
|
||||||
|
NewCastagnoliCRC32(),
|
||||||
|
NewKoopmanCRC32(),
|
||||||
|
NewISOCRC64(),
|
||||||
|
NewECMACRC64(),
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts := 2000
|
||||||
|
|
||||||
|
for _, check := range checks {
|
||||||
|
failed := 0
|
||||||
|
for i := 0; i < attempts; i++ {
|
||||||
|
numBytes := cmn.RandInt()%60 + 1
|
||||||
|
data := cmn.RandBytes(numBytes)
|
||||||
|
_, err := check.CheckECC(data)
|
||||||
|
if err != nil {
|
||||||
|
failed += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we allow up to 1 falsely accepted checksums, as there are random matches
|
||||||
|
assert.InDelta(attempts, failed, 1, "%v", check)
|
||||||
|
}
|
||||||
|
}
|
@ -31,13 +31,14 @@ func (k Keys) GenerateKey(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
key, err := k.manager.Create(req.Name, req.Passphrase, req.Algo)
|
key, seed, err := k.manager.Create(req.Name, req.Passphrase, req.Algo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, err)
|
writeError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSuccess(w, &key)
|
res := types.CreateKeyResponse{key, seed}
|
||||||
|
writeSuccess(w, &res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k Keys) GetKey(w http.ResponseWriter, r *http.Request) {
|
func (k Keys) GetKey(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -40,13 +40,15 @@ func TestKeyServer(t *testing.T) {
|
|||||||
key, code, err := createKey(r, n1, p1, algo)
|
key, code, err := createKey(r, n1, p1, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
require.Equal(http.StatusOK, code)
|
require.Equal(http.StatusOK, code)
|
||||||
require.Equal(key.Name, n1)
|
require.Equal(n1, key.Key.Name)
|
||||||
|
require.NotEmpty(n1, key.Seed)
|
||||||
|
|
||||||
// the other one works
|
// the other one works
|
||||||
key2, code, err := createKey(r, n2, p2, algo)
|
key2, code, err := createKey(r, n2, p2, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
require.Equal(http.StatusOK, code)
|
require.Equal(http.StatusOK, code)
|
||||||
require.Equal(key2.Name, n2)
|
require.Equal(key2.Key.Name, n2)
|
||||||
|
require.NotEmpty(n2, key.Seed)
|
||||||
|
|
||||||
// let's abstract this out a bit....
|
// let's abstract this out a bit....
|
||||||
keys, code, err = listKeys(r)
|
keys, code, err = listKeys(r)
|
||||||
@ -62,9 +64,9 @@ func TestKeyServer(t *testing.T) {
|
|||||||
k, code, err := getKey(r, n1)
|
k, code, err := getKey(r, n1)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
require.Equal(http.StatusOK, code)
|
require.Equal(http.StatusOK, code)
|
||||||
assert.Equal(k.Name, n1)
|
assert.Equal(n1, k.Name)
|
||||||
assert.NotNil(k.Address)
|
assert.NotNil(k.Address)
|
||||||
assert.Equal(k.Address, key.Address)
|
assert.Equal(key.Key.Address, k.Address)
|
||||||
|
|
||||||
// delete with proper key
|
// delete with proper key
|
||||||
_, code, err = deleteKey(r, n1, p1)
|
_, code, err = deleteKey(r, n1, p1)
|
||||||
@ -89,6 +91,7 @@ func setupServer() http.Handler {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
|
|
||||||
// build your http server
|
// build your http server
|
||||||
@ -134,7 +137,7 @@ func getKey(h http.Handler, name string) (*keys.Info, int, error) {
|
|||||||
return &data, rr.Code, err
|
return &data, rr.Code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func createKey(h http.Handler, name, passphrase, algo string) (*keys.Info, int, error) {
|
func createKey(h http.Handler, name, passphrase, algo string) (*types.CreateKeyResponse, int, error) {
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
post := types.CreateKeyRequest{
|
post := types.CreateKeyRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
@ -157,9 +160,9 @@ func createKey(h http.Handler, name, passphrase, algo string) (*keys.Info, int,
|
|||||||
return nil, rr.Code, nil
|
return nil, rr.Code, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data := keys.Info{}
|
data := new(types.CreateKeyResponse)
|
||||||
err = json.Unmarshal(rr.Body.Bytes(), &data)
|
err = json.Unmarshal(rr.Body.Bytes(), data)
|
||||||
return &data, rr.Code, err
|
return data, rr.Code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) {
|
func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
|
import "github.com/tendermint/go-crypto/keys"
|
||||||
|
|
||||||
// CreateKeyRequest is sent to create a new key
|
// CreateKeyRequest is sent to create a new key
|
||||||
type CreateKeyRequest struct {
|
type CreateKeyRequest struct {
|
||||||
Name string `json:"name" validate:"required,min=4,printascii"`
|
Name string `json:"name" validate:"required,min=4,printascii"`
|
||||||
@ -26,3 +28,8 @@ type ErrorResponse struct {
|
|||||||
Error string `json:"error"` // error message if Success is false
|
Error string `json:"error"` // error message if Success is false
|
||||||
Code int `json:"code"` // error code if Success is false
|
Code int `json:"code"` // error code if Success is false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CreateKeyResponse struct {
|
||||||
|
Key keys.Info `json:"key"`
|
||||||
|
Seed string `json:"seed_phrase"`
|
||||||
|
}
|
||||||
|
@ -63,7 +63,10 @@ type Signer interface {
|
|||||||
// Manager allows simple CRUD on a keystore, as an aid to signing
|
// Manager allows simple CRUD on a keystore, as an aid to signing
|
||||||
type Manager interface {
|
type Manager interface {
|
||||||
Signer
|
Signer
|
||||||
Create(name, passphrase, algo string) (Info, error)
|
// 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)
|
List() (Infos, error)
|
||||||
Get(name string) (Info, error)
|
Get(name string) (Info, error)
|
||||||
Update(name, oldpass, newpass string) error
|
Update(name, oldpass, newpass string) error
|
||||||
|
@ -18,13 +18,14 @@ func TestMultiSig(t *testing.T) {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
n, p := "foo", "bar"
|
n, p := "foo", "bar"
|
||||||
n2, p2 := "other", "thing"
|
n2, p2 := "other", "thing"
|
||||||
|
|
||||||
acct, err := cstore.Create(n, p, algo)
|
acct, _, err := cstore.Create(n, p, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
acct2, err := cstore.Create(n2, p2, algo)
|
acct2, _, err := cstore.Create(n2, p2, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
type signer struct {
|
type signer struct {
|
||||||
|
@ -18,13 +18,14 @@ func TestOneSig(t *testing.T) {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
n, p := "foo", "bar"
|
n, p := "foo", "bar"
|
||||||
n2, p2 := "other", "thing"
|
n2, p2 := "other", "thing"
|
||||||
|
|
||||||
acct, err := cstore.Create(n, p, algo)
|
acct, _, err := cstore.Create(n, p, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
acct2, err := cstore.Create(n2, p2, algo)
|
acct2, _, err := cstore.Create(n2, p2, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
@ -6,9 +6,10 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
crypto "github.com/tendermint/go-crypto"
|
crypto "github.com/tendermint/go-crypto"
|
||||||
data "github.com/tendermint/go-wire/data"
|
"github.com/tendermint/go-crypto/keys"
|
||||||
"github.com/tendermint/go-crypto/keys/cryptostore"
|
"github.com/tendermint/go-crypto/keys/cryptostore"
|
||||||
"github.com/tendermint/go-crypto/keys/storage/memstorage"
|
"github.com/tendermint/go-crypto/keys/storage/memstorage"
|
||||||
|
data "github.com/tendermint/go-wire/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReader(t *testing.T) {
|
func TestReader(t *testing.T) {
|
||||||
@ -18,14 +19,15 @@ func TestReader(t *testing.T) {
|
|||||||
cstore := cryptostore.New(
|
cstore := cryptostore.New(
|
||||||
cryptostore.SecretBox,
|
cryptostore.SecretBox,
|
||||||
memstorage.New(),
|
memstorage.New(),
|
||||||
|
keys.MustLoadCodec("english"),
|
||||||
)
|
)
|
||||||
type sigs struct{ name, pass string }
|
type sigs struct{ name, pass string }
|
||||||
u := sigs{"alice", "1234"}
|
u := sigs{"alice", "1234"}
|
||||||
u2 := sigs{"bob", "foobar"}
|
u2 := sigs{"bob", "foobar"}
|
||||||
|
|
||||||
_, err := cstore.Create(u.name, u.pass, algo)
|
_, _, err := cstore.Create(u.name, u.pass, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
_, err = cstore.Create(u2.name, u2.pass, algo)
|
_, _, err = cstore.Create(u2.name, u2.pass, algo)
|
||||||
require.Nil(err, "%+v", err)
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
199
keys/wordcodec.go
Normal file
199
keys/wordcodec.go
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/tendermint/go-crypto/keys/wordlist"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BankSize = 2048
|
||||||
|
|
||||||
|
// TODO: add error-checking codecs for invalid phrases
|
||||||
|
|
||||||
|
type Codec interface {
|
||||||
|
BytesToWords([]byte) ([]string, error)
|
||||||
|
WordsToBytes([]string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WordCodec struct {
|
||||||
|
words []string
|
||||||
|
bytes map[string]int
|
||||||
|
check ECC
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Codec = &WordCodec{}
|
||||||
|
|
||||||
|
func NewCodec(words []string) (codec *WordCodec, err error) {
|
||||||
|
if len(words) != BankSize {
|
||||||
|
return codec, errors.Errorf("Bank must have %d words, found %d", BankSize, len(words))
|
||||||
|
}
|
||||||
|
|
||||||
|
res := &WordCodec{
|
||||||
|
words: words,
|
||||||
|
// TODO: configure this outside???
|
||||||
|
check: NewIEEECRC32(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadCodec loads a pre-compiled language file
|
||||||
|
func LoadCodec(bank string) (codec *WordCodec, err error) {
|
||||||
|
words, err := loadBank(bank)
|
||||||
|
if err != nil {
|
||||||
|
return codec, err
|
||||||
|
}
|
||||||
|
return NewCodec(words)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustLoadCodec panics if word bank is missing, only for tests
|
||||||
|
func MustLoadCodec(bank string) *WordCodec {
|
||||||
|
codec, err := LoadCodec(bank)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return codec
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadBank opens a wordlist file and returns all words inside
|
||||||
|
func loadBank(bank string) ([]string, error) {
|
||||||
|
filename := "keys/wordlist/" + bank + ".txt"
|
||||||
|
words, err := wordlist.Asset(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wordsAll := strings.Split(strings.TrimSpace(string(words)), "\n")
|
||||||
|
return wordsAll, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// // TODO: read from go-bind assets
|
||||||
|
// func getData(filename string) (string, error) {
|
||||||
|
// f, err := os.Open(filename)
|
||||||
|
// if err != nil {
|
||||||
|
// return "", errors.WithStack(err)
|
||||||
|
// }
|
||||||
|
// defer f.Close()
|
||||||
|
|
||||||
|
// data, err := ioutil.ReadAll(f)
|
||||||
|
// if err != nil {
|
||||||
|
// return "", errors.WithStack(err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return string(data), nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// given this many bytes, we will produce this many words
|
||||||
|
func wordlenFromBytes(numBytes int) int {
|
||||||
|
// 2048 words per bank, which is 2^11.
|
||||||
|
// 8 bits per byte, and we add +10 so it rounds up
|
||||||
|
return (8*numBytes + 10) / 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// given this many words, we will produce this many bytes.
|
||||||
|
// sometimes there are two possibilities.
|
||||||
|
// if maybeShorter is true, then represents len OR len-1 bytes
|
||||||
|
func bytelenFromWords(numWords int) (length int, maybeShorter bool) {
|
||||||
|
// calculate the max number of complete bytes we could store in this word
|
||||||
|
length = 11 * numWords / 8
|
||||||
|
// if one less byte would also generate this length, set maybeShorter
|
||||||
|
if wordlenFromBytes(length-1) == numWords {
|
||||||
|
maybeShorter = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add checksum
|
||||||
|
func (c *WordCodec) BytesToWords(raw []byte) (words []string, err error) {
|
||||||
|
// always add a checksum to the data
|
||||||
|
data := c.check.AddECC(raw)
|
||||||
|
numWords := wordlenFromBytes(len(data))
|
||||||
|
|
||||||
|
n2048 := big.NewInt(2048)
|
||||||
|
nData := big.NewInt(0).SetBytes(data)
|
||||||
|
nRem := big.NewInt(0)
|
||||||
|
// Alternative, use condition "nData.BitLen() > 0"
|
||||||
|
// to allow for shorter words when data has leading 0's
|
||||||
|
for i := 0; i < numWords; i++ {
|
||||||
|
nData.DivMod(nData, n2048, nRem)
|
||||||
|
rem := nRem.Int64()
|
||||||
|
w := c.words[rem]
|
||||||
|
// double-check bank on generation...
|
||||||
|
_, err := c.GetIndex(w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
words = append(words, w)
|
||||||
|
}
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *WordCodec) WordsToBytes(words []string) ([]byte, error) {
|
||||||
|
l := len(words)
|
||||||
|
|
||||||
|
if l == 0 {
|
||||||
|
return nil, errors.New("Didn't provide any words")
|
||||||
|
}
|
||||||
|
|
||||||
|
n2048 := big.NewInt(2048)
|
||||||
|
nData := big.NewInt(0)
|
||||||
|
// since we output words based on the remainder, the first word has the lowest
|
||||||
|
// value... we must load them in reverse order
|
||||||
|
for i := 1; i <= l; i++ {
|
||||||
|
rem, err := c.GetIndex(words[l-i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nRem := big.NewInt(int64(rem))
|
||||||
|
nData.Mul(nData, n2048)
|
||||||
|
nData.Add(nData, nRem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we copy into a slice of the expected size, so it is not shorter if there
|
||||||
|
// are lots of leading 0s
|
||||||
|
dataBytes := nData.Bytes()
|
||||||
|
|
||||||
|
// copy into the container we have with the expected size
|
||||||
|
outLen, flex := bytelenFromWords(len(words))
|
||||||
|
toCheck := make([]byte, outLen)
|
||||||
|
if len(dataBytes) > outLen {
|
||||||
|
return nil, errors.New("Invalid data, could not have been generated by this codec")
|
||||||
|
}
|
||||||
|
copy(toCheck[outLen-len(dataBytes):], dataBytes)
|
||||||
|
|
||||||
|
// validate the checksum...
|
||||||
|
output, err := c.check.CheckECC(toCheck)
|
||||||
|
if flex && err != nil {
|
||||||
|
// if flex, try again one shorter....
|
||||||
|
toCheck = toCheck[1:]
|
||||||
|
output, err = c.check.CheckECC(toCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIndex finds the index of the words to create bytes
|
||||||
|
// Generates a map the first time it is loaded, to avoid needless
|
||||||
|
// computation when list is not used.
|
||||||
|
func (c *WordCodec) GetIndex(word string) (int, error) {
|
||||||
|
// generate the first time
|
||||||
|
if c.bytes == nil {
|
||||||
|
b := map[string]int{}
|
||||||
|
for i, w := range c.words {
|
||||||
|
if _, ok := b[w]; ok {
|
||||||
|
return -1, errors.Errorf("Duplicate word in list: %s", w)
|
||||||
|
}
|
||||||
|
b[w] = i
|
||||||
|
}
|
||||||
|
c.bytes = b
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the index, or an error
|
||||||
|
rem, ok := c.bytes[word]
|
||||||
|
if !ok {
|
||||||
|
return -1, errors.Errorf("Unrecognized word: %s", word)
|
||||||
|
}
|
||||||
|
return rem, nil
|
||||||
|
}
|
180
keys/wordcodec_test.go
Normal file
180
keys/wordcodec_test.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
cmn "github.com/tendermint/tmlibs/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLengthCalc(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
bytes, words int
|
||||||
|
flexible bool
|
||||||
|
}{
|
||||||
|
{1, 1, false},
|
||||||
|
{2, 2, false},
|
||||||
|
// bytes pairs with same word count
|
||||||
|
{3, 3, true},
|
||||||
|
{4, 3, true},
|
||||||
|
{5, 4, false},
|
||||||
|
// bytes pairs with same word count
|
||||||
|
{10, 8, true},
|
||||||
|
{11, 8, true},
|
||||||
|
{12, 9, false},
|
||||||
|
{13, 10, false},
|
||||||
|
{20, 15, false},
|
||||||
|
// bytes pairs with same word count
|
||||||
|
{21, 16, true},
|
||||||
|
{32, 24, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
wl := wordlenFromBytes(tc.bytes)
|
||||||
|
assert.Equal(tc.words, wl, "%d", tc.bytes)
|
||||||
|
|
||||||
|
bl, flex := bytelenFromWords(tc.words)
|
||||||
|
assert.Equal(tc.flexible, flex, "%d", tc.words)
|
||||||
|
if !flex {
|
||||||
|
assert.Equal(tc.bytes, bl, "%d", tc.words)
|
||||||
|
} else {
|
||||||
|
// check if it is either tc.bytes or tc.bytes +1
|
||||||
|
choices := []int{tc.bytes, tc.bytes + 1}
|
||||||
|
assert.Contains(choices, bl, "%d", tc.words)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeDecode(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
codec, err := LoadCodec("english")
|
||||||
|
require.Nil(err, "%+v", err)
|
||||||
|
|
||||||
|
cases := [][]byte{
|
||||||
|
{7, 8, 9}, // TODO: 3 words -> 3 or 4 bytes
|
||||||
|
{12, 54, 99, 11}, // TODO: 3 words -> 3 or 4 bytes
|
||||||
|
{0, 54, 99, 11}, // TODO: 3 words -> 3 or 4 bytes, detect leading 0
|
||||||
|
{1, 2, 3, 4, 5}, // normal
|
||||||
|
{0, 0, 0, 0, 122, 23, 82, 195}, // leading 0s (8 chars, unclear)
|
||||||
|
{0, 0, 0, 0, 5, 22, 123, 55, 22}, // leading 0s (9 chars, clear)
|
||||||
|
{22, 44, 55, 1, 13, 0, 0, 0, 0}, // trailing 0s (9 chars, clear)
|
||||||
|
{0, 5, 253, 2, 0}, // leading and trailing zeros
|
||||||
|
{255, 196, 172, 234, 192, 255}, // big numbers
|
||||||
|
{255, 196, 172, 1, 234, 192, 255}, // big numbers, two length choices
|
||||||
|
// others?
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
w, err := codec.BytesToWords(tc)
|
||||||
|
if assert.Nil(err, "%d: %v", i, err) {
|
||||||
|
b, err := codec.WordsToBytes(w)
|
||||||
|
if assert.Nil(err, "%d: %v", i, err) {
|
||||||
|
assert.Equal(len(tc), len(b))
|
||||||
|
assert.Equal(tc, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckInvalidLists(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
trivial := []string{"abc", "def"}
|
||||||
|
short := make([]string, 1234)
|
||||||
|
long := make([]string, BankSize+1)
|
||||||
|
right := make([]string, BankSize)
|
||||||
|
dups := make([]string, BankSize)
|
||||||
|
|
||||||
|
for _, list := range [][]string{short, long, right, dups} {
|
||||||
|
for i := range list {
|
||||||
|
list[i] = cmn.RandStr(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// create one single duplicate
|
||||||
|
dups[192] = dups[782]
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
words []string
|
||||||
|
loadable bool
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{trivial, false, false},
|
||||||
|
{short, false, false},
|
||||||
|
{long, false, false},
|
||||||
|
{dups, true, false}, // we only check dups on first use...
|
||||||
|
{right, true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
codec, err := NewCodec(tc.words)
|
||||||
|
if !tc.loadable {
|
||||||
|
assert.NotNil(err, "%d", i)
|
||||||
|
} else if assert.Nil(err, "%d: %+v", i, err) {
|
||||||
|
data := cmn.RandBytes(32)
|
||||||
|
w, err := codec.BytesToWords(data)
|
||||||
|
if tc.valid {
|
||||||
|
assert.Nil(err, "%d: %+v", i, err)
|
||||||
|
b, err := codec.WordsToBytes(w)
|
||||||
|
assert.Nil(err, "%d: %+v", i, err)
|
||||||
|
assert.Equal(data, b)
|
||||||
|
} else {
|
||||||
|
assert.NotNil(err, "%d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRandWord(c *WordCodec) string {
|
||||||
|
idx := cmn.RandInt() % BankSize
|
||||||
|
return c.words[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDiffWord(c *WordCodec, not string) string {
|
||||||
|
w := getRandWord(c)
|
||||||
|
if w == not {
|
||||||
|
w = getRandWord(c)
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckTypoDetection(t *testing.T) {
|
||||||
|
assert, require := assert.New(t), require.New(t)
|
||||||
|
|
||||||
|
banks := []string{"english", "spanish", "japanese", "chinese_simplified"}
|
||||||
|
|
||||||
|
for _, bank := range banks {
|
||||||
|
codec, err := LoadCodec(bank)
|
||||||
|
require.Nil(err, "%s: %+v", bank, err)
|
||||||
|
for i := 0; i < 1000; i++ {
|
||||||
|
numBytes := cmn.RandInt()%60 + 1
|
||||||
|
data := cmn.RandBytes(numBytes)
|
||||||
|
|
||||||
|
words, err := codec.BytesToWords(data)
|
||||||
|
assert.Nil(err, "%s: %+v", bank, err)
|
||||||
|
good, err := codec.WordsToBytes(words)
|
||||||
|
assert.Nil(err, "%s: %+v", bank, err)
|
||||||
|
assert.Equal(data, good, bank)
|
||||||
|
|
||||||
|
// now try some tweaks...
|
||||||
|
cut := words[1:]
|
||||||
|
_, err = codec.WordsToBytes(cut)
|
||||||
|
assert.NotNil(err, "%s: %s", bank, words)
|
||||||
|
|
||||||
|
// swap a word within the bank, should fails
|
||||||
|
words[3] = getDiffWord(codec, words[3])
|
||||||
|
_, err = codec.WordsToBytes(words)
|
||||||
|
assert.NotNil(err, "%s: %s", bank, words)
|
||||||
|
|
||||||
|
// put a random word here, must fail
|
||||||
|
words[3] = cmn.RandStr(10)
|
||||||
|
_, err = codec.WordsToBytes(words)
|
||||||
|
assert.NotNil(err, "%s: %s", bank, words)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
keys/wordcodecbench_test.go
Normal file
68
keys/wordcodecbench_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package keys
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
cmn "github.com/tendermint/tmlibs/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func warmupCodec(bank string) *WordCodec {
|
||||||
|
codec, err := LoadCodec(bank)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err = codec.GetIndex(codec.words[123])
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCodec(b *testing.B) {
|
||||||
|
banks := []string{"english", "spanish", "japanese", "chinese_simplified"}
|
||||||
|
|
||||||
|
for _, bank := range banks {
|
||||||
|
b.Run(bank, func(sub *testing.B) {
|
||||||
|
codec := warmupCodec(bank)
|
||||||
|
sub.ResetTimer()
|
||||||
|
benchSuite(sub, codec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchSuite(b *testing.B, codec *WordCodec) {
|
||||||
|
b.Run("to_words", func(sub *testing.B) {
|
||||||
|
benchMakeWords(sub, codec)
|
||||||
|
})
|
||||||
|
b.Run("to_bytes", func(sub *testing.B) {
|
||||||
|
benchParseWords(sub, codec)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchMakeWords(b *testing.B, codec *WordCodec) {
|
||||||
|
numBytes := 32
|
||||||
|
data := cmn.RandBytes(numBytes)
|
||||||
|
for i := 1; i <= b.N; i++ {
|
||||||
|
_, err := codec.BytesToWords(data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchParseWords(b *testing.B, codec *WordCodec) {
|
||||||
|
// generate a valid test string to parse
|
||||||
|
numBytes := 32
|
||||||
|
data := cmn.RandBytes(numBytes)
|
||||||
|
words, err := codec.BytesToWords(data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 1; i <= b.N; i++ {
|
||||||
|
_, err := codec.WordsToBytes(words)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2048
keys/wordlist/chinese_simplified.txt
Normal file
2048
keys/wordlist/chinese_simplified.txt
Normal file
File diff suppressed because it is too large
Load Diff
2048
keys/wordlist/english.txt
Normal file
2048
keys/wordlist/english.txt
Normal file
File diff suppressed because it is too large
Load Diff
2048
keys/wordlist/japanese.txt
Normal file
2048
keys/wordlist/japanese.txt
Normal file
File diff suppressed because it is too large
Load Diff
2048
keys/wordlist/spanish.txt
Normal file
2048
keys/wordlist/spanish.txt
Normal file
File diff suppressed because it is too large
Load Diff
308
keys/wordlist/wordlist.go
Normal file
308
keys/wordlist/wordlist.go
Normal file
File diff suppressed because one or more lines are too long
111
tests/keys.sh
Executable file
111
tests/keys.sh
Executable file
@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
EXE=keys
|
||||||
|
|
||||||
|
oneTimeSetUp() {
|
||||||
|
PASS=qwertyuiop
|
||||||
|
export TM_HOME=$HOME/.keys_test
|
||||||
|
rm -rf $TM_HOME
|
||||||
|
assertTrue $?
|
||||||
|
}
|
||||||
|
|
||||||
|
newKey(){
|
||||||
|
assertNotNull "keyname required" "$1"
|
||||||
|
KEYPASS=${2:-qwertyuiop}
|
||||||
|
KEY=$(echo $KEYPASS | ${EXE} new $1 -o json)
|
||||||
|
if ! assertTrue "created $1" $?; then return 1; fi
|
||||||
|
assertEquals "$1" $(echo $KEY | jq .key.name | tr -d \")
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
# updateKey <name> <oldkey> <newkey>
|
||||||
|
updateKey() {
|
||||||
|
(echo $2; echo $3) | keys update $1 > /dev/null
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
test00MakeKeys() {
|
||||||
|
USER=demouser
|
||||||
|
assertFalse "already user $USER" "${EXE} get $USER"
|
||||||
|
newKey $USER
|
||||||
|
assertTrue "no user $USER" "${EXE} get $USER"
|
||||||
|
# make sure bad password not accepted
|
||||||
|
assertFalse "accepts short password" "echo 123 | keys new badpass"
|
||||||
|
}
|
||||||
|
|
||||||
|
test01ListKeys() {
|
||||||
|
# one line plus the number of keys
|
||||||
|
assertEquals "2" $(keys list | wc -l)
|
||||||
|
newKey foobar
|
||||||
|
assertEquals "3" $(keys list | wc -l)
|
||||||
|
# we got the proper name here...
|
||||||
|
assertEquals "foobar" $(keys list -o json | jq .[1].name | tr -d \" )
|
||||||
|
# we get all names in normal output
|
||||||
|
EXPECTEDNAMES=$(echo demouser; echo foobar)
|
||||||
|
TEXTNAMES=$(keys list | tail -n +2 | cut -f1)
|
||||||
|
assertEquals "$EXPECTEDNAMES" "$TEXTNAMES"
|
||||||
|
# let's make sure the addresses match!
|
||||||
|
assertEquals "text and json addresses don't match" $(keys list | tail -1 | cut -f3) $(keys list -o json | jq .[1].address | tr -d \")
|
||||||
|
}
|
||||||
|
|
||||||
|
test02updateKeys() {
|
||||||
|
USER=changer
|
||||||
|
PASS1=awsedrftgyhu
|
||||||
|
PASS2=S4H.9j.D9S7hso
|
||||||
|
PASS3=h8ybO7GY6d2
|
||||||
|
|
||||||
|
newKey $USER $PASS1
|
||||||
|
assertFalse "accepts invalid pass" "updateKey $USER $PASS2 $PASS2"
|
||||||
|
assertTrue "doesn't update" "updateKey $USER $PASS1 $PASS2"
|
||||||
|
assertTrue "takes new key after update" "updateKey $USER $PASS2 $PASS3"
|
||||||
|
}
|
||||||
|
|
||||||
|
test03recoverKeys() {
|
||||||
|
USER=sleepy
|
||||||
|
PASS1=S4H.9j.D9S7hso
|
||||||
|
|
||||||
|
USER2=easy
|
||||||
|
PASS2=1234567890
|
||||||
|
|
||||||
|
# make a user and check they exist
|
||||||
|
echo "create..."
|
||||||
|
KEY=$(echo $PASS1 | ${EXE} new $USER -o json)
|
||||||
|
if ! assertTrue "created $USER" $?; then return 1; fi
|
||||||
|
if [ -n "$DEBUG" ]; then echo $KEY; echo; fi
|
||||||
|
|
||||||
|
SEED=$(echo $KEY | jq .seed | tr -d \")
|
||||||
|
ADDR=$(echo $KEY | jq .key.address | tr -d \")
|
||||||
|
PUBKEY=$(echo $KEY | jq .key.pubkey | tr -d \")
|
||||||
|
assertTrue "${EXE} get $USER > /dev/null"
|
||||||
|
|
||||||
|
# let's delete this key
|
||||||
|
echo "delete..."
|
||||||
|
assertFalse "echo foo | ${EXE} delete $USER > /dev/null"
|
||||||
|
assertTrue "echo $PASS1 | ${EXE} delete $USER > /dev/null"
|
||||||
|
assertFalse "${EXE} get $USER > /dev/null"
|
||||||
|
|
||||||
|
# fails on short password
|
||||||
|
echo "recover..."
|
||||||
|
assertFalse "echo foo; echo $SEED | ${EXE} recover $USER2 -o json > /dev/null"
|
||||||
|
# fails on bad seed
|
||||||
|
assertFalse "echo $PASS2; echo \"silly white whale tower bongo\" | ${EXE} recover $USER2 -o json > /dev/null"
|
||||||
|
# now we got it
|
||||||
|
KEY2=$((echo $PASS2; echo $SEED) | ${EXE} recover $USER2 -o json)
|
||||||
|
if ! assertTrue "recovery failed: $KEY2" $?; then return 1; fi
|
||||||
|
if [ -n "$DEBUG" ]; then echo $KEY2; echo; fi
|
||||||
|
|
||||||
|
# make sure it looks the same
|
||||||
|
NAME2=$(echo $KEY2 | jq .name | tr -d \")
|
||||||
|
ADDR2=$(echo $KEY2 | jq .address | tr -d \")
|
||||||
|
PUBKEY2=$(echo $KEY2 | jq .pubkey | tr -d \")
|
||||||
|
assertEquals "wrong username" "$USER2" "$NAME2"
|
||||||
|
assertEquals "address doesn't match" "$ADDR" "$ADDR2"
|
||||||
|
assertEquals "pubkey doesn't match" "$PUBKEY" "$PUBKEY2"
|
||||||
|
|
||||||
|
# and we can find the info
|
||||||
|
assertTrue "${EXE} get $USER2 > /dev/null"
|
||||||
|
}
|
||||||
|
|
||||||
|
# load and run these tests with shunit2!
|
||||||
|
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory
|
||||||
|
. $DIR/shunit2
|
Reference in New Issue
Block a user