Browse Source

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)
pull/1782/head
Ismail Khoffi 7 years ago
committed by GitHub
parent
commit
4634063698
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 590 additions and 10086 deletions
  1. +6
    -6
      Gopkg.lock
  2. +0
    -4
      Gopkg.toml
  3. +1
    -1
      Makefile
  4. +1
    -0
      amino.go
  5. +4
    -4
      armor.go
  6. +1
    -1
      keys/bcrypt/bcrypt.go
  7. +62
    -0
      keys/bip39/wordcodec.go
  8. +15
    -0
      keys/bip39/wordcodec_test.go
  9. +0
    -352
      keys/hd/address.go
  10. +0
    -37
      keys/hd/address_test.go
  11. +83
    -0
      keys/hd/fundraiser_test.go
  12. +0
    -238
      keys/hd/hd_test.go
  13. +168
    -0
      keys/hd/hdpath.go
  14. +73
    -0
      keys/hd/hdpath_test.go
  15. +106
    -68
      keys/keybase.go
  16. +52
    -120
      keys/keybase_test.go
  17. +8
    -28
      keys/keys.go
  18. +10
    -7
      keys/types.go
  19. +0
    -208
      keys/words/ecc.go
  20. +0
    -62
      keys/words/ecc_test.go
  21. +0
    -200
      keys/words/wordcodec.go
  22. +0
    -180
      keys/words/wordcodec_test.go
  23. +0
    -68
      keys/words/wordcodecbench_test.go
  24. +0
    -2048
      keys/words/wordlist/chinese_simplified.txt
  25. +0
    -2048
      keys/words/wordlist/english.txt
  26. +0
    -2048
      keys/words/wordlist/japanese.txt
  27. +0
    -2048
      keys/words/wordlist/spanish.txt
  28. +0
    -310
      keys/words/wordlist/wordlist.go

+ 6
- 6
Gopkg.lock View File

@ -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"


+ 0
- 4
Gopkg.toml View File

@ -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"


+ 1
- 1
Makefile View File

@ -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
- 0
amino.go View File

@ -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{},


+ 4
- 4
armor.go View File

@ -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()
} }


+ 1
- 1
keys/bcrypt/bcrypt.go View File

