@ -1,3 +1,4 @@ | |||
*.swp | |||
*.swo | |||
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 |
@ -1,3 +1,3 @@ | |||
package crypto | |||
const Version = "0.2.0" | |||
const Version = "0.2.1" |