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:
parent
fed8807a32
commit
4634063698
|
@ -1,6 +1,12 @@
|
||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
# 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]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/brejski/hid"
|
name = "github.com/brejski/hid"
|
||||||
|
@ -63,12 +69,6 @@
|
||||||
packages = ["."]
|
packages = ["."]
|
||||||
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
|
revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a"
|
||||||
|
|
||||||
[[projects]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/howeyc/crc16"
|
|
||||||
packages = ["."]
|
|
||||||
revision = "2b2a61e366a66d3efb279e46176e7291001e0354"
|
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/jmhodges/levigo"
|
name = "github.com/jmhodges/levigo"
|
||||||
|
|
|
@ -28,10 +28,6 @@
|
||||||
name = "github.com/btcsuite/btcutil"
|
name = "github.com/btcsuite/btcutil"
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/howeyc/crc16"
|
|
||||||
branch = "master"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/pkg/errors"
|
name = "github.com/pkg/errors"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -15,7 +15,7 @@ check: check_tools
|
||||||
# Command to generate the workd list (kept here for documentation purposes only):
|
# Command to generate the workd list (kept here for documentation purposes only):
|
||||||
wordlist:
|
wordlist:
|
||||||
# To re-generate wordlist.go run:
|
# 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
|
build: wordlist
|
||||||
# Nothing else to build!
|
# Nothing else to build!
|
||||||
|
|
1
amino.go
1
amino.go
|
@ -15,6 +15,7 @@ func init() {
|
||||||
RegisterAmino(cdc)
|
RegisterAmino(cdc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterAmino registers all go-crypto related types in the given (amino) codec.
|
||||||
func RegisterAmino(cdc *amino.Codec) {
|
func RegisterAmino(cdc *amino.Codec) {
|
||||||
cdc.RegisterInterface((*PubKey)(nil), nil)
|
cdc.RegisterInterface((*PubKey)(nil), nil)
|
||||||
cdc.RegisterConcrete(PubKeyEd25519{},
|
cdc.RegisterConcrete(PubKeyEd25519{},
|
||||||
|
|
8
armor.go
8
armor.go
|
@ -2,9 +2,9 @@ package crypto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
|
||||||
. "github.com/tendermint/tmlibs/common"
|
|
||||||
"golang.org/x/crypto/openpgp/armor"
|
"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)
|
buf := new(bytes.Buffer)
|
||||||
w, err := armor.Encode(buf, blockType, headers)
|
w, err := armor.Encode(buf, blockType, headers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PanicSanity("Error encoding ascii armor: " + err.Error())
|
panic(fmt.Errorf("could not encode ascii armor: %s", err))
|
||||||
}
|
}
|
||||||
_, err = w.Write(data)
|
_, err = w.Write(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PanicSanity("Error encoding ascii armor: " + err.Error())
|
panic(fmt.Errorf("could not encode ascii armor: %s", err))
|
||||||
}
|
}
|
||||||
err = w.Close()
|
err = w.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
PanicSanity("Error encoding ascii armor: " + err.Error())
|
panic(fmt.Errorf("could not encode ascii armor: %s", err))
|
||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,7 +88,7 @@ type hashed struct {
|
||||||
// to compare the returned hashed password with its cleartext version.
|
// to compare the returned hashed password with its cleartext version.
|
||||||
func GenerateFromPassword(salt []byte, password []byte, cost int) ([]byte, error) {
|
func GenerateFromPassword(salt []byte, password []byte, cost int) ([]byte, error) {
|
||||||
if len(salt) != maxSaltSize {
|
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)
|
p, err := newFromPassword(salt, password, cost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
|
@ -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))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
172
keys/keybase.go
172
keys/keybase.go
|
@ -7,62 +7,117 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
crypto "github.com/tendermint/go-crypto"
|
"github.com/tendermint/go-crypto"
|
||||||
"github.com/tendermint/go-crypto/keys/words"
|
"github.com/tendermint/go-crypto/keys/bip39"
|
||||||
|
"github.com/tendermint/go-crypto/keys/hd"
|
||||||
dbm "github.com/tendermint/tmlibs/db"
|
dbm "github.com/tendermint/tmlibs/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dbKeybase combines encyption and storage implementation to provide
|
var _ Keybase = dbKeybase{}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
|
|
||||||
|
// dbKeybase combines encryption and storage implementation to provide
|
||||||
// a full-featured key manager
|
// a full-featured key manager
|
||||||
type dbKeybase struct {
|
type dbKeybase struct {
|
||||||
db dbm.DB
|
db dbm.DB
|
||||||
codec words.Codec
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(db dbm.DB, codec words.Codec) dbKeybase {
|
// New creates a new keybase instance using the passed DB for reading and writing keys.
|
||||||
|
func New(db dbm.DB) Keybase {
|
||||||
return dbKeybase{
|
return dbKeybase{
|
||||||
db: db,
|
db: db,
|
||||||
codec: codec,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Keybase = dbKeybase{}
|
|
||||||
|
|
||||||
// CreateMnemonic generates a new key and persists it to storage, encrypted
|
// CreateMnemonic generates a new key and persists it to storage, encrypted
|
||||||
// using the passphrase. It returns the generated seedphrase
|
// using the provided password.
|
||||||
// (mnemonic) and the key Info. It returns an error if it fails to
|
// 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
|
// generate a key for the given algo type, or if another key is
|
||||||
// already stored under the same name.
|
// already stored under the same name.
|
||||||
func (kb dbKeybase) CreateMnemonic(name, passphrase string, algo SignAlgo) (Info, string, error) {
|
func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, mnemonic string, err error) {
|
||||||
// NOTE: secret is SHA256 hashed by secp256k1 and ed25519.
|
if language != English {
|
||||||
// 16 byte secret corresponds to 12 BIP39 words.
|
return nil, "", ErrUnsupportedLanguage
|
||||||
// XXX: Ledgers use 24 words now - should we ?
|
}
|
||||||
secret := crypto.CRandBytes(16)
|
if algo != Secp256k1 {
|
||||||
priv, err := generate(algo, secret)
|
err = ErrUnsupportedSigningAlgo
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// default number of words (24):
|
||||||
|
mnemonicS, err := bip39.NewMnemonic(bip39.FreshKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return
|
||||||
|
}
|
||||||
|
mnemonic = strings.Join(mnemonicS, " ")
|
||||||
|
seed := bip39.MnemonicToSeed(mnemonic)
|
||||||
|
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// encrypt and persist the key
|
// CreateFundraiserKey converts a mnemonic to a private key and persists it,
|
||||||
info := kb.writeLocalKey(priv, name, passphrase)
|
// encrypted with the given password.
|
||||||
|
// TODO(ismail)
|
||||||
// we append the type byte to the serialized secret to help with
|
func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) {
|
||||||
// recovery
|
words := strings.Split(mnemonic, " ")
|
||||||
// ie [secret] = [type] + [secret]
|
if len(words) != 12 {
|
||||||
typ := cryptoAlgoToByte(algo)
|
err = fmt.Errorf("recovering only works with 12 word (fundraiser) mnemonics, got: %v words", len(words))
|
||||||
secret = append([]byte{typ}, secret...)
|
return
|
||||||
|
}
|
||||||
// return the mnemonic phrase
|
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
|
||||||
words, err := kb.codec.BytesToWords(secret)
|
if err != nil {
|
||||||
seed := strings.Join(words, " ")
|
return
|
||||||
return info, seed, err
|
}
|
||||||
|
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
|
// 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
|
// 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) {
|
func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (Info, error) {
|
||||||
if algo != AlgoSecp256k1 {
|
if algo != Secp256k1 {
|
||||||
return nil, fmt.Errorf("Only secp256k1 is supported for Ledger devices")
|
return nil, ErrUnsupportedSigningAlgo
|
||||||
}
|
}
|
||||||
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path)
|
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,29 +133,24 @@ func (kb dbKeybase) CreateOffline(name string, pub crypto.PubKey) (Info, error)
|
||||||
return kb.writeOfflineKey(pub, name), nil
|
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
|
func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info Info, err error) {
|
||||||
// seedphrase is input not output.
|
// create master key and derive first key:
|
||||||
func (kb dbKeybase) Recover(name, passphrase, seedphrase string) (Info, error) {
|
masterPriv, ch := hd.ComputeMastersFromSeed(seed)
|
||||||
words := strings.Split(strings.TrimSpace(seedphrase), " ")
|
derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, fullHdPath)
|
||||||
secret, err := kb.codec.WordsToBytes(words)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// secret is comprised of the actual secret with the type
|
// if we have a password, use it to encrypt the private key and store it
|
||||||
// appended.
|
// else store the public key only
|
||||||
// ie [secret] = [type] + [secret]
|
if passwd != "" {
|
||||||
typ, secret := secret[0], secret[1:]
|
info = kb.writeLocalKey(crypto.PrivKeySecp256k1(derivedPriv), name, passwd)
|
||||||
algo := byteToSignAlgo(typ)
|
} else {
|
||||||
priv, err := generate(algo, secret)
|
pubk := crypto.PrivKeySecp256k1(derivedPriv).PubKey()
|
||||||
if err != nil {
|
info = kb.writeOfflineKey(pubk, name)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
return
|
||||||
// encrypt and persist key.
|
|
||||||
public := kb.writeLocalKey(priv, name, passphrase)
|
|
||||||
return public, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the keys from storage in alphabetical order.
|
// 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) {
|
func (kb dbKeybase) Export(name string) (armor string, err error) {
|
||||||
bz := kb.db.Get(infoKey(name))
|
bz := kb.db.Get(infoKey(name))
|
||||||
if bz == nil {
|
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
|
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) {
|
func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) {
|
||||||
bz := kb.db.Get(infoKey(name))
|
bz := kb.db.Get(infoKey(name))
|
||||||
if bz == nil {
|
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)
|
info, err := readInfo(bz)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -276,7 +326,7 @@ func (kb dbKeybase) Update(name, oldpass, newpass string) error {
|
||||||
kb.writeLocalKey(key, name, newpass)
|
kb.writeLocalKey(key, name, newpass)
|
||||||
return nil
|
return nil
|
||||||
default:
|
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))
|
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 {
|
func infoKey(name string) []byte {
|
||||||
return []byte(fmt.Sprintf("%s.info", name))
|
return []byte(fmt.Sprintf("%s.info", name))
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,24 +6,21 @@ import (
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
dbm "github.com/tendermint/tmlibs/db"
|
|
||||||
|
|
||||||
"github.com/tendermint/go-crypto"
|
"github.com/tendermint/go-crypto"
|
||||||
"github.com/tendermint/go-crypto/keys"
|
"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
|
// TestKeyManagement makes sure we can manipulate these keys well
|
||||||
func TestKeyManagement(t *testing.T) {
|
func TestKeyManagement(t *testing.T) {
|
||||||
|
|
||||||
// make the storage with reasonable defaults
|
// make the storage with reasonable defaults
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
dbm.NewMemDB(),
|
dbm.NewMemDB(),
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
algo := keys.AlgoEd25519
|
algo := keys.Secp256k1
|
||||||
n1, n2, n3 := "personal", "business", "other"
|
n1, n2, n3 := "personal", "business", "other"
|
||||||
p1, p2 := "1234", "really-secure!@#$"
|
p1, p2 := "1234", "really-secure!@#$"
|
||||||
|
|
||||||
|
@ -32,14 +29,18 @@ func TestKeyManagement(t *testing.T) {
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
assert.Empty(t, l)
|
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
|
// create some keys
|
||||||
_, err = cstore.Get(n1)
|
_, err = cstore.Get(n1)
|
||||||
assert.NotNil(t, err)
|
assert.Error(t, err)
|
||||||
i, _, err := cstore.CreateMnemonic(n1, p1, algo)
|
i, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, n1, i.GetName())
|
require.Equal(t, n1, i.GetName())
|
||||||
require.Nil(t, err)
|
_, _, err = cstore.CreateMnemonic(n2, keys.English, p2, algo)
|
||||||
_, _, err = cstore.CreateMnemonic(n2, p2, algo)
|
require.NoError(t, err)
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
// we can get these keys
|
// we can get these keys
|
||||||
i2, err := cstore.Get(n2)
|
i2, err := cstore.Get(n2)
|
||||||
|
@ -49,7 +50,7 @@ func TestKeyManagement(t *testing.T) {
|
||||||
|
|
||||||
// list shows them in order
|
// list shows them in order
|
||||||
keyS, err := cstore.List()
|
keyS, err := cstore.List()
|
||||||
require.Nil(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 2, len(keyS))
|
require.Equal(t, 2, len(keyS))
|
||||||
// note these are in alphabetical order
|
// note these are in alphabetical order
|
||||||
assert.Equal(t, n2, keyS[0].GetName())
|
assert.Equal(t, n2, keyS[0].GetName())
|
||||||
|
@ -60,12 +61,12 @@ func TestKeyManagement(t *testing.T) {
|
||||||
err = cstore.Delete("bad name", "foo")
|
err = cstore.Delete("bad name", "foo")
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
err = cstore.Delete(n1, p1)
|
err = cstore.Delete(n1, p1)
|
||||||
require.Nil(t, err)
|
require.NoError(t, err)
|
||||||
keyS, err = cstore.List()
|
keyS, err = cstore.List()
|
||||||
require.Nil(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, len(keyS))
|
assert.Equal(t, 1, len(keyS))
|
||||||
_, err = cstore.Get(n1)
|
_, err = cstore.Get(n1)
|
||||||
assert.NotNil(t, err)
|
assert.Error(t, err)
|
||||||
|
|
||||||
// create an offline key
|
// create an offline key
|
||||||
o1 := "offline"
|
o1 := "offline"
|
||||||
|
@ -76,56 +77,45 @@ func TestKeyManagement(t *testing.T) {
|
||||||
require.Equal(t, pub1, i.GetPubKey())
|
require.Equal(t, pub1, i.GetPubKey())
|
||||||
require.Equal(t, o1, i.GetName())
|
require.Equal(t, o1, i.GetName())
|
||||||
keyS, err = cstore.List()
|
keyS, err = cstore.List()
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, 2, len(keyS))
|
require.Equal(t, 2, len(keyS))
|
||||||
|
|
||||||
// delete the offline key
|
// delete the offline key
|
||||||
err = cstore.Delete(o1, "no")
|
err = cstore.Delete(o1, "no")
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
err = cstore.Delete(o1, "yes")
|
err = cstore.Delete(o1, "yes")
|
||||||
require.Nil(t, err)
|
require.NoError(t, err)
|
||||||
keyS, err = cstore.List()
|
keyS, err = cstore.List()
|
||||||
|
require.NoError(t, err)
|
||||||
require.Equal(t, 1, len(keyS))
|
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
|
// TestSignVerify does some detailed checks on how we sign and validate
|
||||||
// signatures
|
// signatures
|
||||||
func TestSignVerify(t *testing.T) {
|
func TestSignVerify(t *testing.T) {
|
||||||
|
|
||||||
// make the storage with reasonable defaults
|
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
dbm.NewMemDB(),
|
dbm.NewMemDB(),
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
algo := keys.AlgoSecp256k1
|
algo := keys.Secp256k1
|
||||||
|
|
||||||
n1, n2, n3 := "some dude", "a dudette", "dude-ish"
|
n1, n2, n3 := "some dude", "a dudette", "dude-ish"
|
||||||
p1, p2, p3 := "1234", "foobar", "foobar"
|
p1, p2, p3 := "1234", "foobar", "foobar"
|
||||||
|
|
||||||
// create two users and get their info
|
// 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)
|
require.Nil(t, err)
|
||||||
|
|
||||||
i2, _, err := cstore.CreateMnemonic(n2, p2, algo)
|
i2, _, err := cstore.CreateMnemonic(n2, keys.English, p2, algo)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
|
|
||||||
// Import a public key
|
// Import a public key
|
||||||
armor, err := cstore.ExportPubKey(n2)
|
armor, err := cstore.ExportPubKey(n2)
|
||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
cstore.ImportPubKey(n3, armor)
|
cstore.ImportPubKey(n3, armor)
|
||||||
_, err = cstore.Get(n3)
|
i3, err := cstore.Get(n3)
|
||||||
require.Nil(t, err)
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, i3.GetName(), n3)
|
||||||
|
|
||||||
|
|
||||||
// let's try to sign some messages
|
// let's try to sign some messages
|
||||||
d1 := []byte("my first message")
|
d1 := []byte("my first message")
|
||||||
|
@ -178,61 +168,6 @@ func TestSignVerify(t *testing.T) {
|
||||||
assert.NotNil(t, err)
|
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) {
|
func assertPassword(t *testing.T, cstore keys.Keybase, name, pass, badpass string) {
|
||||||
err := cstore.Update(name, badpass, pass)
|
err := cstore.Update(name, badpass, pass)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
@ -247,18 +182,16 @@ func TestExportImport(t *testing.T) {
|
||||||
db := dbm.NewMemDB()
|
db := dbm.NewMemDB()
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
db,
|
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.NoError(t, err)
|
||||||
assert.Equal(t, info.GetName(), "john")
|
assert.Equal(t, info.GetName(), "john")
|
||||||
addr := info.GetPubKey().Address()
|
|
||||||
|
|
||||||
john, err := cstore.Get("john")
|
john, err := cstore.Get("john")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, john.GetName(), "john")
|
assert.Equal(t, info.GetName(), "john")
|
||||||
assert.Equal(t, john.GetPubKey().Address(), addr)
|
johnAddr := info.GetPubKey().Address()
|
||||||
|
|
||||||
armor, err := cstore.Export("john")
|
armor, err := cstore.Export("john")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -269,22 +202,23 @@ func TestExportImport(t *testing.T) {
|
||||||
john2, err := cstore.Get("john2")
|
john2, err := cstore.Get("john2")
|
||||||
assert.NoError(t, err)
|
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.GetName(), "john")
|
||||||
assert.Equal(t, john, john2)
|
assert.Equal(t, john, john2)
|
||||||
}
|
}
|
||||||
|
//
|
||||||
func TestExportImportPubKey(t *testing.T) {
|
func TestExportImportPubKey(t *testing.T) {
|
||||||
// make the storage with reasonable defaults
|
// make the storage with reasonable defaults
|
||||||
db := dbm.NewMemDB()
|
db := dbm.NewMemDB()
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
db,
|
db,
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a private-public key pair and ensure consistency
|
// CreateMnemonic a private-public key pair and ensure consistency
|
||||||
info, _, err := cstore.CreateMnemonic("john", "passphrase", keys.AlgoEd25519)
|
notPasswd := "n9y25ah7"
|
||||||
assert.NoError(t, err)
|
info, _, err := cstore.CreateMnemonic("john", keys.English, notPasswd, keys.Secp256k1)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotEqual(t, info, "")
|
||||||
assert.Equal(t, info.GetName(), "john")
|
assert.Equal(t, info.GetName(), "john")
|
||||||
addr := info.GetPubKey().Address()
|
addr := info.GetPubKey().Address()
|
||||||
john, err := cstore.Get("john")
|
john, err := cstore.Get("john")
|
||||||
|
@ -320,15 +254,14 @@ func TestAdvancedKeyManagement(t *testing.T) {
|
||||||
// make the storage with reasonable defaults
|
// make the storage with reasonable defaults
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
dbm.NewMemDB(),
|
dbm.NewMemDB(),
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
algo := keys.AlgoSecp256k1
|
algo := keys.Secp256k1
|
||||||
n1, n2 := "old-name", "new name"
|
n1, n2 := "old-name", "new name"
|
||||||
p1, p2 := "1234", "foobar"
|
p1, p2 := "1234", "foobar"
|
||||||
|
|
||||||
// make sure key works with initial password
|
// 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)
|
require.Nil(t, err, "%+v", err)
|
||||||
assertPassword(t, cstore, n1, p1, p2)
|
assertPassword(t, cstore, n1, p1, p2)
|
||||||
|
|
||||||
|
@ -370,18 +303,17 @@ func TestSeedPhrase(t *testing.T) {
|
||||||
// make the storage with reasonable defaults
|
// make the storage with reasonable defaults
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
dbm.NewMemDB(),
|
dbm.NewMemDB(),
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
algo := keys.AlgoEd25519
|
algo := keys.Secp256k1
|
||||||
n1, n2 := "lost-key", "found-again"
|
n1, n2 := "lost-key", "found-again"
|
||||||
p1, p2 := "1234", "foobar"
|
p1, p2 := "1234", "foobar"
|
||||||
|
|
||||||
// make sure key works with initial password
|
// 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)
|
require.Nil(t, err, "%+v", err)
|
||||||
assert.Equal(t, n1, info.GetName())
|
assert.Equal(t, n1, info.GetName())
|
||||||
assert.NotEmpty(t, seed)
|
assert.NotEmpty(t, mnemonic)
|
||||||
|
|
||||||
// now, let us delete this key
|
// now, let us delete this key
|
||||||
err = cstore.Delete(n1, p1)
|
err = cstore.Delete(n1, p1)
|
||||||
|
@ -389,9 +321,10 @@ func TestSeedPhrase(t *testing.T) {
|
||||||
_, err = cstore.Get(n1)
|
_, err = cstore.Get(n1)
|
||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
|
|
||||||
// let us re-create it from the seed-phrase
|
// let us re-create it from the mnemonic-phrase
|
||||||
newInfo, err := cstore.Recover(n2, p2, seed)
|
params := *hd.NewFundraiserParams(0 ,0 )
|
||||||
require.Nil(t, err, "%+v", err)
|
newInfo, err := cstore.Derive(n2,mnemonic, p2, params)
|
||||||
|
require.NoError(t, err)
|
||||||
assert.Equal(t, n2, newInfo.GetName())
|
assert.Equal(t, n2, newInfo.GetName())
|
||||||
assert.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address())
|
assert.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address())
|
||||||
assert.Equal(t, info.GetPubKey(), newInfo.GetPubKey())
|
assert.Equal(t, info.GetPubKey(), newInfo.GetPubKey())
|
||||||
|
@ -401,13 +334,12 @@ func ExampleNew() {
|
||||||
// Select the encryption and storage for your cryptostore
|
// Select the encryption and storage for your cryptostore
|
||||||
cstore := keys.New(
|
cstore := keys.New(
|
||||||
dbm.NewMemDB(),
|
dbm.NewMemDB(),
|
||||||
words.MustLoadCodec("english"),
|
|
||||||
)
|
)
|
||||||
ed := keys.AlgoEd25519
|
|
||||||
sec := keys.AlgoSecp256k1
|
sec := keys.Secp256k1
|
||||||
|
|
||||||
// Add keys and see they return in alphabetical order
|
// 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 {
|
if err != nil {
|
||||||
// this should never happen
|
// this should never happen
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -415,8 +347,8 @@ func ExampleNew() {
|
||||||
// return info here just like in List
|
// return info here just like in List
|
||||||
fmt.Println(bob.GetName())
|
fmt.Println(bob.GetName())
|
||||||
}
|
}
|
||||||
cstore.CreateMnemonic("Alice", "secret", sec)
|
cstore.CreateMnemonic("Alice", keys.English, "secret", sec)
|
||||||
cstore.CreateMnemonic("Carl", "mitm", ed)
|
cstore.CreateMnemonic("Carl", keys.English, "mitm", sec)
|
||||||
info, _ := cstore.List()
|
info, _ := cstore.List()
|
||||||
for _, i := range info {
|
for _, i := range info {
|
||||||
fmt.Println(i.GetName())
|
fmt.Println(i.GetName())
|
||||||
|
@ -429,7 +361,7 @@ func ExampleNew() {
|
||||||
fmt.Println("don't accept real passphrase")
|
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")
|
binfo, _ := cstore.Get("Bob")
|
||||||
if !binfo.GetPubKey().Equals(bob.GetPubKey()) {
|
if !binfo.GetPubKey().Equals(bob.GetPubKey()) {
|
||||||
fmt.Println("Get and Create return different keys")
|
fmt.Println("Get and Create return different keys")
|
||||||
|
|
34
keys/keys.go
34
keys/keys.go
|
@ -1,32 +1,12 @@
|
||||||
package keys
|
package keys
|
||||||
|
|
||||||
import "fmt"
|
// SigningAlgo defines an algorithm to derive key-pairs which can be used for cryptographic signing.
|
||||||
|
type SigningAlgo string
|
||||||
type SignAlgo string
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AlgoEd25519 = SignAlgo("ed25519")
|
// Secp256k1 uses the Bitcoin secp256k1 ECDSA parameters.
|
||||||
AlgoSecp256k1 = SignAlgo("secp256k1")
|
Secp256k1 = SigningAlgo("secp256k1")
|
||||||
|
// Ed25519 represents the Ed25519 signature system.
|
||||||
|
// It is currently not supported for end-user keys (wallets/ledgers).
|
||||||
|
Ed25519 = SigningAlgo("ed25519")
|
||||||
)
|
)
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ package keys
|
||||||
|
|
||||||
import (
|
import (
|
||||||
crypto "github.com/tendermint/go-crypto"
|
crypto "github.com/tendermint/go-crypto"
|
||||||
|
"github.com/tendermint/go-crypto/keys/hd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keybase exposes operations on a generic keystore
|
// 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 some bytes, looking up the private key to use
|
||||||
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error)
|
Sign(name, passphrase string, msg []byte) (crypto.Signature, crypto.PubKey, error)
|
||||||
|
|
||||||
// Create a new locally-stored keypair, returning the mnemonic
|
// CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic
|
||||||
CreateMnemonic(name, passphrase string, algo SignAlgo) (info Info, seed string, err error)
|
// key from that.
|
||||||
// Recover takes a seedphrase and loads in the key
|
CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error)
|
||||||
Recover(name, passphrase, seedphrase string) (info Info, erro 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
|
// 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
|
// Create, store, and return a new offline key reference
|
||||||
CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error)
|
CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error)
|
||||||
|
@ -34,7 +37,7 @@ type Keybase interface {
|
||||||
ExportPubKey(name string) (armor string, err error)
|
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 {
|
type Info interface {
|
||||||
// Human-readable type for key listing
|
// Human-readable type for key listing
|
||||||
GetType() string
|
GetType() string
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
Loading…
Reference in New Issue