@ -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 {


+ 62
- 0
keys/bip39/wordcodec.go View File

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

+ 15
- 0
keys/bip39/wordcodec_test.go View File

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

+ 0
- 352
keys/hd/address.go View File

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

+ 0
- 37
keys/hd/address_test.go View File

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

+ 83
- 0
keys/hd/fundraiser_test.go View File

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

+ 0
- 238
keys/hd/hd_test.go View File

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

+ 168
- 0
keys/hd/hdpath.go View File

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

+ 73
- 0
keys/hd/hdpath_test.go View File

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

+ 106
- 68
keys/keybase.go View File

@ -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/keys/words"
"github.com/tendermint/go-crypto"
"github.com/tendermint/go-crypto/keys/bip39"
"github.com/tendermint/go-crypto/keys/hd"
dbm "github.com/tendermint/tmlibs/db" 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
codec words.Codec
db dbm.DB
} }
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,
codec: codec,
db: db,
} }
} }
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
// (mnemonic) and the key Info. It returns an error if it fails to
// using the provided password.
// It returns the generated mnemonic and the key Info.
// It returns an error if it fails to
// generate a key for the given algo type, or if another key is // 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) {
// NOTE: secret is SHA256 hashed by secp256k1 and ed25519.
// 16 byte secret corresponds to 12 BIP39 words.
// XXX: Ledgers use 24 words now - should we ?
secret := crypto.CRandBytes(16)
priv, err := generate(algo, secret)
func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, mnemonic string, err error) {
if language != English {
return nil, "", ErrUnsupportedLanguage
}
if algo != Secp256k1 {
err = ErrUnsupportedSigningAlgo
return
}
// default number of words (24):
mnemonicS, err := bip39.NewMnemonic(bip39.FreshKey)
if err != nil { 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
info := kb.writeLocalKey(priv, name, passphrase)
// CreateFundraiserKey converts a mnemonic to a private key and persists it,
// encrypted with the given password.
// TODO(ismail)
func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) {
words := strings.Split(mnemonic, " ")
if len(words) != 12 {
err = fmt.Errorf("recovering only works with 12 word (fundraiser) mnemonics, got: %v words", len(words))
return
}
seed, err := bip39.MnemonicToSeedWithErrChecking(mnemonic)
if err != nil {
return
}
info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath)
return
}
// we append the type byte to the serialized secret to help with
// recovery
// ie [secret] = [type] + [secret]
typ := cryptoAlgoToByte(algo)
secret = append([]byte{typ}, secret...)
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 the mnemonic phrase
words, err := kb.codec.BytesToWords(secret)
seed := strings.Join(words, " ")
return info, seed, err
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) {
if algo != AlgoSecp256k1 {
return nil, fmt.Errorf("Only secp256k1 is supported for Ledger devices")
func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (Info, error) {
if algo != Secp256k1 {
return nil, ErrUnsupportedSigningAlgo
} }
priv, err := crypto.NewPrivKeyLedgerSecp256k1(path) 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
// seedphrase is input not output.
func (kb dbKeybase) Recover(name, passphrase, seedphrase string) (Info, error) {
words := strings.Split(strings.TrimSpace(seedphrase), " ")
secret, err := kb.codec.WordsToBytes(words)
if err != nil {
return nil, err
}
// secret is comprised of the actual secret with the type
// appended.
// ie [secret] = [type] + [secret]
typ, secret := secret[0], secret[1:]
algo := byteToSignAlgo(typ)
priv, err := generate(algo, secret)
func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info Info, err error) {
// create master key and derive first key:
masterPriv, ch := hd.ComputeMastersFromSeed(seed)
derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, fullHdPath)
if err != nil { if err != nil {
return nil, err
return
} }
// encrypt and persist key.
public := kb.writeLocalKey(priv, name, passphrase)
return public, nil
// if we have a password, use it to encrypt the private key and store it
// else store the public key only
if passwd != "" {
info = kb.writeLocalKey(crypto.PrivKeySecp256k1(derivedPriv), name, passwd)
} else {
pubk := crypto.PrivKeySecp256k1(derivedPriv).PubKey()
info = kb.writeOfflineKey(pubk, name)
}
return
} }
// 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))
} }

+ 52
- 120
keys/keybase_test.go View File

@ -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)
i, _, err := cstore.CreateMnemonic(n1, p1, algo)
assert.Error(t, err)
i, _, err := cstore.CreateMnemonic(n1, keys.English, p1, algo)
require.NoError(t, err)
require.Equal(t, n1, i.GetName()) require.Equal(t, n1, i.GetName())
require.Nil(t, err)
_, _, err = cstore.CreateMnemonic(n2, p2, algo)
require.Nil(t, err)
_, _, err = cstore.CreateMnemonic(n2, keys.English, p2, algo)
require.NoError(t, err)
// we can get these keys // 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)
require.Nil(t, err)
i3, err := cstore.Get(n3)
require.NoError(t, err)
require.Equal(t, i3.GetName(), n3)
// let's try to sign some messages // 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, john.GetPubKey().Address(), addr)
assert.Equal(t, info.GetName(), "john")
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
info, _, err := cstore.CreateMnemonic("john", "passphrase", keys.AlgoEd25519)
assert.NoError(t, err)
// CreateMnemonic a private-public key pair and ensure consistency
notPasswd := "n9y25ah7"
info, _, err := cstore.CreateMnemonic("john", keys.English, notPasswd, keys.Secp256k1)
assert.Nil(t, err)
assert.NotEqual(t, info, "")
assert.Equal(t, info.GetName(), "john") 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
newInfo, err := cstore.Recover(n2, p2, seed)
require.Nil(t, err, "%+v", err)
// let us re-create it from the mnemonic-phrase
params := *hd.NewFundraiserParams(0 ,0 )
newInfo, err := cstore.Derive(n2,mnemonic, p2, params)
require.NoError(t, err)
assert.Equal(t, n2, newInfo.GetName()) assert.Equal(t, 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("Carl", "mitm", ed)
cstore.CreateMnemonic("Alice", keys.English, "secret", sec)
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")


+ 8
- 28
keys/keys.go View File

@ -1,32 +1,12 @@
package keys 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 ( 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")
)

+ 10
- 7
keys/types.go View File

@ -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(name, passphrase string, algo SignAlgo) (info Info, seed string, err error)
// Recover takes a seedphrase and loads in the key
Recover(name, passphrase, seedphrase string) (info Info, erro error)
// CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic
// key from that.
CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error)
// CreateFundraiserKey takes a mnemonic and derives, a password
CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error)
// Derive derives a key from the passed mnemonic using a BIP44 path.
Derive(name, mnemonic, passwd string, params hd.BIP44Params) (Info, error)
// Create, store, and return a new Ledger key reference // 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


