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