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
|
||||
*.swo
|
||||
vendor
|
||||
shunit2
|
||||
|
23
Makefile
23
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 = \
|
||||
github.com/Masterminds/glide
|
||||
github.com/Masterminds/glide \
|
||||
github.com/jteeuwen/go-bindata/go-bindata
|
||||
REPO:=github.com/tendermint/go-crypto
|
||||
|
||||
docs:
|
||||
@go get github.com/davecheney/godoc2md
|
||||
godoc2md $(REPO) > README.md
|
||||
|
||||
all: install test
|
||||
all: get_vendor_deps install test
|
||||
|
||||
install:
|
||||
go install ./cmd/keys
|
||||
|
||||
test:
|
||||
test: test_unit test_cli
|
||||
|
||||
test_unit:
|
||||
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
|
||||
@rm -rf vendor/
|
||||
@ -24,6 +36,9 @@ get_vendor_deps: ensure_tools
|
||||
ensure_tools:
|
||||
go get $(GOTOOLS)
|
||||
|
||||
wordlist:
|
||||
go-bindata -ignore ".*\.go" -o keys/wordlist/wordlist.go -pkg "wordlist" keys/wordlist/...
|
||||
|
||||
prepgen: install
|
||||
go install ./vendor/github.com/btcsuite/btcutil/base58
|
||||
go install ./vendor/github.com/stretchr/testify/assert
|
||||
|
@ -18,4 +18,4 @@ dependencies:
|
||||
test:
|
||||
override:
|
||||
- "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
|
||||
}
|
12
cmd/get.go
12
cmd/get.go
@ -22,10 +22,13 @@ import (
|
||||
|
||||
// getCmd represents the get command
|
||||
var getCmd = &cobra.Command{
|
||||
Use: "get <name>",
|
||||
Use: "get [name]",
|
||||
Short: "Get details of one key",
|
||||
Long: `Return public details of one local key.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: runGetCmd,
|
||||
}
|
||||
|
||||
func runGetCmd(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")
|
||||
}
|
||||
@ -36,9 +39,4 @@ var getCmd = &cobra.Command{
|
||||
printInfo(info)
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(getCmd)
|
||||
}
|
||||
|
@ -22,6 +22,9 @@ import (
|
||||
)
|
||||
|
||||
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.Execute()
|
||||
}
|
||||
|
10
cmd/list.go
10
cmd/list.go
@ -22,15 +22,13 @@ var listCmd = &cobra.Command{
|
||||
Short: "List all keys",
|
||||
Long: `Return a list of all public keys stored by this key manager
|
||||
along with their associated name and address.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
RunE: runListCmd,
|
||||
}
|
||||
|
||||
func runListCmd(cmd *cobra.Command, args []string) error {
|
||||
infos, err := GetKeyManager().List()
|
||||
if err == nil {
|
||||
printInfos(infos)
|
||||
}
|
||||
return err
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(listCmd)
|
||||
}
|
||||
|
54
cmd/new.go
54
cmd/new.go
@ -15,42 +15,80 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
flagType = "type"
|
||||
flagNoBackup = "no-backup"
|
||||
)
|
||||
|
||||
// newCmd represents the new command
|
||||
var newCmd = &cobra.Command{
|
||||
Use: "new <name>",
|
||||
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
|
||||
passed as a command line argument for security.`,
|
||||
RunE: newPassword,
|
||||
RunE: runNewCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(newCmd)
|
||||
newCmd.Flags().StringP("type", "t", "ed25519", "Type of key (ed25519|secp256k1)")
|
||||
newCmd.Flags().StringP(flagType, "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 {
|
||||
return errors.New("You must provide a name for the key")
|
||||
}
|
||||
name := args[0]
|
||||
algo := viper.GetString("type")
|
||||
algo := viper.GetString(flagType)
|
||||
|
||||
pass, err := getCheckPassword("Enter a passphrase:", "Repeat the passphrase:")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info, err := GetKeyManager().Create(name, pass, algo)
|
||||
info, seed, err := GetKeyManager().Create(name, pass, algo)
|
||||
if err == nil {
|
||||
printInfo(info)
|
||||
printCreate(info, seed)
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
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"
|
||||
@ -42,17 +36,15 @@ used by light-clients, full nodes, or any other application that
|
||||
needs to sign with a private key.`,
|
||||
}
|
||||
|
||||
// GetKeyManager initializes a key manager based on the configuration
|
||||
func GetKeyManager() keys.Manager {
|
||||
if manager == nil {
|
||||
// store the keys directory
|
||||
rootDir := viper.GetString(cli.HomeFlag)
|
||||
keyDir := filepath.Join(rootDir, KeySubdir)
|
||||
// and construct the key manager
|
||||
manager = cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
filestorage.New(keyDir),
|
||||
)
|
||||
}
|
||||
return manager
|
||||
func init() {
|
||||
RootCmd.AddCommand(getCmd)
|
||||
RootCmd.AddCommand(listCmd)
|
||||
RootCmd.AddCommand(newCmd)
|
||||
RootCmd.AddCommand(updateCmd)
|
||||
RootCmd.AddCommand(deleteCmd)
|
||||
RootCmd.AddCommand(recoverCmd)
|
||||
}
|
||||
|
||||
func RegisterServer() {
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
}
|
||||
|
22
cmd/serve.go
22
cmd/serve.go
@ -28,6 +28,11 @@ import (
|
||||
"github.com/tendermint/go-crypto/keys/server"
|
||||
)
|
||||
|
||||
const (
|
||||
flagPort = "port"
|
||||
flagSocket = "socket"
|
||||
)
|
||||
|
||||
// serveCmd represents the serve command
|
||||
var serveCmd = &cobra.Command{
|
||||
Use: "serve",
|
||||
@ -36,27 +41,26 @@ var serveCmd = &cobra.Command{
|
||||
private keys much more in depth than the cli can perform.
|
||||
In particular, this will allow you to sign transactions with
|
||||
the private keys in the store.`,
|
||||
RunE: serveHTTP,
|
||||
RunE: runServeCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(serveCmd)
|
||||
serveCmd.Flags().IntP("port", "p", 8118, "TCP Port for listen for http server")
|
||||
serveCmd.Flags().StringP("socket", "s", "", "UNIX socket for more secure http server")
|
||||
serveCmd.Flags().StringP("type", "t", "ed25519", "Default key type (ed25519|secp256k1)")
|
||||
serveCmd.Flags().IntP(flagPort, "p", 8118, "TCP Port for listen for http server")
|
||||
serveCmd.Flags().StringP(flagSocket, "s", "", "UNIX socket for more secure http server")
|
||||
serveCmd.Flags().StringP(flagType, "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 err error
|
||||
socket := viper.GetString("socket")
|
||||
socket := viper.GetString(flagSocket)
|
||||
if socket != "" {
|
||||
l, err = createSocket(socket)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot create socket")
|
||||
}
|
||||
} else {
|
||||
port := viper.GetInt("port")
|
||||
port := viper.GetInt(flagPort)
|
||||
l, err = net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
if err != nil {
|
||||
return errors.Errorf("Cannot listen on port %d", port)
|
||||
@ -64,7 +68,7 @@ func serveHTTP(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
router := mux.NewRouter()
|
||||
ks := server.New(GetKeyManager(), viper.GetString("type"))
|
||||
ks := server.New(GetKeyManager(), viper.GetString(flagType))
|
||||
ks.Register(router)
|
||||
|
||||
// only set cors for tcp listener
|
||||
|
@ -24,17 +24,12 @@ import (
|
||||
|
||||
// updateCmd represents the update command
|
||||
var updateCmd = &cobra.Command{
|
||||
Use: "update <name>",
|
||||
Use: "update [name]",
|
||||
Short: "Change the password for a private key",
|
||||
Long: `Change the password for a private key.`,
|
||||
RunE: updatePassword,
|
||||
RunE: runUpdateCmd,
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(updateCmd)
|
||||
}
|
||||
|
||||
func updatePassword(cmd *cobra.Command, args []string) error {
|
||||
func runUpdateCmd(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")
|
||||
}
|
||||
|
78
cmd/utils.go
78
cmd/utils.go
@ -1,30 +1,96 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/bgentry/speakeasy"
|
||||
"github.com/mattn/go-isatty"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/viper"
|
||||
keys "github.com/tendermint/go-crypto/keys"
|
||||
|
||||
data "github.com/tendermint/go-wire/data"
|
||||
"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) {
|
||||
pass, err := speakeasy.Ask(prompt)
|
||||
// GetKeyManager initializes a key manager based on the configuration
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
if len(pass) < PassLength {
|
||||
return "", errors.Errorf("Password must be at least %d characters", PassLength)
|
||||
return strings.TrimSpace(pass), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
// simple read on no-tty
|
||||
if !inputIsTty() {
|
||||
return getPassword(prompt)
|
||||
}
|
||||
|
||||
// TODO: own function???
|
||||
pass, err := getPassword(prompt)
|
||||
if err != nil {
|
||||
|
15
glide.lock
generated
15
glide.lock
generated
@ -1,5 +1,5 @@
|
||||
hash: 3bcee9fbccf29d21217b24b6a83ec51e1514f37b2ae5d8718cf6c5df80f4fb2c
|
||||
updated: 2017-05-15T09:40:53.073691731-04:00
|
||||
updated: 2017-06-19T17:16:58.037568333+02:00
|
||||
imports:
|
||||
- name: github.com/bgentry/speakeasy
|
||||
version: 4aabc24848ce5fd31929f7d1e4ea74d3709c14cd
|
||||
@ -17,8 +17,6 @@ imports:
|
||||
- hdkeychain
|
||||
- name: github.com/btcsuite/fastsha256
|
||||
version: 637e656429416087660c84436a2a035d69d54e2e
|
||||
- name: github.com/clipperhouse/typewriter
|
||||
version: c1a48da378ebb7db1db9f35981b5cc24bf2e5b85
|
||||
- name: github.com/fsnotify/fsnotify
|
||||
version: 4da3e2cfbabc9f751898f250b49f2439785783a1
|
||||
- name: github.com/go-kit/kit
|
||||
@ -60,6 +58,8 @@ imports:
|
||||
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
|
||||
- name: github.com/magiconair/properties
|
||||
version: 51463bfca2576e06c62a8504b5c0f06d61312647
|
||||
- name: github.com/mattn/go-isatty
|
||||
version: 9622e0cc9d8f9be434ca605520ff9a16808fee47
|
||||
- name: github.com/mitchellh/mapstructure
|
||||
version: cc8532a8e9a55ea36402aa21efdf403a60d34096
|
||||
- name: github.com/pelletier/go-buffruneio
|
||||
@ -88,12 +88,12 @@ imports:
|
||||
- edwards25519
|
||||
- extra25519
|
||||
- name: github.com/tendermint/go-wire
|
||||
version: 97beaedf0f4dbc035309157c92be3b30cc6e5d74
|
||||
version: 5f88da3dbc1a72844e6dfaf274ce87f851d488eb
|
||||
subpackages:
|
||||
- data
|
||||
- data/base58
|
||||
- name: github.com/tendermint/tmlibs
|
||||
version: 8f5a175ff4c869fedde710615a11f5745ff69bf3
|
||||
version: bd9d0d1637dadf1330e167189d5e5031aadcda6f
|
||||
subpackages:
|
||||
- cli
|
||||
- common
|
||||
@ -119,11 +119,6 @@ imports:
|
||||
subpackages:
|
||||
- transform
|
||||
- unicode/norm
|
||||
- name: golang.org/x/tools
|
||||
version: 144c6642b5d832d6c44a53dad6ee61665dd432ce
|
||||
subpackages:
|
||||
- go/ast/astutil
|
||||
- imports
|
||||
- name: gopkg.in/go-playground/validator.v9
|
||||
version: 6d8c18553ea1ac493d049edd6f102f52e618f085
|
||||
- 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
|
||||
|
||||
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
|
||||
// a full-featured key manager
|
||||
type Manager struct {
|
||||
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{
|
||||
es: encryptedStorage{
|
||||
coder: coder,
|
||||
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
|
||||
// another key already stored under this name
|
||||
//
|
||||
// algo must be a supported go-crypto algorithm:
|
||||
//
|
||||
func (s Manager) Create(name, passphrase, algo string) (keys.Info, error) {
|
||||
// algo must be a supported go-crypto algorithm: ed25519, secp256k1
|
||||
func (s Manager) Create(name, passphrase, algo string) (keys.Info, string, error) {
|
||||
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 {
|
||||
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)
|
||||
return info(name, key), err
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
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/storage/memstorage"
|
||||
)
|
||||
@ -18,6 +19,7 @@ func TestKeyManagement(t *testing.T) {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
|
||||
algo := crypto.NameEd25519
|
||||
@ -32,10 +34,10 @@ func TestKeyManagement(t *testing.T) {
|
||||
// create some keys
|
||||
_, err = cstore.Get(n1)
|
||||
assert.NotNil(err)
|
||||
i, err := cstore.Create(n1, p1, algo)
|
||||
i, _, err := cstore.Create(n1, p1, algo)
|
||||
require.Equal(n1, i.Name)
|
||||
require.Nil(err)
|
||||
_, err = cstore.Create(n2, p2, algo)
|
||||
_, _, err = cstore.Create(n2, p2, algo)
|
||||
require.Nil(err)
|
||||
|
||||
// we can get these keys
|
||||
@ -154,6 +156,7 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
|
||||
algo := crypto.NameSecp256k1
|
||||
@ -161,7 +164,7 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
||||
p1, p2, p3, pt := "1234", "foobar", "ding booms!", "really-secure!@#$"
|
||||
|
||||
// make sure key works with initial password
|
||||
_, err := cstore.Create(n1, p1, algo)
|
||||
_, _, err := cstore.Create(n1, p1, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
assertPassword(assert, cstore, n1, p1, p2)
|
||||
|
||||
@ -199,6 +202,41 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
||||
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() {
|
||||
// // Select the encryption and storage for your cryptostore
|
||||
// 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
|
||||
}
|
||||
|
||||
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 {
|
||||
writeError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccess(w, &key)
|
||||
res := types.CreateKeyResponse{key, seed}
|
||||
writeSuccess(w, &res)
|
||||
}
|
||||
|
||||
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)
|
||||
require.Nil(err, "%+v", err)
|
||||
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
|
||||
key2, code, err := createKey(r, n2, p2, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
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....
|
||||
keys, code, err = listKeys(r)
|
||||
@ -62,9 +64,9 @@ func TestKeyServer(t *testing.T) {
|
||||
k, code, err := getKey(r, n1)
|
||||
require.Nil(err, "%+v", err)
|
||||
require.Equal(http.StatusOK, code)
|
||||
assert.Equal(k.Name, n1)
|
||||
assert.Equal(n1, k.Name)
|
||||
assert.NotNil(k.Address)
|
||||
assert.Equal(k.Address, key.Address)
|
||||
assert.Equal(key.Key.Address, k.Address)
|
||||
|
||||
// delete with proper key
|
||||
_, code, err = deleteKey(r, n1, p1)
|
||||
@ -89,6 +91,7 @@ func setupServer() http.Handler {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
|
||||
// build your http server
|
||||
@ -134,7 +137,7 @@ func getKey(h http.Handler, name string) (*keys.Info, int, error) {
|
||||
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()
|
||||
post := types.CreateKeyRequest{
|
||||
Name: name,
|
||||
@ -157,9 +160,9 @@ func createKey(h http.Handler, name, passphrase, algo string) (*keys.Info, int,
|
||||
return nil, rr.Code, nil
|
||||
}
|
||||
|
||||
data := keys.Info{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &data)
|
||||
return &data, rr.Code, err
|
||||
data := new(types.CreateKeyResponse)
|
||||
err = json.Unmarshal(rr.Body.Bytes(), data)
|
||||
return data, rr.Code, err
|
||||
}
|
||||
|
||||
func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package types
|
||||
|
||||
import "github.com/tendermint/go-crypto/keys"
|
||||
|
||||
// CreateKeyRequest is sent to create a new key
|
||||
type CreateKeyRequest struct {
|
||||
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
|
||||
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
|
||||
type Manager interface {
|
||||
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)
|
||||
Get(name string) (Info, error)
|
||||
Update(name, oldpass, newpass string) error
|
||||
|
@ -18,13 +18,14 @@ func TestMultiSig(t *testing.T) {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
n, p := "foo", "bar"
|
||||
n2, p2 := "other", "thing"
|
||||
|
||||
acct, err := cstore.Create(n, p, algo)
|
||||
acct, _, err := cstore.Create(n, p, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
acct2, err := cstore.Create(n2, p2, algo)
|
||||
acct2, _, err := cstore.Create(n2, p2, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
|
||||
type signer struct {
|
||||
|
@ -18,13 +18,14 @@ func TestOneSig(t *testing.T) {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
n, p := "foo", "bar"
|
||||
n2, p2 := "other", "thing"
|
||||
|
||||
acct, err := cstore.Create(n, p, algo)
|
||||
acct, _, err := cstore.Create(n, p, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
acct2, err := cstore.Create(n2, p2, algo)
|
||||
acct2, _, err := cstore.Create(n2, p2, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
|
||||
cases := []struct {
|
||||
|
@ -6,9 +6,10 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
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/storage/memstorage"
|
||||
data "github.com/tendermint/go-wire/data"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
@ -18,14 +19,15 @@ func TestReader(t *testing.T) {
|
||||
cstore := cryptostore.New(
|
||||
cryptostore.SecretBox,
|
||||
memstorage.New(),
|
||||
keys.MustLoadCodec("english"),
|
||||
)
|
||||
type sigs struct{ name, pass string }
|
||||
u := sigs{"alice", "1234"}
|
||||
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)
|
||||
_, err = cstore.Create(u2.name, u2.pass, algo)
|
||||
_, _, err = cstore.Create(u2.name, u2.pass, algo)
|
||||
require.Nil(err, "%+v", err)
|
||||
|
||||
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