+ 0
- 208
keys/words/ecc.go View File

@ -1,208 +0,0 @@
package words
import (
"encoding/binary"
"errors"
"hash/crc32"
"hash/crc64"
"github.com/howeyc/crc16"
)
// ECC is used for anything that calculates an error-correcting code
type ECC interface {
// AddECC calculates an error-correcting code for the input
// returns an output with the code appended
AddECC([]byte) []byte
// CheckECC verifies if the ECC is proper on the input and returns
// the data with the code removed, or an error
CheckECC([]byte) ([]byte, error)
}
var errInputTooShort = errors.New("input too short, no checksum present")
var errChecksumDoesntMatch = errors.New("checksum does not match")
// NoECC is a no-op placeholder, kind of useless... except for tests
type NoECC struct{}
var _ ECC = NoECC{}
func (_ NoECC) AddECC(input []byte) []byte { return input }
func (_ NoECC) CheckECC(input []byte) ([]byte, error) { return input, nil }
// CRC16 does the ieee crc16 polynomial check
type CRC16 struct {
Poly uint16
table *crc16.Table
}
var _ ECC = (*CRC16)(nil)
const crc16ByteCount = 2
func NewIBMCRC16() *CRC16 {
return &CRC16{Poly: crc16.IBM}
}
func NewSCSICRC16() *CRC16 {
return &CRC16{Poly: crc16.SCSI}
}
func NewCCITTCRC16() *CRC16 {
return &CRC16{Poly: crc16.CCITT}
}
func (c *CRC16) AddECC(input []byte) []byte {
table := c.getTable()
// get crc and convert to some bytes...
crc := crc16.Checksum(input, table)
check := make([]byte, crc16ByteCount)
binary.BigEndian.PutUint16(check, crc)
// append it to the input
output := append(input, check...)
return output
}
func (c *CRC16) CheckECC(input []byte) ([]byte, error) {
table := c.getTable()
if len(input) <= crc16ByteCount {
return nil, errInputTooShort
}
cut := len(input) - crc16ByteCount
data, check := input[:cut], input[cut:]
crc := binary.BigEndian.Uint16(check)
calc := crc16.Checksum(data, table)
if crc != calc {
return nil, errChecksumDoesntMatch
}
return data, nil
}
func (c *CRC16) getTable() *crc16.Table {
if c.table != nil {
return c.table
}
if c.Poly == 0 {
c.Poly = crc16.IBM
}
c.table = crc16.MakeTable(c.Poly)
return c.table
}
// CRC32 does the ieee crc32 polynomial check
type CRC32 struct {
Poly uint32
table *crc32.Table
}
var _ ECC = (*CRC32)(nil)
func NewIEEECRC32() *CRC32 {
return &CRC32{Poly: crc32.IEEE}
}
func NewCastagnoliCRC32() *CRC32 {
return &CRC32{Poly: crc32.Castagnoli}
}
func NewKoopmanCRC32() *CRC32 {
return &CRC32{Poly: crc32.Koopman}
}
func (c *CRC32) AddECC(input []byte) []byte {
table := c.getTable()
// get crc and convert to some bytes...
crc := crc32.Checksum(input, table)
check := make([]byte, crc32.Size)
binary.BigEndian.PutUint32(check, crc)
// append it to the input
output := append(input, check...)
return output
}
func (c *CRC32) CheckECC(input []byte) ([]byte, error) {
table := c.getTable()
if len(input) <= crc32.Size {
return nil, errInputTooShort
}
cut := len(input) - crc32.Size
data, check := input[:cut], input[cut:]
crc := binary.BigEndian.Uint32(check)
calc := crc32.Checksum(data, table)
if crc != calc {
return nil, errChecksumDoesntMatch
}
return data, nil
}
func (c *CRC32) getTable() *crc32.Table {
if c.table == nil {
if c.Poly == 0 {
c.Poly = crc32.IEEE
}
c.table = crc32.MakeTable(c.Poly)
}
return c.table
}
// CRC64 does the ieee crc64 polynomial check
type CRC64 struct {
Poly uint64
table *crc64.Table
}
var _ ECC = (*CRC64)(nil)
func NewISOCRC64() *CRC64 {
return &CRC64{Poly: crc64.ISO}
}
func NewECMACRC64() *CRC64 {
return &CRC64{Poly: crc64.ECMA}
}
func (c *CRC64) AddECC(input []byte) []byte {
table := c.getTable()
// get crc and convert to some bytes...
crc := crc64.Checksum(input, table)
check := make([]byte, crc64.Size)
binary.BigEndian.PutUint64(check, crc)
// append it to the input
output := append(input, check...)
return output
}
func (c *CRC64) CheckECC(input []byte) ([]byte, error) {
table := c.getTable()
if len(input) <= crc64.Size {
return nil, errInputTooShort
}
cut := len(input) - crc64.Size
data, check := input[:cut], input[cut:]
crc := binary.BigEndian.Uint64(check)
calc := crc64.Checksum(data, table)
if crc != calc {
return nil, errChecksumDoesntMatch
}
return data, nil
}
func (c *CRC64) getTable() *crc64.Table {
if c.table == nil {
if c.Poly == 0 {
c.Poly = crc64.ISO
}
c.table = crc64.MakeTable(c.Poly)
}
return c.table
}

