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:
parent
80be64a1ec
commit
52cdcf8f0f
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
@ -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
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue