keys package: fundraiser compatibility and HD keys (BIP 39 & BIP 32 / BIP 44) (#118)

- fundraiser compatibility for HD keys (BIP 39 & BIP 32 / BIP 44)
This commit is contained in:
Ismail Khoffi 2018-06-20 13:30:22 -07:00 committed by GitHub
parent fed8807a32
commit 4634063698
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 600 additions and 10096 deletions

12
Gopkg.lock generated
View File

@ -1,6 +1,12 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/bartekn/go-bip39"
packages = ["."]
revision = "a05967ea095d81c8fe4833776774cfaff8e5036c"
[[projects]]
branch = "master"
name = "github.com/brejski/hid"
@ -63,12 +69,6 @@
packages = ["."]
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
[[projects]]
branch = "master"
name = "github.com/howeyc/crc16"
packages = ["."]
revision = "2b2a61e366a66d3efb279e46176e7291001e0354"
[[projects]]
branch = "master"
name = "github.com/jmhodges/levigo"

View File

@ -28,10 +28,6 @@
name = "github.com/btcsuite/btcutil"
branch = "master"
[[constraint]]
name = "github.com/howeyc/crc16"
branch = "master"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"

View File

@ -15,7 +15,7 @@ check: check_tools
# Command to generate the workd list (kept here for documentation purposes only):
wordlist:
# To re-generate wordlist.go run:
# go-bindata -ignore ".*\.go" -o keys/words/wordlist/wordlist.go -pkg "wordlist" keys/words/wordlist/...
# go-bindata -ignore ".*\.go" -o keys/words/bip39/wordlist.go -pkg "wordlist" keys/bip39/wordlist/...
build: wordlist
# Nothing else to build!

View File

@ -15,6 +15,7 @@ func init() {
RegisterAmino(cdc)
}
// RegisterAmino registers all go-crypto related types in the given (amino) codec.
func RegisterAmino(cdc *amino.Codec) {
cdc.RegisterInterface((*PubKey)(nil), nil)
cdc.RegisterConcrete(PubKeyEd25519{},

View File

@ -2,9 +2,9 @@ package crypto
import (
"bytes"
"fmt"
"io/ioutil"
. "github.com/tendermint/tmlibs/common"
"golang.org/x/crypto/openpgp/armor"
)
@ -12,15 +12,15 @@ func EncodeArmor(blockType string, headers map[string]string, data []byte) strin
buf := new(bytes.Buffer)
w, err := armor.Encode(buf, blockType, headers)
if err != nil {
PanicSanity("Error encoding ascii armor: " + err.Error())
panic(fmt.Errorf("could not encode ascii armor: %s", err))
}
_, err = w.Write(data)
if err != nil {
PanicSanity("Error encoding ascii armor: " + err.Error())
panic(fmt.Errorf("could not encode ascii armor: %s", err))
}
err = w.Close()
if err != nil {
PanicSanity("Error encoding ascii armor: " + err.Error())
panic(fmt.Errorf("could not encode ascii armor: %s", err))
}
return buf.String()
}

View File

@ -88,7 +88,7 @@ type hashed struct {
// to compare the returned hashed password with its cleartext version.
func GenerateFromPassword(salt []byte, password []byte, cost int) ([]byte, error) {
if len(salt) != maxSaltSize {
return nil, fmt.Errorf("Salt len must be %v", maxSaltSize)
return nil, fmt.Errorf("salt len must be %v", maxSaltSize)
}
p, err := newFromPassword(salt, password, cost)
if err != nil {

62
keys/bip39/wordcodec.go Normal file
View File

@ -0,0 +1,62 @@
package bip39
import (
"strings"
"github.com/bartekn/go-bip39"
)
// ValidSentenceLen defines the mnemonic sentence lengths supported by this BIP 39 library.
type ValidSentenceLen uint8
const (
// FundRaiser is the sentence length used during the cosmos fundraiser (12 words).
FundRaiser ValidSentenceLen = 12
// FreshKey is the sentence length used for newly created keys (24 words).
FreshKey ValidSentenceLen = 24
)
// NewMnemonic will return a string consisting of the mnemonic words for
// the given sentence length.
func NewMnemonic(len ValidSentenceLen) (words []string, err error) {
// len = (ENT + checksum) / 11
var ENT int
switch len {
case FundRaiser:
ENT = 128
case FreshKey:
ENT = 256
}
var entropy []byte
entropy, err = bip39.NewEntropy(ENT)
if err != nil {
return
}
var mnemonic string
mnemonic, err = bip39.NewMnemonic(entropy)
if err != nil {
return
}
words = strings.Split(mnemonic, " ")
return
}
// MnemonicToSeed creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password).
// This method does not validate the mnemonics checksum.
func MnemonicToSeed(mne string) (seed []byte) {
// we do not checksum here...
seed = bip39.NewSeed(mne, "")
return
}
// MnemonicToSeedWithErrChecking returns the same seed as MnemonicToSeed.
// It creates a BIP 39 seed from the passed mnemonic (with an empty BIP 39 password).
//
// Different from MnemonicToSeed it validates the checksum.
// For details on the checksum see the BIP 39 spec.
func MnemonicToSeedWithErrChecking(mne string) (seed []byte, err error) {
seed, err = bip39.NewSeedWithErrorChecking(mne, "")
return
}

View File

@ -0,0 +1,15 @@
package bip39
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestWordCodec_NewMnemonic(t *testing.T) {
_, err := NewMnemonic(FundRaiser)
assert.NoError(t, err, "unexpected error generating fundraiser mnemonic")
_, err = NewMnemonic(FreshKey)
assert.NoError(t, err, "unexpected error generating new 24-word mnemonic")
}

View File

@ -1,352 +0,0 @@
package hd
// XXX This package doesn't work with our address scheme,
// XXX and it probably doesn't work for our other pubkey types.
// XXX Fix it up to be more general but compatible.
import (
"crypto/ecdsa"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"hash"
"math/big"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/base58"
"golang.org/x/crypto/ripemd160"
)
/*
This file implements BIP32 HD wallets.
Note it only works for SECP256k1 keys.
It also includes some Bitcoin specific utility functions.
*/
// ComputeBTCAddress returns the BTC address using the pubKeyHex and chainCodeHex
// for the given path and index.
func ComputeBTCAddress(pubKeyHex string, chainCodeHex string, path string, index int32) string {
pubKeyBytes := DerivePublicKeyForPath(
HexDecode(pubKeyHex),
HexDecode(chainCodeHex),
fmt.Sprintf("%v/%v", path, index),
)
return BTCAddrFromPubKeyBytes(pubKeyBytes)
}
// ComputePrivateKey returns the private key using the master mprivHex and chainCodeHex
// for the given path and index.
func ComputePrivateKey(mprivHex string, chainHex string, path string, index int32) string {
privKeyBytes := DerivePrivateKeyForPath(
HexDecode(mprivHex),
HexDecode(chainHex),
fmt.Sprintf("%v/%v", path, index),
)
return HexEncode(privKeyBytes)
}
// ComputeBTCAddressForPrivKey returns the Bitcoin address for the given privKey.
func ComputeBTCAddressForPrivKey(privKey string) string {
pubKeyBytes := PubKeyBytesFromPrivKeyBytes(HexDecode(privKey), true)
return BTCAddrFromPubKeyBytes(pubKeyBytes)
}
// SignBTCMessage signs a "Bitcoin Signed Message".
func SignBTCMessage(privKey string, message string, compress bool) string {
prefixBytes := []byte("Bitcoin Signed Message:\n")
messageBytes := []byte(message)
bytes := []byte{}
bytes = append(bytes, byte(len(prefixBytes)))
bytes = append(bytes, prefixBytes...)
bytes = append(bytes, byte(len(messageBytes)))
bytes = append(bytes, messageBytes...)
privKeyBytes := HexDecode(privKey)
x, y := btcec.S256().ScalarBaseMult(privKeyBytes)
ecdsaPubKey := ecdsa.PublicKey{
Curve: btcec.S256(),
X: x,
Y: y,
}
ecdsaPrivKey := &btcec.PrivateKey{
PublicKey: ecdsaPubKey,
D: new(big.Int).SetBytes(privKeyBytes),
}
sigbytes, err := btcec.SignCompact(btcec.S256(), ecdsaPrivKey, CalcHash256(bytes), compress)
if err != nil {
panic(err)
}
return base64.StdEncoding.EncodeToString(sigbytes)
}
// ComputeMastersFromSeed returns the master public key, master secret, and chain code in hex.
func ComputeMastersFromSeed(seed string) (string, string, string) {
key, data := []byte("Bitcoin seed"), []byte(seed)
secret, chain := I64(key, data)
pubKeyBytes := PubKeyBytesFromPrivKeyBytes(secret, true)
return HexEncode(pubKeyBytes), HexEncode(secret), HexEncode(chain)
}
// ComputeWIF returns the privKey in Wallet Import Format.
func ComputeWIF(privKey string, compress bool) string {
return WIFFromPrivKeyBytes(HexDecode(privKey), compress)
}
// ComputeBTCTxId returns the bitcoin transaction ID.
func ComputeBTCTxId(rawTxHex string) string {
return HexEncode(ReverseBytes(CalcHash256(HexDecode(rawTxHex))))
}
/*
func printKeyInfo(privKeyBytes []byte, pubKeyBytes []byte, chain []byte) {
if pubKeyBytes == nil {
pubKeyBytes = PubKeyBytesFromPrivKeyBytes(privKeyBytes, true)
}
addr := AddrFromPubKeyBytes(pubKeyBytes)
log.Println("\nprikey:\t%v\npubKeyBytes:\t%v\naddr:\t%v\nchain:\t%v",
HexEncode(privKeyBytes),
HexEncode(pubKeyBytes),
addr,
HexEncode(chain))
}
*/
//-------------------------------------------------------------------
// DerivePrivateKeyForPath derives the private key by following the path from privKeyBytes,
// using the given chainCode.
func DerivePrivateKeyForPath(privKeyBytes []byte, chainCode []byte, path string) []byte {
data := privKeyBytes
parts := strings.Split(path, "/")
for _, part := range parts {
prime := part[len(part)-1:] == "'"
// prime == private derivation. Otherwise public.
if prime {
part = part[:len(part)-1]
}
i, err := strconv.Atoi(part)
if err != nil {
panic(err)
}
if i < 0 {
panic(errors.New("index too large."))
}
data, chainCode = DerivePrivateKey(data, chainCode, uint32(i), prime)
//printKeyInfo(data, nil, chain)
}
return data
}
// DerivePublicKeyForPath derives the public key by following the path from pubKeyBytes
// using the given chainCode.
func DerivePublicKeyForPath(pubKeyBytes []byte, chainCode []byte, path string) []byte {
data := pubKeyBytes
parts := strings.Split(path, "/")
for _, part := range parts {
prime := part[len(part)-1:] == "'"
if prime {
panic(errors.New("cannot do a prime derivation from public key"))
}
i, err := strconv.Atoi(part)
if err != nil {
panic(err)
}
if i < 0 {
panic(errors.New("index too large."))
}
data, chainCode = DerivePublicKey(data, chainCode, uint32(i))
//printKeyInfo(nil, data, chainCode)
}
return data
}
// DerivePrivateKey derives the private key with index and chainCode.
// If prime is true, the derivation is 'hardened'.
// It returns the new private key and new chain code.
func DerivePrivateKey(privKeyBytes []byte, chainCode []byte, index uint32, prime bool) ([]byte, []byte) {
var data []byte
if prime {
index = index | 0x80000000
data = append([]byte{byte(0)}, privKeyBytes...)
} else {
public := PubKeyBytesFromPrivKeyBytes(privKeyBytes, true)
data = public
}
data = append(data, uint32ToBytes(index)...)
data2, chainCode2 := I64(chainCode, data)
x := addScalars(privKeyBytes, data2)
return x, chainCode2
}
// DerivePublicKey derives the public key with index and chainCode.
// It returns the new public key and new chain code.
func DerivePublicKey(pubKeyBytes []byte, chainCode []byte, index uint32) ([]byte, []byte) {
data := []byte{}
data = append(data, pubKeyBytes...)
data = append(data, uint32ToBytes(index)...)
data2, chainCode2 := I64(chainCode, data)
data2p := PubKeyBytesFromPrivKeyBytes(data2, true)
return addPoints(pubKeyBytes, data2p), chainCode2
}
// eliptic curve pubkey addition
func addPoints(a []byte, b []byte) []byte {
ap, err := btcec.ParsePubKey(a, btcec.S256())
if err != nil {
panic(err)
}
bp, err := btcec.ParsePubKey(b, btcec.S256())
if err != nil {
panic(err)
}
sumX, sumY := btcec.S256().Add(ap.X, ap.Y, bp.X, bp.Y)
sum := &btcec.PublicKey{
Curve: btcec.S256(),
X: sumX,
Y: sumY,
}
return sum.SerializeCompressed()
}
// modular big endian addition
func addScalars(a []byte, b []byte) []byte {
aInt := new(big.Int).SetBytes(a)
bInt := new(big.Int).SetBytes(b)
sInt := new(big.Int).Add(aInt, bInt)
x := sInt.Mod(sInt, btcec.S256().N).Bytes()
x2 := [32]byte{}
copy(x2[32-len(x):], x)
return x2[:]
}
func uint32ToBytes(i uint32) []byte {
b := [4]byte{}
binary.BigEndian.PutUint32(b[:], i)
return b[:]
}
//-------------------------------------------------------------------
// HexEncode encodes b in hex.
func HexEncode(b []byte) string {
return hex.EncodeToString(b)
}
// HexDecode hex decodes the str. If str is not valid hex
// it will return an empty byte slice.
func HexDecode(str string) []byte {
b, _ := hex.DecodeString(str)
return b
}
// I64 returns the two halfs of the SHA512 HMAC of key and data.
func I64(key []byte, data []byte) ([]byte, []byte) {
mac := hmac.New(sha512.New, key)
mac.Write(data)
I := mac.Sum(nil)
return I[:32], I[32:]
}
//-------------------------------------------------------------------
const (
btcPrefixPubKeyHash = byte(0x00)
btcPrefixPrivKey = byte(0x80)
)
// BTCAddrFromPubKeyBytes returns a B58 encoded Bitcoin mainnet address.
func BTCAddrFromPubKeyBytes(pubKeyBytes []byte) string {
versionPrefix := btcPrefixPubKeyHash // TODO Make const or configurable
h160 := CalcHash160(pubKeyBytes)
h160 = append([]byte{versionPrefix}, h160...)
checksum := CalcHash256(h160)
b := append(h160, checksum[:4]...)
return base58.Encode(b)
}
// BTCAddrBytesFromPubKeyBytes returns a hex Bitcoin mainnet address and its checksum.
func BTCAddrBytesFromPubKeyBytes(pubKeyBytes []byte) (addrBytes []byte, checksum []byte) {
versionPrefix := btcPrefixPubKeyHash // TODO Make const or configurable
h160 := CalcHash160(pubKeyBytes)
_h160 := append([]byte{versionPrefix}, h160...)
checksum = CalcHash256(_h160)[:4]
return h160, checksum
}
// WIFFromPrivKeyBytes returns the privKeyBytes in Wallet Import Format.
func WIFFromPrivKeyBytes(privKeyBytes []byte, compress bool) string {
versionPrefix := btcPrefixPrivKey // TODO Make const or configurable
bytes := append([]byte{versionPrefix}, privKeyBytes...)
if compress {
bytes = append(bytes, byte(1))
}
checksum := CalcHash256(bytes)
bytes = append(bytes, checksum[:4]...)
return base58.Encode(bytes)
}
// PubKeyBytesFromPrivKeyBytes returns the optionally compressed public key bytes.
func PubKeyBytesFromPrivKeyBytes(privKeyBytes []byte, compress bool) (pubKeyBytes []byte) {
x, y := btcec.S256().ScalarBaseMult(privKeyBytes)
pub := &btcec.PublicKey{
Curve: btcec.S256(),
X: x,
Y: y,
}
if compress {
return pub.SerializeCompressed()
}
return pub.SerializeUncompressed()
}
//--------------------------------------------------------------
// CalcHash returns the hash of data using hasher.
func CalcHash(data []byte, hasher hash.Hash) []byte {
hasher.Write(data)
return hasher.Sum(nil)
}
// CalcHash160 returns the ripemd160(sha256(data)).
func CalcHash160(data []byte) []byte {
return CalcHash(CalcHash(data, sha256.New()), ripemd160.New())
}
// CalcHash256 returns the sha256(sha256(data)).
func CalcHash256(data []byte) []byte {
return CalcHash(CalcHash(data, sha256.New()), sha256.New())
}
// CalcSha512 returns the sha512(data).
func CalcSha512(data []byte) []byte {
return CalcHash(data, sha512.New())
}
// ReverseBytes returns the buf in the opposite order
func ReverseBytes(buf []byte) []byte {
var res []byte
if len(buf) == 0 {
return res
}
// Walk till mid-way, swapping bytes from each end:
// b[i] and b[len-i-1]
blen := len(buf)
res = make([]byte, blen)
mid := blen / 2
for left := 0; left <= mid; left++ {
right := blen - left - 1
res[left] = buf[right]
res[right] = buf[left]
}
return res
}

View File

@ -1,37 +0,0 @@
package hd
/*
import (
"encoding/hex"
"fmt"
"testing"
)
func TestManual(t *testing.T) {
bytes, _ := hex.DecodeString("dfac699f1618c9be4df2befe94dc5f313946ebafa386756bd4926a1ecfd7cf2438426ede521d1ee6512391bc200b7910bcbea593e68d52b874c29bdc5a308ed1")
fmt.Println(bytes)
puk, prk, ch, se := ComputeMastersFromSeed(string(bytes))
fmt.Println(puk, ch, se)
pubBytes2 := DerivePublicKeyForPath(
HexDecode(puk),
HexDecode(ch),
//"44'/118'/0'/0/0",
"0/0",
)
fmt.Printf("PUB2 %X\n", pubBytes2)
privBytes := DerivePrivateKeyForPath(
HexDecode(prk),
HexDecode(ch),
//"44'/118'/0'/0/0",
//"0/0",
"44'/118'/0'/0/0",
)
fmt.Printf("PRIV %X\n", privBytes)
pubBytes := PubKeyBytesFromPrivKeyBytes(privBytes, true)
fmt.Printf("PUB %X\n", pubBytes)
}
*/

View File

@ -0,0 +1,83 @@
package hd
import (
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tyler-smith/go-bip39"
"github.com/tendermint/go-crypto"
)
type addrData struct {
Mnemonic string
Master string
Seed string
Priv string
Pub string
Addr string
}
func initFundraiserTestVectors(t *testing.T) []addrData {
// NOTE: atom fundraiser address
// var hdPath string = "m/44'/118'/0'/0/0"
var hdToAddrTable []addrData
b, err := ioutil.ReadFile("test.json")
if err != nil {
t.Fatalf("could not read fundraiser test vector file (test.json): %s", err)
}
err = json.Unmarshal(b, &hdToAddrTable)
if err != nil {
t.Fatalf("could not decode test vectors (test.json): %s", err)
}
return hdToAddrTable
}
func TestFundraiserCompatibility(t *testing.T) {
hdToAddrTable := initFundraiserTestVectors(t)
for i, d := range hdToAddrTable {
privB, _ := hex.DecodeString(d.Priv)
pubB, _ := hex.DecodeString(d.Pub)
addrB, _ := hex.DecodeString(d.Addr)
seedB, _ := hex.DecodeString(d.Seed)
masterB, _ := hex.DecodeString(d.Master)
seed := bip39.NewSeed(d.Mnemonic, "")
t.Log("================================")
t.Logf("ROUND: %d MNEMONIC: %s", i, d.Mnemonic)
master, ch := ComputeMastersFromSeed(seed)
priv, err := DerivePrivateKeyForPath(master, ch, "44'/118'/0'/0/0")
assert.NoError(t, err)
pub := crypto.PrivKeySecp256k1(priv).PubKey()
t.Log("\tNODEJS GOLANG\n")
t.Logf("SEED \t%X %X\n", seedB, seed)
t.Logf("MSTR \t%X %X\n", masterB, master)
t.Logf("PRIV \t%X %X\n", privB, priv)
t.Logf("PUB \t%X %X\n", pubB, pub)
assert.Equal(t, seedB, seed)
assert.Equal(t, master[:], masterB, fmt.Sprintf("Expected masters to match for %d", i))
assert.Equal(t, priv[:], privB, "Expected priv keys to match")
var pubBFixed [33]byte
copy(pubBFixed[:], pubB)
assert.Equal(t, pub, crypto.PubKeySecp256k1(pubBFixed), fmt.Sprintf("Expected pub keys to match for %d", i))
addr := pub.Address()
t.Logf("ADDR \t%X %X\n", addrB, addr)
assert.Equal(t, addr, crypto.Address(addrB), fmt.Sprintf("Expected addresses to match %d", i))
}
}

View File

@ -1,238 +0,0 @@
package hd
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/tyler-smith/go-bip39"
"github.com/tendermint/go-crypto"
)
type addrData struct {
Mnemonic string
Master string
Seed string
Priv string
Pub string
Addr string
}
// NOTE: atom fundraiser address
// var hdPath string = "m/44'/118'/0'/0/0"
var hdToAddrTable []addrData
func init() {
b, err := ioutil.ReadFile("test.json")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
err = json.Unmarshal(b, &hdToAddrTable)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func TestHDToAddr(t *testing.T) {
for i, d := range hdToAddrTable {
privB, _ := hex.DecodeString(d.Priv)
pubB, _ := hex.DecodeString(d.Pub)
addrB, _ := hex.DecodeString(d.Addr)
seedB, _ := hex.DecodeString(d.Seed)
masterB, _ := hex.DecodeString(d.Master)
seed := bip39.NewSeed(d.Mnemonic, "")
fmt.Println("================================")
fmt.Println("ROUND:", i, "MNEMONIC:", d.Mnemonic)
// master, priv, pub := tylerSmith(seed)
// master, priv, pub := btcsuite(seed)
master, priv, pub := gocrypto(seed)
fmt.Printf("\tNODEJS GOLANG\n")
fmt.Printf("SEED \t%X %X\n", seedB, seed)
fmt.Printf("MSTR \t%X %X\n", masterB, master)
fmt.Printf("PRIV \t%X %X\n", privB, priv)
fmt.Printf("PUB \t%X %X\n", pubB, pub)
_, _ = priv, privB
assert.Equal(t, master, masterB, fmt.Sprintf("Expected masters to match for %d", i))
assert.Equal(t, priv, privB, "Expected priv keys to match")
assert.Equal(t, pub, pubB, fmt.Sprintf("Expected pub keys to match for %d", i))
var pubT crypto.PubKeySecp256k1
copy(pubT[:], pub)
addr := pubT.Address()
fmt.Printf("ADDR \t%X %X\n", addrB, addr)
assert.Equal(t, addr, crypto.Address(addrB), fmt.Sprintf("Expected addresses to match %d", i))
}
}
func TestReverseBytes(t *testing.T) {
tests := [...]struct {
v []byte
want []byte
}{
{[]byte(""), []byte("")},
{nil, nil},
{[]byte("Tendermint"), []byte("tnimredneT")},
{[]byte("T"), []byte("T")},
{[]byte("Te"), []byte("eT")},
}
for i, tt := range tests {
got := ReverseBytes(tt.v)
if !bytes.Equal(got, tt.want) {
t.Errorf("#%d:\ngot= (%x)\nwant=(%x)", i, got, tt.want)
}
}
}
/*
func ifExit(err error, n int) {
if err != nil {
fmt.Println(n, err)
os.Exit(1)
}
}
*/
func gocrypto(seed []byte) ([]byte, []byte, []byte) {
_, priv, ch := ComputeMastersFromSeed(string(seed))
privBytes := DerivePrivateKeyForPath(
HexDecode(priv),
HexDecode(ch),
"44'/118'/0'/0/0",
)
pubBytes := PubKeyBytesFromPrivKeyBytes(privBytes, true)
return HexDecode(priv), privBytes, pubBytes
}
/*
func btcsuite(seed []byte) ([]byte, []byte, []byte) {
fmt.Println("HD")
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
if err != nil {
hmac := hmac.New(sha512.New, []byte("Bitcoin seed"))
hmac.Write([]byte(seed))
intermediary := hmac.Sum(nil)
curve := btcutil.Secp256k1()
curveParams := curve.Params()
// Split it into our key and chain code
keyBytes := intermediary[:32]
fmt.Printf("\t%X\n", keyBytes)
fmt.Printf("\t%X\n", curveParams.N.Bytes())
keyInt, _ := binary.ReadVarint(bytes.NewBuffer(keyBytes))
fmt.Printf("\t%d\n", keyInt)
}
fh := hdkeychain.HardenedKeyStart
k, err := masterKey.Child(uint32(fh + 44))
ifExit(err, 44)
k, err = k.Child(uint32(fh + 118))
ifExit(err, 118)
k, err = k.Child(uint32(fh + 0))
ifExit(err, 1)
k, err = k.Child(uint32(0))
ifExit(err, 2)
k, err = k.Child(uint32(0))
ifExit(err, 3)
ecpriv, err := k.ECPrivKey()
ifExit(err, 10)
ecpub, err := k.ECPubKey()
ifExit(err, 11)
priv := ecpriv.Serialize()
pub := ecpub.SerializeCompressed()
mkey, _ := masterKey.ECPrivKey()
return mkey.Serialize(), priv, pub
}
// return priv and pub
func tylerSmith(seed []byte) ([]byte, []byte, []byte) {
masterKey, err := bip32.NewMasterKey(seed)
if err != nil {
hmac := hmac.New(sha512.New, []byte("Bitcoin seed"))
hmac.Write([]byte(seed))
intermediary := hmac.Sum(nil)
curve := btcutil.Secp256k1()
curveParams := curve.Params()
// Split it into our key and chain code
keyBytes := intermediary[:32]
fmt.Printf("\t%X\n", keyBytes)
fmt.Printf("\t%X\n", curveParams.N.Bytes())
keyInt, _ := binary.ReadVarint(bytes.NewBuffer(keyBytes))
fmt.Printf("\t%d\n", keyInt)
}
ifExit(err, 0)
fh := bip32.FirstHardenedChild
k, err := masterKey.NewChildKey(fh + 44)
ifExit(err, 44)
k, err = k.NewChildKey(fh + 118)
ifExit(err, 118)
k, err = k.NewChildKey(fh + 0)
ifExit(err, 1)
k, err = k.NewChildKey(0)
ifExit(err, 2)
k, err = k.NewChildKey(0)
ifExit(err, 3)
priv := k.Key
pub := k.PublicKey().Key
return masterKey.Key, priv, pub
}
*/
// Benchmarks
var revBytesCases = [][]byte{
nil,
[]byte(""),
[]byte("12"),
// 16byte case
[]byte("abcdefghijklmnop"),
// 32byte case
[]byte("abcdefghijklmnopqrstuvwxyz123456"),
// 64byte case
[]byte("abcdefghijklmnopqrstuvwxyz123456abcdefghijklmnopqrstuvwxyz123456"),
}
func BenchmarkReverseBytes(b *testing.B) {
var sink []byte
for i := 0; i < b.N; i++ {
for _, tt := range revBytesCases {
sink = ReverseBytes(tt)
}
}
b.ReportAllocs()
// sink is necessary to ensure if the compiler tries
// to smart, that it won't optimize away the benchmarks.
if sink != nil {
_ = sink
}
}

168
keys/hd/hdpath.go Normal file
View File

@ -0,0 +1,168 @@
// Package hd provides basic functionality Hierarchical Deterministic Wallets.
//
// The user must understand the overall concept of the BIP 32 and the BIP 44 specs:
// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
//
// In combination with the bip39 package in go-crypto this package provides the functionality for deriving keys using a
// BIP 44 HD path, or, more general, by passing a BIP 32 path.
//
// In particular, this package (together with bip39) provides all necessary functionality to derive keys from
// mnemonics generated during the cosmos fundraiser.
package hd
import (
"crypto/hmac"
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"math/big"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/tendermint/go-crypto"
)
// BIP44Prefix is the parts of the BIP32 HD path that are fixed by what we used during the fundraiser.
const (
BIP44Prefix = "44'/118'/"
FullFundraiserPath = BIP44Prefix + "0'/0/0"
)
// BIP44Params wraps BIP 44 params (5 level BIP 32 path).
// To receive a canonical string representation ala
// m / purpose' / coin_type' / account' / change / address_index
// call String() on a BIP44Params instance.
type BIP44Params struct {
purpose uint32
coinType uint32
account uint32
change bool
addressIdx uint32
}
// NewParams creates a BIP 44 parameter object from the params:
// m / purpose' / coin_type' / account' / change / address_index
func NewParams(purpose, coinType, account uint32, change bool, addressIdx uint32) *BIP44Params {
return &BIP44Params{
purpose: purpose,
coinType: coinType,
account: account,
change: change,
addressIdx: addressIdx,
}
}
// NewFundraiserParams creates a BIP 44 parameter object from the params:
// m / 44' / 118' / account' / 0 / address_index
// The fixed parameters (purpose', coin_type', and change) are determined by what was used in the fundraiser.
func NewFundraiserParams(account uint32, addressIdx uint32) *BIP44Params {
return NewParams(44, 118, account, false, addressIdx)
}
func (p BIP44Params) String() string {
var changeStr string
if p.change {
changeStr = "1"
} else {
changeStr = "0"
}
// m / purpose' / coin_type' / account' / change / address_index
return fmt.Sprintf("%d'/%d'/%d'/%s/%d",
p.purpose,
p.coinType,
p.account,
changeStr,
p.addressIdx)
}
// ComputeMastersFromSeed returns the master public key, master secret, and chain code in hex.
func ComputeMastersFromSeed(seed []byte) (secret [32]byte, chainCode [32]byte) {
masterSecret := []byte("Bitcoin seed")
secret, chainCode = i64(masterSecret, seed)
return
}
// DerivePrivateKeyForPath derives the private key by following the BIP 32/44 path from privKeyBytes,
// using the given chainCode.
func DerivePrivateKeyForPath(privKeyBytes [32]byte, chainCode [32]byte, path string) ([32]byte, error) {
data := privKeyBytes
parts := strings.Split(path, "/")
for _, part := range parts {
// do we have an apostrophe?
harden := part[len(part)-1:] == "'"
// harden == private derivation, else public derivation:
if harden {
part = part[:len(part)-1]
}
idx, err := strconv.Atoi(part)
if err != nil {
return [32]byte{}, fmt.Errorf("invalid BIP 32 path: %s", err)
}
if idx < 0 {
return [32]byte{}, errors.New("invalid BIP 32 path: index negative ot too large")
}
data, chainCode = derivePrivateKey(data, chainCode, uint32(idx), harden)
}
var derivedKey [32]byte
n := copy(derivedKey[:], data[:])
if n != 32 || len(data) != 32 {
return [32]byte{}, fmt.Errorf("expected a (secp256k1) key of length 32, got length: %v", len(data))
}
return derivedKey, nil
}
// derivePrivateKey derives the private key with index and chainCode.
// If harden is true, the derivation is 'hardened'.
// It returns the new private key and new chain code.
// For more information on hardened keys see:
// - https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
func derivePrivateKey(privKeyBytes [32]byte, chainCode [32]byte, index uint32, harden bool) ([32]byte, [32]byte) {
var data []byte
if harden {
index = index | 0x80000000
data = append([]byte{byte(0)}, privKeyBytes[:]...)
} else {
// this can't return an error:
pubkey := crypto.PrivKeySecp256k1(privKeyBytes).PubKey()
public := pubkey.(crypto.PubKeySecp256k1)
data = public[:]
}
data = append(data, uint32ToBytes(index)...)
data2, chainCode2 := i64(chainCode[:], data)
x := addScalars(privKeyBytes[:], data2[:])
return x, chainCode2
}
// modular big endian addition
func addScalars(a []byte, b []byte) [32]byte {
aInt := new(big.Int).SetBytes(a)
bInt := new(big.Int).SetBytes(b)
sInt := new(big.Int).Add(aInt, bInt)
x := sInt.Mod(sInt, btcec.S256().N).Bytes()
x2 := [32]byte{}
copy(x2[32-len(x):], x)
return x2
}
func uint32ToBytes(i uint32) []byte {
b := [4]byte{}
binary.BigEndian.PutUint32(b[:], i)
return b[:]
}
// i64 returns the two halfs of the SHA512 HMAC of key and data.
func i64(key []byte, data []byte) (IL [32]byte, IR [32]byte) {
mac := hmac.New(sha512.New, key)
// sha512 does not err
_, _ = mac.Write(data)
I := mac.Sum(nil)
copy(IL[:], I[:32])
copy(IR[:], I[32:])
return
}

73
keys/hd/hdpath_test.go Normal file
View File

@ -0,0 +1,73 @@
package hd
import (
"encoding/hex"
"fmt"
"github.com/tendermint/go-crypto/keys/bip39"
)
func ExampleStringifyPathParams() {
path := NewParams(44, 0, 0, false, 0)
fmt.Println(path.String())
// Output: 44'/0'/0'/0/0
}
func ExampleSomeBIP32TestVecs() {
seed := bip39.MnemonicToSeed("barrel original fuel morning among eternal " +
"filter ball stove pluck matrix mechanic")
master, ch := ComputeMastersFromSeed(seed)
fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)")
fmt.Println()
// cosmos
priv, _ := DerivePrivateKeyForPath(master, ch, FullFundraiserPath)
fmt.Println(hex.EncodeToString(priv[:]))
// bitcoin
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0")
fmt.Println(hex.EncodeToString(priv[:]))
// ether
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0")
fmt.Println(hex.EncodeToString(priv[:]))
fmt.Println()
fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html")
fmt.Println()
seed = bip39.MnemonicToSeed(
"advice process birth april short trust crater change bacon monkey medal garment " +
"gorilla ranch hour rival razor call lunar mention taste vacant woman sister")
master, ch = ComputeMastersFromSeed(seed)
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/1'/1'/0/4")
fmt.Println(hex.EncodeToString(priv[:]))
seed = bip39.MnemonicToSeed("idea naive region square margin day captain habit " +
"gun second farm pact pulse someone armed")
master, ch = ComputeMastersFromSeed(seed)
priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/420")
fmt.Println(hex.EncodeToString(priv[:]))
fmt.Println()
fmt.Println("BIP 32 example")
fmt.Println()
// bip32 path: m/0/7
seed = bip39.MnemonicToSeed("monitor flock loyal sick object grunt duty ride develop assault harsh history")
master, ch = ComputeMastersFromSeed(seed)
priv, _ = DerivePrivateKeyForPath(master, ch, "0/7")
fmt.Println(hex.EncodeToString(priv[:]))
// Output: keys from fundraiser test-vector (cosmos, bitcoin, ether)
//
// bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c
// e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d
// 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc
//
// keys generated via https://coinomi.com/recovery-phrase-tool.html
//
// a61f10c5fecf40c084c94fa54273b6f5d7989386be4a37669e6d6f7b0169c163
// 32c4599843de3ef161a629a461d12c60b009b676c35050be5f7ded3a3b23501f
//
// BIP 32 example
//
// c4c11d8c03625515905d7e89d25dfc66126fbc629ecca6db489a1a72fc4bda78
}

View File

@ -7,62 +7,117 @@ import (
"strings"
"github.com/pkg/errors"
crypto "github.com/tendermint/go-crypto"
"github.com/tendermint/go-crypto/keys/words"
"github.com/tendermint/go-crypto"
"github.com/tendermint/go-crypto/keys/bip39"
"github.com/tendermint/go-crypto/keys/hd"
dbm "github.com/tendermint/tmlibs/db"
)
// dbKeybase combines encyption and storage implementation to provide
// a full-featured key manager
type dbKeybase struct {
db dbm.DB
codec words.Codec
}
func New(db dbm.DB, codec words.Codec) dbKeybase {
return dbKeybase{
db: db,
codec: codec,
}
}
var _ Keybase = dbKeybase{}
// CreateMnemonic generates a new key and persists it to storage, encrypted
// using the passphrase. It returns the generated seedphrase
// (mnemonic) and the key Info. It returns an error if it fails to
// generate a key for the given algo type, or if another key is
// already stored under the same name.
func (kb dbKeybase) CreateMnemonic(name, passphrase string, algo SignAlgo) (Info, string, error) {
// NOTE: secret is SHA256 hashed by secp256k1 and ed25519.
// 16 byte secret corresponds to 12 BIP39 words.
// XXX: Ledgers use 24 words now - should we ?
secret := crypto.CRandBytes(16)
priv, err := generate(algo, secret)
if err != nil {
return nil, "", err
}
// Language is a language to create the BIP 39 mnemonic in.
// Currently, only english is supported though.
// Find a list of all supported languages in the BIP 39 spec (word lists).
type Language int
// encrypt and persist the key
info := kb.writeLocalKey(priv, name, passphrase)
const (
// English is the default language to create a mnemonic.
// It is the only supported language by this package.
English Language = iota + 1
// Japanese is currently not supported.
Japanese
// Korean is currently not supported.
Korean
// Spanish is currently not supported.
Spanish
// ChineseSimplified is currently not supported.
ChineseSimplified
// ChineseTraditional is currently not supported.
ChineseTraditional
// French is currently not supported.
French
// Italian is currently not supported.
Italian
)
// we append the type byte to the serialized secret to help with
// recovery
// ie [secret] = [type] + [secret]
typ := cryptoAlgoToByte(algo)
secret = append([]byte{typ}, secret...)
var (
// ErrUnsupportedSigningAlgo is raised when the caller tries to use a different signing scheme than secp256k1.
ErrUnsupportedSigningAlgo = errors.New("unsupported signing algo: only secp256k1 is supported")
// ErrUnsupportedLanguage is raised when the caller tries to use a different language than english for creating
// a mnemonic sentence.
ErrUnsupportedLanguage = errors.New("unsupported language: only english is supported")
)
// return the mnemonic phrase
words, err := kb.codec.BytesToWords(secret)
seed := strings.Join(words, " ")
return info, seed, err
// dbKeybase combines encryption and storage implementation to provide
// a full-featured key manager
type dbKeybase struct {
db dbm.DB
}
// New creates a new keybase instance using the passed DB for reading and writing keys.
func New(db dbm.DB) Keybase {
return dbKeybase{
db: db,
}
}
// CreateMnemonic generates a new key and persists it to storage, encrypted
// using the provided password.
// It returns the generated mnemonic and the key Info.
// It returns an error if it fails to
// generate a key for the given algo type, or if another key is
// already stored under the same name.
func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, mnemonic string, err error) {
if language != English {
return nil, "", ErrUnsupportedLanguage
}
if algo != Secp256k1 {
err = ErrUnsupportedSigningAlgo
return
}
// default number of words (24):
mnemonicS, err := bip39.NewMnemonic(bip39.FreshKey)
if err != nil {
return
}
mnemonic = strings.Join(mnemonicS, " ")
seed := bip39.MnemonicToSeed(mnemonic)
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
return
}
// CreateFundraiserKey converts a mnemonic to a private key and persists it,
// encrypted with the given password.
// TODO(ismail)
func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) {
words := strings.Split(mnemonic, " ")
if len(words) != 12 {
err = fmt.Errorf("recovering only works with 12 word (fundraiser) mnemonics, got: %v words", len(words))
return
}
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
if err != nil {
return
}
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
return
}
func (kb dbKeybase) Derive(name, mnemonic, passwd string, params hd.BIP44Params) (info Info, err error) {
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
if err != nil {
return
}
info, err = kb.persistDerivedKey(seed, passwd, name, params.String())
return
}
// CreateLedger creates a new locally-stored reference to a Ledger keypair
// It returns the created key info and an error if the Ledger could not be queried
func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SignAlgo) (Info, error) {
if algo != AlgoSecp256k1 {
return nil, fmt.Errorf("Only secp256k1 is supported for Ledger devices")
func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (Info, error) {
if algo != Secp256k1 {
return nil, ErrUnsupportedSigningAlgo
}
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path)
if err != nil {
@ -78,29 +133,24 @@ func (kb dbKeybase) CreateOffline(name string, pub crypto.PubKey) (Info, error)
return kb.writeOfflineKey(pub, name), nil
}
// Recover converts a seedphrase to a private key and persists it,
// encrypted with the given passphrase. Functions like Create, but
// seedphrase is input not output.
func (kb dbKeybase) Recover(name, passphrase, seedphrase string) (Info, error) {
words := strings.Split(strings.TrimSpace(seedphrase), " ")
secret, err := kb.codec.WordsToBytes(words)
func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info Info, err error) {
// create master key and derive first key:
masterPriv, ch := hd.ComputeMastersFromSeed(seed)
derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, fullHdPath)
if err != nil {
return nil, err
return
}
// secret is comprised of the actual secret with the type
// appended.
// ie [secret] = [type] + [secret]
typ, secret := secret[0], secret[1:]
algo := byteToSignAlgo(typ)
priv, err := generate(algo, secret)
if err != nil {
return nil, err
// if we have a password, use it to encrypt the private key and store it
// else store the public key only
if passwd != "" {
info = kb.writeLocalKey(crypto.PrivKeySecp256k1(derivedPriv), name, passwd)
} else {
pubk := crypto.PrivKeySecp256k1(derivedPriv).PubKey()
info = kb.writeOfflineKey(pubk, name)
}
// encrypt and persist key.
public := kb.writeLocalKey(priv, name, passphrase)
return public, nil
return
}
// List returns the keys from storage in alphabetical order.
@ -173,7 +223,7 @@ func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig crypto.Signat
func (kb dbKeybase) Export(name string) (armor string, err error) {
bz := kb.db.Get(infoKey(name))
if bz == nil {
return "", errors.New("No key to export with name " + name)
return "", fmt.Errorf("no key to export with name %s", name)
}
return armorInfoBytes(bz), nil
}
@ -184,7 +234,7 @@ func (kb dbKeybase) Export(name string) (armor string, err error) {
func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) {
bz := kb.db.Get(infoKey(name))
if bz == nil {
return "", errors.New("No key to export with name " + name)
return "", fmt.Errorf("no key to export with name %s", name)
}
info, err := readInfo(bz)
if err != nil {
@ -276,7 +326,7 @@ func (kb dbKeybase) Update(name, oldpass, newpass string) error {
kb.writeLocalKey(key, name, newpass)
return nil
default:
return fmt.Errorf("Locally stored key required")
return fmt.Errorf("locally stored key required")
}
}
@ -307,18 +357,6 @@ func (kb dbKeybase) writeInfo(info Info, name string) {
kb.db.SetSync(infoKey(name), writeInfo(info))
}
func generate(algo SignAlgo, secret []byte) (crypto.PrivKey, error) {
switch algo {
case AlgoEd25519:
return crypto.GenPrivKeyEd25519FromSecret(secret), nil
case AlgoSecp256k1:
return crypto.GenPrivKeySecp256k1FromSecret(secret), nil
default:
err := errors.Errorf("Cannot generate keys for algorithm: %s", algo)
return nil, err
}
}
func infoKey(name string) []byte {
return []byte(fmt.Sprintf("%s.info", name))
}

View File

@ -6,24 +6,21 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dbm "github.com/tendermint/tmlibs/db"
"github.com/tendermint/go-crypto"
"github.com/tendermint/go-crypto/keys"
"github.com/tendermint/go-crypto/keys/words"
"github.com/tendermint/go-crypto/keys/hd"
dbm "github.com/tendermint/tmlibs/db"
)
// TestKeyManagement makes sure we can manipulate these keys well
func TestKeyManagement(t *testing.T) {
// make the storage with reasonable defaults
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
algo := keys.AlgoEd25519
algo := keys.Secp256k1
n1, n2, n3 := "personal", "business", "other"
p1, p2 := "1234", "really-secure!@#$"
@ -32,14 +29,18 @@ func TestKeyManagement(t *testing.T) {
require.Nil(t, err)
assert.Empty(t, l)
_, _, err = cstore.CreateMnemonic(n1, keys.English, p1, keys.Ed25519)
assert.Error(t, err, "ed25519 keys are currently not supported by keybase")
// create some keys
_, err = cstore.Get(n1)
assert.NotNil(t, err)
i, _, err := cstore.CreateMnemonic(n1, p1, algo)
assert.Error(t, err)
i, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
require.NoError(t, err)
require.Equal(t, n1, i.GetName())
require.Nil(t, err)
_, _, err = cstore.CreateMnemonic(n2, p2, algo)
require.Nil(t, err)
_, _, err = cstore.CreateMnemonic(n2, keys.English, p2, algo)
require.NoError(t, err)
// we can get these keys
i2, err := cstore.Get(n2)
@ -49,7 +50,7 @@ func TestKeyManagement(t *testing.T) {
// list shows them in order
keyS, err := cstore.List()
require.Nil(t, err)
require.NoError(t, err)
require.Equal(t, 2, len(keyS))
// note these are in alphabetical order
assert.Equal(t, n2, keyS[0].GetName())
@ -60,12 +61,12 @@ func TestKeyManagement(t *testing.T) {
err = cstore.Delete("bad name", "foo")
require.NotNil(t, err)
err = cstore.Delete(n1, p1)
require.Nil(t, err)
require.NoError(t, err)
keyS, err = cstore.List()
require.Nil(t, err)
require.NoError(t, err)
assert.Equal(t, 1, len(keyS))
_, err = cstore.Get(n1)
assert.NotNil(t, err)
assert.Error(t, err)
// create an offline key
o1 := "offline"
@ -76,56 +77,45 @@ func TestKeyManagement(t *testing.T) {
require.Equal(t, pub1, i.GetPubKey())
require.Equal(t, o1, i.GetName())
keyS, err = cstore.List()
require.NoError(t, err)
require.Equal(t, 2, len(keyS))
// delete the offline key
err = cstore.Delete(o1, "no")
require.NotNil(t, err)
err = cstore.Delete(o1, "yes")
require.Nil(t, err)
require.NoError(t, err)
keyS, err = cstore.List()
require.NoError(t, err)
require.Equal(t, 1, len(keyS))
// make sure that it only signs with the right password
// tx := mock.NewSig([]byte("mytransactiondata"))
// err = cstore.Sign(n2, p1, tx)
// assert.NotNil(t, err)
// err = cstore.Sign(n2, p2, tx)
// assert.Nil(t, err, "%+v", err)
// sigs, err := tx.Signers()
// assert.Nil(t, err, "%+v", err)
// if assert.Equal(t, 1, len(sigs)) {
// assert.Equal(t, i2.PubKey, sigs[0])
// }
}
// TestSignVerify does some detailed checks on how we sign and validate
// signatures
func TestSignVerify(t *testing.T) {
// make the storage with reasonable defaults
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
algo := keys.AlgoSecp256k1
algo := keys.Secp256k1
n1, n2, n3 := "some dude", "a dudette", "dude-ish"
p1, p2, p3 := "1234", "foobar", "foobar"
// create two users and get their info
i1, _, err := cstore.CreateMnemonic(n1, p1, algo)
i1, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
require.Nil(t, err)
i2, _, err := cstore.CreateMnemonic(n2, p2, algo)
i2, _, err := cstore.CreateMnemonic(n2, keys.English, p2, algo)
require.Nil(t, err)
// Import a public key
armor, err := cstore.ExportPubKey(n2)
require.Nil(t, err)
cstore.ImportPubKey(n3, armor)
_, err = cstore.Get(n3)
require.Nil(t, err)
i3, err := cstore.Get(n3)
require.NoError(t, err)
require.Equal(t, i3.GetName(), n3)
// let's try to sign some messages
d1 := []byte("my first message")
@ -178,61 +168,6 @@ func TestSignVerify(t *testing.T) {
assert.NotNil(t, err)
}
/*
// TestSignWithLedger makes sure we have ledger compatibility with
// the crypto store.
//
// This test will only succeed with a ledger attached to the computer
// and the cosmos app open
func TestSignWithLedger(t *testing.T) {
if os.Getenv("WITH_LEDGER") == "" {
t.Skip("Set WITH_LEDGER to run code on real ledger")
}
// make the storage with reasonable defaults
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
n := "nano-s"
p := "hard2hack"
// create a nano user
c, _, err := cstore.Create(n, p, nano.KeyLedgerEd25519)
require.Nil(t, err, "%+v", err)
assert.Equal(t, c.Key, n)
_, ok := c.PubKey.Unwrap().(nano.PubKeyLedgerEd25519)
require.True(t, ok)
// make sure we can get it back
info, err := cstore.Get(n)
require.Nil(t, err, "%+v", err)
assert.Equal(t, info.Key, n)
key := info.PubKey
require.False(t ,key.Empty())
require.True(t, key.Equals(c.PubKey))
// let's try to sign some messages
d1 := []byte("welcome to cosmos")
d2 := []byte("please turn on the app")
// try signing both data with the ledger...
s1, pub, err := cstore.Sign(n, p, d1)
require.Nil(t, err)
require.Equal(t, info.PubKey, pub)
s2, pub, err := cstore.Sign(n, p, d2)
require.Nil(t, err)
require.Equal(t, info.PubKey, pub)
// now, let's check those signatures work
assert.True(t, key.VerifyBytes(d1, s1))
assert.True(t, key.VerifyBytes(d2, s2))
// and mismatched signatures don't
assert.False(t, key.VerifyBytes(d1, s2))
}
*/
func assertPassword(t *testing.T, cstore keys.Keybase, name, pass, badpass string) {
err := cstore.Update(name, badpass, pass)
assert.NotNil(t, err)
@ -247,18 +182,16 @@ func TestExportImport(t *testing.T) {
db := dbm.NewMemDB()
cstore := keys.New(
db,
words.MustLoadCodec("english"),
)
info, _, err := cstore.CreateMnemonic("john", "passphrase", keys.AlgoEd25519)
info, _, err := cstore.CreateMnemonic("john", keys.English,"secretcpw", keys.Secp256k1)
assert.NoError(t, err)
assert.Equal(t, info.GetName(), "john")
addr := info.GetPubKey().Address()
john, err := cstore.Get("john")
assert.NoError(t, err)
assert.Equal(t, john.GetName(), "john")
assert.Equal(t, john.GetPubKey().Address(), addr)
assert.Equal(t, info.GetName(), "john")
johnAddr := info.GetPubKey().Address()
armor, err := cstore.Export("john")
assert.NoError(t, err)
@ -269,22 +202,23 @@ func TestExportImport(t *testing.T) {
john2, err := cstore.Get("john2")
assert.NoError(t, err)
assert.Equal(t, john.GetPubKey().Address(), addr)
assert.Equal(t, john.GetPubKey().Address(), johnAddr)
assert.Equal(t, john.GetName(), "john")
assert.Equal(t, john, john2)
}
//
func TestExportImportPubKey(t *testing.T) {
// make the storage with reasonable defaults
db := dbm.NewMemDB()
cstore := keys.New(
db,
words.MustLoadCodec("english"),
)
// Create a private-public key pair and ensure consistency
info, _, err := cstore.CreateMnemonic("john", "passphrase", keys.AlgoEd25519)
assert.NoError(t, err)
// CreateMnemonic a private-public key pair and ensure consistency
notPasswd := "n9y25ah7"
info, _, err := cstore.CreateMnemonic("john", keys.English, notPasswd, keys.Secp256k1)
assert.Nil(t, err)
assert.NotEqual(t, info, "")
assert.Equal(t, info.GetName(), "john")
addr := info.GetPubKey().Address()
john, err := cstore.Get("john")
@ -320,15 +254,14 @@ func TestAdvancedKeyManagement(t *testing.T) {
// make the storage with reasonable defaults
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
algo := keys.AlgoSecp256k1
algo := keys.Secp256k1
n1, n2 := "old-name", "new name"
p1, p2 := "1234", "foobar"
// make sure key works with initial password
_, _, err := cstore.CreateMnemonic(n1, p1, algo)
_, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
require.Nil(t, err, "%+v", err)
assertPassword(t, cstore, n1, p1, p2)
@ -370,18 +303,17 @@ func TestSeedPhrase(t *testing.T) {
// make the storage with reasonable defaults
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
algo := keys.AlgoEd25519
algo := keys.Secp256k1
n1, n2 := "lost-key", "found-again"
p1, p2 := "1234", "foobar"
// make sure key works with initial password
info, seed, err := cstore.CreateMnemonic(n1, p1, algo)
info, mnemonic, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
require.Nil(t, err, "%+v", err)
assert.Equal(t, n1, info.GetName())
assert.NotEmpty(t, seed)
assert.NotEmpty(t, mnemonic)
// now, let us delete this key
err = cstore.Delete(n1, p1)
@ -389,9 +321,10 @@ func TestSeedPhrase(t *testing.T) {
_, err = cstore.Get(n1)
require.NotNil(t, err)
// let us re-create it from the seed-phrase
newInfo, err := cstore.Recover(n2, p2, seed)
require.Nil(t, err, "%+v", err)
// let us re-create it from the mnemonic-phrase
params := *hd.NewFundraiserParams(0 ,0 )
newInfo, err := cstore.Derive(n2,mnemonic, p2, params)
require.NoError(t, err)
assert.Equal(t, n2, newInfo.GetName())
assert.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address())
assert.Equal(t, info.GetPubKey(), newInfo.GetPubKey())
@ -401,13 +334,12 @@ func ExampleNew() {
// Select the encryption and storage for your cryptostore
cstore := keys.New(
dbm.NewMemDB(),
words.MustLoadCodec("english"),
)
ed := keys.AlgoEd25519
sec := keys.AlgoSecp256k1
sec := keys.Secp256k1
// Add keys and see they return in alphabetical order
bob, _, err := cstore.CreateMnemonic("Bob", "friend", ed)
bob, _, err := cstore.CreateMnemonic("Bob", keys.English, "friend", sec)
if err != nil {
// this should never happen
fmt.Println(err)
@ -415,8 +347,8 @@ func ExampleNew() {
// return info here just like in List
fmt.Println(bob.GetName())
}
cstore.CreateMnemonic("Alice", "secret", sec)
cstore.CreateMnemonic("Carl", "mitm", ed)
cstore.CreateMnemonic("Alice", keys.English, "secret", sec)
cstore.CreateMnemonic("Carl", keys.English, "mitm", sec)
info, _ := cstore.List()
for _, i := range info {
fmt.Println(i.GetName())
@ -429,7 +361,7 @@ func ExampleNew() {
fmt.Println("don't accept real passphrase")
}
// and we can validate the signature with publically available info
// and we can validate the signature with publicly available info
binfo, _ := cstore.Get("Bob")
if !binfo.GetPubKey().Equals(bob.GetPubKey()) {
fmt.Println("Get and Create return different keys")

View File

@ -1,32 +1,12 @@
package keys
import "fmt"
type SignAlgo string
// SigningAlgo defines an algorithm to derive key-pairs which can be used for cryptographic signing.
type SigningAlgo string
const (
AlgoEd25519 = SignAlgo("ed25519")
AlgoSecp256k1 = SignAlgo("secp256k1")
)
func cryptoAlgoToByte(key SignAlgo) byte {
switch key {
case AlgoEd25519:
return 0x01
case AlgoSecp256k1:
return 0x02
default:
panic(fmt.Sprintf("Unexpected type key %v", key))
}
}
func byteToSignAlgo(b byte) SignAlgo {
switch b {
case 0x01:
return AlgoEd25519
case 0x02:
return AlgoSecp256k1
default:
panic(fmt.Sprintf("Unexpected type byte %X", b))
}
}
// Secp256k1 uses the Bitcoin secp256k1 ECDSA parameters.
Secp256k1 = SigningAlgo("secp256k1")
// Ed25519 represents the Ed25519 signature system.
// It is currently not supported for end-user keys (wallets/ledgers).
Ed25519 = SigningAlgo("ed25519")
)

View File

@ -2,6 +2,7 @@ package keys
import (
crypto "github.com/tendermint/go-crypto"
"github.com/tendermint/go-crypto/keys/hd"
)
// Keybase exposes operations on a generic keystore
@ -15,13 +16,15 @@ type Keybase interface {
// Sign some bytes, looking up the private key to use
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error)
// Create a new locally-stored keypair, returning the mnemonic
CreateMnemonic(name, passphrase string, algo SignAlgo) (info Info, seed string, err error)
// Recover takes a seedphrase and loads in the key
Recover(name, passphrase, seedphrase string) (info Info, erro error)
// CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic
// key from that.
CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error)
// CreateFundraiserKey takes a mnemonic and derives, a password
CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error)
// Derive derives a key from the passed mnemonic using a BIP44 path.
Derive(name, mnemonic, passwd string, params hd.BIP44Params) (Info, error)
// Create, store, and return a new Ledger key reference
CreateLedger(name string, path crypto.DerivationPath, algo SignAlgo) (info Info, err error)
CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (info Info, err error)
// Create, store, and return a new offline key reference
CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error)
@ -34,7 +37,7 @@ type Keybase interface {
ExportPubKey(name string) (armor string, err error)
}
// Publically exposed information about a keypair
// Info is the publicly exposed information about a keypair
type Info interface {
// Human-readable type for key listing
GetType() string

View File

@ -1,208 +0,0 @@
package words
import (
"encoding/binary"
"errors"
"hash/crc32"
"hash/crc64"
"github.com/howeyc/crc16"
)
// 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)
}
var errInputTooShort = errors.New("input too short, no checksum present")
var errChecksumDoesntMatch = errors.New("checksum does not match")
// 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 }
// CRC16 does the ieee crc16 polynomial check
type CRC16 struct {
Poly uint16
table *crc16.Table
}
var _ ECC = (*CRC16)(nil)
const crc16ByteCount = 2
func NewIBMCRC16() *CRC16 {
return &CRC16{Poly: crc16.IBM}
}
func NewSCSICRC16() *CRC16 {
return &CRC16{Poly: crc16.SCSI}
}
func NewCCITTCRC16() *CRC16 {
return &CRC16{Poly: crc16.CCITT}
}
func (c *CRC16) AddECC(input []byte) []byte {
table := c.getTable()
// get crc and convert to some bytes...
crc := crc16.Checksum(input, table)
check := make([]byte, crc16ByteCount)
binary.BigEndian.PutUint16(check, crc)
// append it to the input
output := append(input, check...)
return output
}
func (c *CRC16) CheckECC(input []byte) ([]byte, error) {
table := c.getTable()
if len(input) <= crc16ByteCount {
return nil, errInputTooShort
}
cut := len(input) - crc16ByteCount
data, check := input[:cut], input[cut:]
crc := binary.BigEndian.Uint16(check)
calc := crc16.Checksum(data, table)
if crc != calc {
return nil, errChecksumDoesntMatch
}
return data, nil
}
func (c *CRC16) getTable() *crc16.Table {
if c.table != nil {
return c.table
}
if c.Poly == 0 {
c.Poly = crc16.IBM
}
c.table = crc16.MakeTable(c.Poly)
return c.table
}
// CRC32 does the ieee crc32 polynomial check
type CRC32 struct {
Poly uint32
table *crc32.Table
}
var _ ECC = (*CRC32)(nil)
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, errInputTooShort
}
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, errChecksumDoesntMatch
}
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)(nil)
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, errInputTooShort
}
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, errChecksumDoesntMatch
}
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
}

View File

@ -1,62 +0,0 @@
package words
import (
"testing"
asrt "github.com/stretchr/testify/assert"
cmn "github.com/tendermint/tmlibs/common"
)
var codecs = []ECC{
NewIBMCRC16(),
NewSCSICRC16(),
NewCCITTCRC16(),
NewIEEECRC32(),
NewCastagnoliCRC32(),
NewKoopmanCRC32(),
NewISOCRC64(),
NewECMACRC64(),
}
// TestECCPasses makes sure that the AddECC/CheckECC methods are symetric
func TestECCPasses(t *testing.T) {
assert := asrt.New(t)
checks := append(codecs, NoECC{})
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 := asrt.New(t)
checks := codecs
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)
}
}

View File

@ -1,200 +0,0 @@
package words
import (
"math/big"
"strings"
"github.com/pkg/errors"
"github.com/tendermint/go-crypto/keys/words/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(),
// check: NewIBMCRC16(),
}
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/words/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
}

View File

@ -1,180 +0,0 @@
package words
import (
"testing"
asrt "github.com/stretchr/testify/assert"
rqr "github.com/stretchr/testify/require"
cmn "github.com/tendermint/tmlibs/common"
)
func TestLengthCalc(t *testing.T) {
assert := asrt.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 := asrt.New(t), rqr.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 := asrt.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, err1 := codec.WordsToBytes(w)
assert.Nil(err1, "%d: %+v", i, err1)
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 := asrt.New(t), rqr.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 + 4
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)
}
}
}

View File

@ -1,68 +0,0 @@
package words
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