mirror of
https://github.com/tendermint/tendermint.git
synced 2026-07-23 16:43:01 +00:00
Merge pull request #19 from tendermint/feature/cli-improvements
cli improvements
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
*.swp
|
||||
*.swo
|
||||
vendor
|
||||
shunit2
|
||||
|
||||
@@ -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,13 +36,16 @@ 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
|
||||
go install ./vendor/github.com/stretchr/testify/require
|
||||
go install ./vendor/golang.org/x/crypto/bcrypt
|
||||
|
||||
codegen:
|
||||
codegen:
|
||||
@echo "--> regenerating all interface wrappers"
|
||||
@gen
|
||||
@echo "Done!"
|
||||
|
||||
+1
-1
@@ -18,4 +18,4 @@ dependencies:
|
||||
test:
|
||||
override:
|
||||
- "go version"
|
||||
- "cd $PROJECT_PATH && make get_vendor_deps && make test"
|
||||
- "cd $PROJECT_PATH && make all"
|
||||
|
||||
@@ -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
@@ -22,23 +22,21 @@ 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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+7
-9
@@ -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 {
|
||||
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
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
+13
-9
@@ -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
|
||||
|
||||
+3
-8
@@ -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")
|
||||
}
|
||||
|
||||
+72
-6
@@ -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 {
|
||||
|
||||
Generated
+5
-10
@@ -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
|
||||
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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Executable
+111
@@ -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