+ 0
- 62
keys/words/ecc_test.go View File

@ -1,62 +0,0 @@
package words
import (
"testing"
asrt "github.com/stretchr/testify/assert"
cmn "github.com/tendermint/tmlibs/common"
)
var codecs = []ECC{
NewIBMCRC16(),
NewSCSICRC16(),
NewCCITTCRC16(),
NewIEEECRC32(),
NewCastagnoliCRC32(),
NewKoopmanCRC32(),
NewISOCRC64(),
NewECMACRC64(),
}
// TestECCPasses makes sure that the AddECC/CheckECC methods are symetric
func TestECCPasses(t *testing.T) {
assert := asrt.New(t)
checks := append(codecs, NoECC{})
for _, check := range checks {
for i := 0; i < 2000; i++ {
numBytes := cmn.RandInt()%60 + 1
data := cmn.RandBytes(numBytes)
checked := check.AddECC(data)
res, err := check.CheckECC(checked)
if assert.Nil(err, "%#v: %+v", check, err) {
assert.Equal(data, res, "%v", check)
}
}
}
}
// TestECCFails makes sure random data will (usually) fail the checksum
func TestECCFails(t *testing.T) {
assert := asrt.New(t)
checks := codecs
attempts := 2000
for _, check := range checks {
failed := 0
for i := 0; i < attempts; i++ {
numBytes := cmn.RandInt()%60 + 1
data := cmn.RandBytes(numBytes)
_, err := check.CheckECC(data)
if err != nil {
failed += 1
}
}
// we allow up to 1 falsely accepted checksums, as there are random matches
assert.InDelta(attempts, failed, 1, "%v", check)
}
}

+ 0
- 200
keys/words/wordcodec.go View File

