cli improvementspull/1782/head
@ -1,3 +1,4 @@ | |||||
*.swp | *.swp | ||||
*.swo | *.swo | ||||
vendor | vendor | ||||
shunit2 |
@ -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 | |||||
} |
@ -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 | |||||
} |
@ -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) | |||||
@ -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) | |||||
} | |||||
} |
@ -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) | |||||
} | |||||
} | |||||
} |
@ -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 |