Browse Source

Merge pull request #19 from tendermint/feature/cli-improvements

cli improvements
pull/1782/head
Ethan Frey 8 years ago
committed by GitHub
parent
commit
8bdb5ceda4
35 changed files with 9679 additions and 140 deletions
  1. +1
    -0
      .gitignore
  2. +20
    -5
      Makefile
  3. +1
    -1
      circle.yml
  4. +49
    -0
      cmd/delete.go
  5. +13
    -15
      cmd/get.go
  6. +3
    -0
      cmd/keys/main.go
  7. +7
    -9
      cmd/list.go
  8. +46
    -8
      cmd/new.go
  9. +53
    -0
      cmd/recover.go
  10. +11
    -19
      cmd/root.go
  11. +13
    -9
      cmd/serve.go
  12. +3
    -8
      cmd/update.go
  13. +72
    -6
      cmd/utils.go
  14. +5
    -10
      glide.lock
  15. +0
    -22
      keys/Makefile
  16. +41
    -7
      keys/cryptostore/holder.go
  17. +41
    -3
      keys/cryptostore/holder_test.go
  18. +141
    -0
      keys/ecc.go
  19. +65
    -0
      keys/ecc_test.go
  20. +3
    -2
      keys/server/keys.go
  21. +11
    -8
      keys/server/keys_test.go
  22. +7
    -0
      keys/server/types/keys.go
  23. +4
    -1
      keys/transactions.go
  24. +3
    -2
      keys/tx/multi_test.go
  25. +3
    -2
      keys/tx/one_test.go
  26. +5
    -3
      keys/tx/reader_test.go
  27. +199
    -0
      keys/wordcodec.go
  28. +180
    -0
      keys/wordcodec_test.go
  29. +68
    -0
      keys/wordcodecbench_test.go
  30. +2048
    -0
      keys/wordlist/chinese_simplified.txt
  31. +2048
    -0
      keys/wordlist/english.txt
  32. +2048
    -0
      keys/wordlist/japanese.txt
  33. +2048
    -0
      keys/wordlist/spanish.txt
  34. +308
    -0
      keys/wordlist/wordlist.go
  35. +111
    -0
      tests/keys.sh

+ 1
- 0
.gitignore View File

@ -1,3 +1,4 @@
*.swp *.swp
*.swo *.swo
vendor vendor
shunit2

+ 20
- 5
Makefile View File

@ -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!"

+ 1
- 1
circle.yml View File

@ -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
- 0
cmd/delete.go View 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
}

+ 13
- 15
cmd/get.go View File

@ -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 {
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
},
RunE: runGetCmd,
} }
func init() {
RootCmd.AddCommand(getCmd)
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")
}
name := args[0]
info, err := GetKeyManager().Get(name)
if err == nil {
printInfo(info)
}
return err
} }

+ 3
- 0
cmd/keys/main.go View File

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

+ 7
- 9
cmd/list.go View File

@ -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 {
infos, err := GetKeyManager().List()
if err == nil {
printInfos(infos)
}
return err
},
RunE: runListCmd,
} }
func init() {
RootCmd.AddCommand(listCmd)
func runListCmd(cmd *cobra.Command, args []string) error {
infos, err := GetKeyManager().List()
if err == nil {
printInfos(infos)
}
return err
} }

+ 46
- 8
cmd/new.go View File

@ -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("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 { 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
- 0
cmd/recover.go View 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
}

+ 11
- 19
cmd/root.go View File

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

+ 13
- 9
cmd/serve.go View File

@ -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("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 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


+ 3
- 8
cmd/update.go View File

@ -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: 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 { 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")
} }


+ 72
- 6
cmd/utils.go View File

@ -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
// 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
}
return strings.TrimSpace(pass), nil
}
func getPassword(prompt string) (string, error) {
pass, err := speakeasy.Ask(prompt)
func getPassword(prompt string) (pass string, err error) {
if inputIsTty() {
pass, err = speakeasy.Ask(prompt)
} else {
pass, err = stdinPassword()
}
if err != nil { if err != nil {
return "", err return "", err
} }
if len(pass) < PassLength {
return "", errors.Errorf("Password must be at least %d characters", PassLength)
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 {


+ 5
- 10
glide.lock View File

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


+ 0
- 22
keys/Makefile View File

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

+ 41
- 7
keys/cryptostore/holder.go View File

@ -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,15 +37,42 @@ 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:
//
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) gen, err := getGenerator(algo)
if err != nil { if err != nil {
return keys.Info{}, err
return keys.Info{}, "", err
} }
key := gen.Generate() key := gen.Generate()
err = s.es.Put(name, passphrase, key) 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, 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 return info(name, key), err
} }


+ 41
- 3
keys/cryptostore/holder_test.go View File

@ -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
- 0
keys/ecc.go View 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
- 0
keys/ecc_test.go View 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)
}
}

+ 3
- 2
keys/server/keys.go View File

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


+ 11
- 8
keys/server/keys_test.go View File

@ -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{}
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) { func deleteKey(h http.Handler, name, passphrase string) (*types.ErrorResponse, int, error) {


+ 7
- 0
keys/server/types/keys.go View File

@ -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"`
}

+ 4
- 1
keys/transactions.go View File

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


+ 3
- 2
keys/tx/multi_test.go View File

@ -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 {


+ 3
- 2
keys/tx/one_test.go View File

@ -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 {


+ 5
- 3
keys/tx/reader_test.go View File

@ -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
- 0
keys/wordcodec.go View 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
- 0
keys/wordcodec_test.go View 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
- 0
keys/wordcodecbench_test.go View 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
- 0
keys/wordlist/chinese_simplified.txt
File diff suppressed because it is too large
View File


+ 2048
- 0
keys/wordlist/english.txt
File diff suppressed because it is too large
View File


+ 2048
- 0
keys/wordlist/japanese.txt
File diff suppressed because it is too large
View File


+ 2048
- 0
keys/wordlist/spanish.txt
File diff suppressed because it is too large
View File


+ 308
- 0
keys/wordlist/wordlist.go
File diff suppressed because it is too large
View File


+ 111
- 0
tests/keys.sh View 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

Loading…
Cancel
Save