@ -1,200 +0,0 @@
package words
import (
"math/big"
"strings"
"github.com/pkg/errors"
"github.com/tendermint/go-crypto/keys/words/wordlist"
)
const BankSize = 2048
// TODO: add error-checking codecs for invalid phrases
type Codec interface {
BytesToWords([]byte) ([]string, error)
WordsToBytes([]string) ([]byte, error)
}
type WordCodec struct {
words []string
bytes map[string]int
check ECC
}
var _ Codec = &WordCodec{}
func NewCodec(words []string) (codec *WordCodec, err error) {
if len(words) != BankSize {
return codec, errors.Errorf("Bank must have %d words, found %d", BankSize, len(words))
}
res := &WordCodec{
words: words,
// TODO: configure this outside???
check: NewIEEECRC32(),
// check: NewIBMCRC16(),
}
return res, nil
}
// LoadCodec loads a pre-compiled language file
func LoadCodec(bank string) (codec *WordCodec, err error) {
words, err := loadBank(bank)
if err != nil {
return codec, err
}
return NewCodec(words)
}
// MustLoadCodec panics if word bank is missing, only for tests
func MustLoadCodec(bank string) *WordCodec {
codec, err := LoadCodec(bank)
if err != nil {
panic(err)
}
return codec
}
// loadBank opens a wordlist file and returns all words inside
func loadBank(bank string) ([]string, error) {
filename := "keys/words/wordlist/" + bank + ".txt"
words, err := wordlist.Asset(filename)
if err != nil {
return nil, err
}
wordsAll := strings.Split(strings.TrimSpace(string(words)), "\n")
return wordsAll, nil
}
// // TODO: read from go-bind assets
// func getData(filename string) (string, error) {
// f, err := os.Open(filename)
// if err != nil {
// return "", errors.WithStack(err)
// }
// defer f.Close()
// data, err := ioutil.ReadAll(f)
// if err != nil {
// return "", errors.WithStack(err)
// }
// return string(data), nil
// }
// given this many bytes, we will produce this many words
func wordlenFromBytes(numBytes int) int {
// 2048 words per bank, which is 2^11.
// 8 bits per byte, and we add +10 so it rounds up
return (8*numBytes + 10) / 11
}
// given this many words, we will produce this many bytes.
// sometimes there are two possibilities.
// if maybeShorter is true, then represents len OR len-1 bytes
func bytelenFromWords(numWords int) (length int, maybeShorter bool) {
// calculate the max number of complete bytes we could store in this word
length = 11 * numWords / 8
// if one less byte would also generate this length, set maybeShorter
if wordlenFromBytes(length-1) == numWords {
maybeShorter = true
}
return
}
// TODO: add checksum
func (c *WordCodec) BytesToWords(raw []byte) (words []string, err error) {
// always add a checksum to the data
data := c.check.AddECC(raw)
numWords := wordlenFromBytes(len(data))
n2048 := big.NewInt(2048)
nData := big.NewInt(0).SetBytes(data)
nRem := big.NewInt(0)
// Alternative, use condition "nData.BitLen() > 0"
// to allow for shorter words when data has leading 0's
for i := 0; i < numWords; i++ {
nData.DivMod(nData, n2048, nRem)
rem := nRem.Int64()
w := c.words[rem]
// double-check bank on generation...
_, err := c.GetIndex(w)
if err != nil {
return nil, err
}
words = append(words, w)
}
return words, nil
}
func (c *WordCodec) WordsToBytes(words []string) ([]byte, error) {
l := len(words)
if l == 0 {
return nil, errors.New("Didn't provide any words")
}
n2048 := big.NewInt(2048)
nData := big.NewInt(0)
// since we output words based on the remainder, the first word has the lowest
// value... we must load them in reverse order
for i := 1; i <= l; i++ {
rem, err := c.GetIndex(words[l-i])
if err != nil {
return nil, err
}
nRem := big.NewInt(int64(rem))
nData.Mul(nData, n2048)
nData.Add(nData, nRem)
}
// we copy into a slice of the expected size, so it is not shorter if there
// are lots of leading 0s
dataBytes := nData.Bytes()
// copy into the container we have with the expected size
outLen, flex := bytelenFromWords(len(words))
toCheck := make([]byte, outLen)
if len(dataBytes) > outLen {
return nil, errors.New("Invalid data, could not have been generated by this codec")
}
copy(toCheck[outLen-len(dataBytes):], dataBytes)
// validate the checksum...
output, err := c.check.CheckECC(toCheck)
if flex && err != nil {
// if flex, try again one shorter....
toCheck = toCheck[1:]
output, err = c.check.CheckECC(toCheck)
}
return output, err
}
// GetIndex finds the index of the words to create bytes
// Generates a map the first time it is loaded, to avoid needless
// computation when list is not used.
func (c *WordCodec) GetIndex(word string) (int, error) {
// generate the first time
if c.bytes == nil {
b := map[string]int{}
for i, w := range c.words {
if _, ok := b[w]; ok {
return -1, errors.Errorf("Duplicate word in list: %s", w)
}
b[w] = i
}
c.bytes = b
}
// get the index, or an error
rem, ok := c.bytes[word]
if !ok {
return -1, errors.Errorf("Unrecognized word: %s", word)
}
return rem, nil
}

+ 0
- 180
keys/words/wordcodec_test.go View File

@ -1,180 +0,0 @@
package words
import (
"testing"
asrt "github.com/stretchr/testify/assert"
rqr "github.com/stretchr/testify/require"
cmn "github.com/tendermint/tmlibs/common"
)
func TestLengthCalc(t *testing.T) {
assert := asrt.New(t)
cases := []struct {
bytes, words int
flexible bool
}{
{1, 1, false},
{2, 2, false},
// bytes pairs with same word count
{3, 3, true},
{4, 3, true},
{5, 4, false},
// bytes pairs with same word count
{10, 8, true},
{11, 8, true},
{12, 9, false},
{13, 10, false},
{20, 15, false},
// bytes pairs with same word count
{21, 16, true},
{32, 24, true},
}
for _, tc := range cases {
wl := wordlenFromBytes(tc.bytes)
assert.Equal(tc.words, wl, "%d", tc.bytes)
bl, flex := bytelenFromWords(tc.words)
assert.Equal(tc.flexible, flex, "%d", tc.words)
if !flex {
assert.Equal(tc.bytes, bl, "%d", tc.words)
} else {
// check if it is either tc.bytes or tc.bytes +1
choices := []int{tc.bytes, tc.bytes + 1}
assert.Contains(choices, bl, "%d", tc.words)
}
}
}
func TestEncodeDecode(t *testing.T) {
assert, require := asrt.New(t), rqr.New(t)
codec, err := LoadCodec("english")
require.Nil(err, "%+v", err)
cases := [][]byte{
{7, 8, 9}, // TODO: 3 words -> 3 or 4 bytes
{12, 54, 99, 11}, // TODO: 3 words -> 3 or 4 bytes
{0, 54, 99, 11}, // TODO: 3 words -> 3 or 4 bytes, detect leading 0
{1, 2, 3, 4, 5}, // normal
{0, 0, 0, 0, 122, 23, 82, 195}, // leading 0s (8 chars, unclear)
{0, 0, 0, 0, 5, 22, 123, 55, 22}, // leading 0s (9 chars, clear)
{22, 44, 55, 1, 13, 0, 0, 0, 0}, // trailing 0s (9 chars, clear)
{0, 5, 253, 2, 0}, // leading and trailing zeros
{255, 196, 172, 234, 192, 255}, // big numbers
{255, 196, 172, 1, 234, 192, 255}, // big numbers, two length choices
// others?
}
for i, tc := range cases {
w, err := codec.BytesToWords(tc)
if assert.Nil(err, "%d: %v", i, err) {
b, err := codec.WordsToBytes(w)
if assert.Nil(err, "%d: %v", i, err) {
assert.Equal(len(tc), len(b))
assert.Equal(tc, b)
}
}
}
}
func TestCheckInvalidLists(t *testing.T) {
assert := asrt.New(t)
trivial := []string{"abc", "def"}
short := make([]string, 1234)
long := make([]string, BankSize+1)
right := make([]string, BankSize)
dups := make([]string, BankSize)
for _, list := range [][]string{short, long, right, dups} {
for i := range list {
list[i] = cmn.RandStr(8)
}
}
// create one single duplicate
dups[192] = dups[782]
cases := []struct {
words []string
loadable bool
valid bool
}{
{trivial, false, false},
{short, false, false},
{long, false, false},
{dups, true, false}, // we only check dups on first use...
{right, true, true},
}
for i, tc := range cases {
codec, err := NewCodec(tc.words)
if !tc.loadable {
assert.NotNil(err, "%d", i)
} else if assert.Nil(err, "%d: %+v", i, err) {
data := cmn.RandBytes(32)
w, err := codec.BytesToWords(data)
if tc.valid {
assert.Nil(err, "%d: %+v", i, err)
b, err1 := codec.WordsToBytes(w)
assert.Nil(err1, "%d: %+v", i, err1)
assert.Equal(data, b)
} else {
assert.NotNil(err, "%d", i)
}
}
}
}
func getRandWord(c *WordCodec) string {
idx := cmn.RandInt() % BankSize
return c.words[idx]
}
func getDiffWord(c *WordCodec, not string) string {
w := getRandWord(c)
if w == not {
w = getRandWord(c)
}
return w
}
func TestCheckTypoDetection(t *testing.T) {
assert, require := asrt.New(t), rqr.New(t)
banks := []string{"english", "spanish", "japanese", "chinese_simplified"}
for _, bank := range banks {
codec, err := LoadCodec(bank)
require.Nil(err, "%s: %+v", bank, err)
for i := 0; i < 1000; i++ {
numBytes := cmn.RandInt()%60 + 4
data := cmn.RandBytes(numBytes)
words, err := codec.BytesToWords(data)
assert.Nil(err, "%s: %+v", bank, err)
good, err := codec.WordsToBytes(words)
assert.Nil(err, "%s: %+v", bank, err)
assert.Equal(data, good, bank)
// now try some tweaks...
cut := words[1:]
_, err = codec.WordsToBytes(cut)
assert.NotNil(err, "%s: %s", bank, words)
// swap a word within the bank, should fails
words[3] = getDiffWord(codec, words[3])
_, err = codec.WordsToBytes(words)
assert.NotNil(err, "%s: %s", bank, words)
// put a random word here, must fail
words[3] = cmn.RandStr(10)
_, err = codec.WordsToBytes(words)
assert.NotNil(err, "%s: %s", bank, words)
}
}
}

