wallet compatibility (#858)

* update master key generations using "Bitcoin seed" as hmac key following BIP32

* use `"mnemonic" + passphrase` as salt for pbkdf2 following BIP39

* test generated addresses and compatibility with BIP44

* check generated public keys

* test children private keys

* update MasterKey to be a constant

* don't export salt and masterKey constants

* use `crypto.FromECDSA` to get the hex representation of the priv key

* use empty pwd for the generation of the BIP39 seed, keeping pwd to encrypt keys

* add comment before the seed generation with empty passphrase
This commit is contained in:
Andrea Franz 2018-05-14 19:13:56 +02:00 committed by GitHub
parent 80be64a1ec
commit 52cdcf8f0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 229 deletions

View File

@ -104,16 +104,18 @@ type ExtendedKey struct {
CachedPubKeyData []byte // (non-serialized) used for memoization of public key (calculated from a private key) CachedPubKeyData []byte // (non-serialized) used for memoization of public key (calculated from a private key)
} }
const masterSecret = "Bitcoin seed"
// NewMaster creates new master node, root of HD chain/tree. // NewMaster creates new master node, root of HD chain/tree.
// Both master and child nodes are of ExtendedKey type, and all the children derive from the root node. // Both master and child nodes are of ExtendedKey type, and all the children derive from the root node.
func NewMaster(seed, salt []byte) (*ExtendedKey, error) { func NewMaster(seed []byte) (*ExtendedKey, error) {
// Ensure seed is within expected limits // Ensure seed is within expected limits
lseed := len(seed) lseed := len(seed)
if lseed < MinSeedBytes || lseed > MaxSeedBytes { if lseed < MinSeedBytes || lseed > MaxSeedBytes {
return nil, ErrInvalidSeedLen return nil, ErrInvalidSeedLen
} }
secretKey, chainCode, err := splitHMAC(seed, salt) secretKey, chainCode, err := splitHMAC(seed, []byte(masterSecret))
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -4,10 +4,12 @@ import (
"bytes" "bytes"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/ethereum/go-ethereum/crypto"
) )
func TestBIP32Vectors(t *testing.T) { func TestBIP32Vectors(t *testing.T) {
@ -114,7 +116,7 @@ tests:
continue continue
} }
extKey, err := NewMaster(seed, []byte("Bitcoin seed")) extKey, err := NewMaster(seed)
if err != nil { if err != nil {
t.Errorf("NewMasterKey #%d (%s): %v", i, test.name, err) t.Errorf("NewMasterKey #%d (%s): %v", i, test.name, err)
continue continue
@ -380,21 +382,21 @@ func TestChildDerivation(t *testing.T) {
func TestErrors(t *testing.T) { func TestErrors(t *testing.T) {
// Should get an error when seed has too few bytes. // Should get an error when seed has too few bytes.
_, err := NewMaster(bytes.Repeat([]byte{0x00}, 15), []byte{0x00}) _, err := NewMaster(bytes.Repeat([]byte{0x00}, 15))
if err != ErrInvalidSeedLen { if err != ErrInvalidSeedLen {
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v", t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
err, ErrInvalidSeedLen) err, ErrInvalidSeedLen)
} }
// Should get an error when seed has too many bytes. // Should get an error when seed has too many bytes.
_, err = NewMaster(bytes.Repeat([]byte{0x00}, 65), []byte{0x00}) _, err = NewMaster(bytes.Repeat([]byte{0x00}, 65))
if err != ErrInvalidSeedLen { if err != ErrInvalidSeedLen {
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v", t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
err, ErrInvalidSeedLen) err, ErrInvalidSeedLen)
} }
// Generate a new key and neuter it to a public extended key. // Generate a new key and neuter it to a public extended key.
mnemonic := NewMnemonic(Salt) mnemonic := NewMnemonic()
phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage) phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage)
if err != nil { if err != nil {
@ -402,7 +404,7 @@ func TestErrors(t *testing.T) {
} }
password := "badpassword" password := "badpassword"
extKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, password), []byte(Salt)) extKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
if err != nil { if err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
return return
@ -533,6 +535,68 @@ func TestBIP44ChildDerivation(t *testing.T) {
t.Logf("Account 1 key: %s", accounKey2.String()) t.Logf("Account 1 key: %s", accounKey2.String())
} }
func TestHDWalletCompatibility(t *testing.T) {
password := "TREZOR"
mnemonic := NewMnemonic()
mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
seed := mnemonic.MnemonicSeed(mnemonicPhrase, password)
rootKey, err := NewMaster(seed)
if err != nil {
t.Errorf("couldn't create master extended key: %v", err)
}
expectedAddresses := []struct {
address string
pubKey string
privKey string
}{
{
address: "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6",
pubKey: "0x03986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f4778",
privKey: "0x62f1d86b246c81bdd8f6c166d56896a4a5e1eddbcaebe06480e5c0bc74c28224",
},
{
address: "0x7AF7283bd1462C3b957e8FAc28Dc19cBbF2FAdfe",
pubKey: "0x03462e7b95dab24fe8a57ac897d9026545ec4327c9c5e4a772e5d14cc5422f9489",
privKey: "0x49ee230b1605382ac1c40079191bca937fc30e8c2fa845b7de27a96ffcc4ddbf",
},
{
address: "0x05f48E30fCb69ADcd2A591Ebc7123be8BE72D7a1",
pubKey: "0x036650e4b2b8e731a0ef12cda892b70cb95e78ea6e576ba995019b5e9aa7d9c0f5",
privKey: "0xeef2c0702151930b84cffcaa642af58e692956314519114e78f3211a6465f28b",
},
{
address: "0xbfE91Bc05cE66013660D7Eb742F74BD324DA5F92",
pubKey: "0x0201d1c12e8fcea03a68ad5fd0d02fd0a4bfe0339618f949e2e30cf311e8b83c46",
privKey: "0xbca51d1d3529a0e0787933a2293cf46d9b973ea3ea00e28d3bd33590bc7f7156",
},
}
for i := 0; i < len(expectedAddresses); i++ {
key, err := rootKey.BIP44Child(CoinTypeETH, uint32(i))
if err != nil {
t.Errorf("Error deriving BIP44-compliant key: %s", err)
}
privateKeyECDSA := key.ToECDSA()
address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey).Hex()
if address != expectedAddresses[i].address {
t.Errorf("wrong address generated. expected %s, got %s", expectedAddresses[i].address, address)
}
pubKey := fmt.Sprintf("0x%x", (crypto.CompressPubkey(&privateKeyECDSA.PublicKey)))
if pubKey != expectedAddresses[i].pubKey {
t.Errorf("wrong public key generated. expected %s, got %s", expectedAddresses[i].pubKey, pubKey)
}
privKey := fmt.Sprintf("0x%x", crypto.FromECDSA(privateKeyECDSA))
if privKey != expectedAddresses[i].privKey {
t.Errorf("wrong private key generated. expected %s, got %s", expectedAddresses[i].privKey, privKey)
}
}
}
//func TestNewKey(t *testing.T) { //func TestNewKey(t *testing.T) {
// mnemonic := NewMnemonic() // mnemonic := NewMnemonic()
// //
@ -543,7 +607,7 @@ func TestBIP44ChildDerivation(t *testing.T) {
// //
// password := "badpassword" // password := "badpassword"
// mnemonic.salt = "Bitcoin seed" // mnemonic.salt = "Bitcoin seed"
// key, err := NewMaster(mnemonic.MnemonicSeed(phrase, password), []byte(mnemonic.salt)) // key, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
// if err != nil { // if err != nil {
// t.Error(err) // t.Error(err)
// } // }

File diff suppressed because one or more lines are too long

View File

@ -13,24 +13,18 @@ type VectorsFile struct {
} }
type Vector struct { type Vector struct {
language, salt, password, input, mnemonic, seed, xprv string language, password, input, mnemonic, seed, xprv string
} }
func TestNewMnemonic(t *testing.T) { func TestNewMnemonic(t *testing.T) {
m1 := NewMnemonic("") m1 := NewMnemonic()
if m1.salt != Salt { if m1.salt != defaultSalt {
t.Errorf("expected default salt, got: %q", m1.salt) t.Errorf("expected default salt, got: %q", m1.salt)
} }
customSalt := "custom-salt"
m2 := NewMnemonic(customSalt)
if m2.salt != customSalt {
t.Errorf("expected %q, got: %q", customSalt, m2.salt)
}
} }
func TestMnemonic_WordList(t *testing.T) { func TestMnemonic_WordList(t *testing.T) {
m := NewMnemonic(Salt) m := NewMnemonic()
_, err := m.WordList(EnglishLanguage) _, err := m.WordList(EnglishLanguage)
if err != nil { if err != nil {
t.Errorf("expected WordList to return WordList without errors, got: %s", err) t.Errorf("expected WordList to return WordList without errors, got: %s", err)
@ -48,7 +42,7 @@ func TestMnemonic_WordList(t *testing.T) {
// TestMnemonicPhrase // TestMnemonicPhrase
func TestMnemonicPhrase(t *testing.T) { func TestMnemonicPhrase(t *testing.T) {
mnemonic := NewMnemonic(Salt) mnemonic := NewMnemonic()
// test strength validation // test strength validation
strengths := []entropyStrength{127, 129, 257} strengths := []entropyStrength{127, 129, 257}
@ -84,13 +78,22 @@ func TestMnemonicPhrase(t *testing.T) {
stats := map[string]int{} stats := map[string]int{}
for _, vector := range vectorsFile.vectors { for _, vector := range vectorsFile.vectors {
stats[vector.language]++ stats[vector.language]++
mnemonic := NewMnemonic(vector.salt) mnemonic := NewMnemonic()
seed := mnemonic.MnemonicSeed(vector.mnemonic, vector.password) seed := mnemonic.MnemonicSeed(vector.mnemonic, vector.password)
if fmt.Sprintf("%x", seed) != vector.seed { if fmt.Sprintf("%x", seed) != vector.seed {
t.Errorf("Test failed (%s): incorrect seed (%x) generated (expected: %s)", vector.language, seed, vector.seed) t.Errorf("Test failed (%s): incorrect seed (%x) generated (expected: %s)", vector.language, seed, vector.seed)
return return
} }
//t.Logf("Test passed: correct seed (%x) generated (expected: %s)", seed, vector.seed) //t.Logf("Test passed: correct seed (%x) generated (expected: %s)", seed, vector.seed)
rootKey, err := NewMaster(seed)
if err != nil {
t.Error(err)
}
if rootKey.String() != vector.xprv {
t.Errorf("Test failed (%s): incorrect xprv (%s) generated (expected: %s)", vector.language, rootKey, vector.xprv)
}
} }
for language, count := range stats { for language, count := range stats {
t.Logf("[%s]: %d tests completed", language, count) t.Logf("[%s]: %d tests completed", language, count)
@ -111,7 +114,7 @@ func LoadVectorsFile(path string) (*VectorsFile, error) {
// load data into Vector structs // load data into Vector structs
for language, data := range vectorsFile.Data { for language, data := range vectorsFile.Data {
for _, item := range data { for _, item := range data {
vectorsFile.vectors = append(vectorsFile.vectors, &Vector{language, item[0], item[1], item[2], item[3], item[4], item[5]}) vectorsFile.vectors = append(vectorsFile.vectors, &Vector{language, item[0], item[1], item[2], item[3], item[4]})
} }
} }
@ -119,6 +122,6 @@ func LoadVectorsFile(path string) (*VectorsFile, error) {
} }
func (v *Vector) String() string { func (v *Vector) String() string {
return fmt.Sprintf("{salt: %s, password: %s, input: %s, mnemonic: %s, seed: %s, xprv: %s}", return fmt.Sprintf("{password: %s, input: %s, mnemonic: %s, seed: %s, xprv: %s}",
v.salt, v.password, v.input, v.mnemonic, v.seed, v.xprv) v.password, v.input, v.mnemonic, v.seed, v.xprv)
} }

File diff suppressed because it is too large Load Diff

View File

@ -51,14 +51,18 @@ func NewManager(geth GethServiceProvider) *Manager {
// sub-account derivations) // sub-account derivations)
func (m *Manager) CreateAccount(password string) (address, pubKey, mnemonic string, err error) { func (m *Manager) CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
// generate mnemonic phrase // generate mnemonic phrase
mn := extkeys.NewMnemonic(extkeys.Salt) mn := extkeys.NewMnemonic()
mnemonic, err = mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage) mnemonic, err = mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
if err != nil { if err != nil {
return "", "", "", fmt.Errorf("can not create mnemonic seed: %v", err) return "", "", "", fmt.Errorf("can not create mnemonic seed: %v", err)
} }
// generate extended master key (see BIP32) // Generate extended master key (see BIP32)
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) // We call extkeys.NewMaster with a seed generated with the 12 mnemonic words
// but without using the optional password as an extra entropy as described in BIP39.
// Future ideas/iterations in Status can add an an advanced options
// for expert users, to be able to add a passphrase to the generation of the seed.
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
if err != nil { if err != nil {
return "", "", "", fmt.Errorf("can not create master extended key: %v", err) return "", "", "", fmt.Errorf("can not create master extended key: %v", err)
} }
@ -136,8 +140,8 @@ func (m *Manager) CreateChildAccount(parentAddress, password string) (address, p
// Once master key is re-generated, it is inserted into keystore (if not already there). // Once master key is re-generated, it is inserted into keystore (if not already there).
func (m *Manager) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) { func (m *Manager) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
// re-create extended key (see BIP32) // re-create extended key (see BIP32)
mn := extkeys.NewMnemonic(extkeys.Salt) mn := extkeys.NewMnemonic()
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
if err != nil { if err != nil {
return "", "", ErrInvalidMasterKeyCreated return "", "", ErrInvalidMasterKeyCreated
} }