+ 0
- 68
keys/words/wordcodecbench_test.go View File

@ -1,68 +0,0 @@
package words
import (
"testing"
cmn "github.com/tendermint/tmlibs/common"
)
func warmupCodec(bank string) *WordCodec {
codec, err := LoadCodec(bank)
if err != nil {
panic(err)
}
_, err = codec.GetIndex(codec.words[123])
if err != nil {
panic(err)
}
return codec
}
func BenchmarkCodec(b *testing.B) {
banks := []string{"english", "spanish", "japanese", "chinese_simplified"}
for _, bank := range banks {
b.Run(bank, func(sub *testing.B) {
codec := warmupCodec(bank)
sub.ResetTimer()
benchSuite(sub, codec)
})
}
}
func benchSuite(b *testing.B, codec *WordCodec) {
b.Run("to_words", func(sub *testing.B) {
benchMakeWords(sub, codec)
})
b.Run("to_bytes", func(sub *testing.B) {
benchParseWords(sub, codec)
})
}
func benchMakeWords(b *testing.B, codec *WordCodec) {
numBytes := 32
data := cmn.RandBytes(numBytes)
for i := 1; i <= b.N; i++ {
_, err := codec.BytesToWords(data)
if err != nil {
panic(err)
}
}
}
func benchParseWords(b *testing.B, codec *WordCodec) {
// generate a valid test string to parse
numBytes := 32
data := cmn.RandBytes(numBytes)
words, err := codec.BytesToWords(data)
if err != nil {
panic(err)
}
for i := 1; i <= b.N; i++ {
_, err := codec.WordsToBytes(words)
if err != nil {
panic(err)
}
}
}

+ 0
- 2048
keys/words/wordlist/chinese_simplified.txt
File diff suppressed because it is too large
View File


+ 0
- 2048
keys/words/wordlist/english.txt
File diff suppressed because it is too large
View File


+ 0
- 2048
keys/words/wordlist/japanese.txt
File diff suppressed because it is too large
View File


+ 0
- 2048
keys/words/wordlist/spanish.txt
File diff suppressed because it is too large
View File


+ 0
- 310
keys/words/wordlist/wordlist.go
File diff suppressed because it is too large
View File


Loading…
Cancel
Save