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)
|
||||
}
|
||||
|
||||
const masterSecret = "Bitcoin seed"
|
||||
|
||||
// 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.
|
||||
func NewMaster(seed, salt []byte) (*ExtendedKey, error) {
|
||||
func NewMaster(seed []byte) (*ExtendedKey, error) {
|
||||
// Ensure seed is within expected limits
|
||||
lseed := len(seed)
|
||||
if lseed < MinSeedBytes || lseed > MaxSeedBytes {
|
||||
return nil, ErrInvalidSeedLen
|
||||
}
|
||||
|
||||
secretKey, chainCode, err := splitHMAC(seed, salt)
|
||||
secretKey, chainCode, err := splitHMAC(seed, []byte(masterSecret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -4,10 +4,12 @@ import (
|
|||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
)
|
||||
|
||||
func TestBIP32Vectors(t *testing.T) {
|
||||
|
@ -114,7 +116,7 @@ tests:
|
|||
continue
|
||||
}
|
||||
|
||||
extKey, err := NewMaster(seed, []byte("Bitcoin seed"))
|
||||
extKey, err := NewMaster(seed)
|
||||
if err != nil {
|
||||
t.Errorf("NewMasterKey #%d (%s): %v", i, test.name, err)
|
||||
continue
|
||||
|
@ -380,21 +382,21 @@ func TestChildDerivation(t *testing.T) {
|
|||
|
||||
func TestErrors(t *testing.T) {
|
||||
// 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 {
|
||||
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
|
||||
err, ErrInvalidSeedLen)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
t.Errorf("NewMaster: mismatched error -- got: %v, want: %v",
|
||||
err, ErrInvalidSeedLen)
|
||||
}
|
||||
|
||||
// Generate a new key and neuter it to a public extended key.
|
||||
mnemonic := NewMnemonic(Salt)
|
||||
mnemonic := NewMnemonic()
|
||||
|
||||
phrase, err := mnemonic.MnemonicPhrase(128, EnglishLanguage)
|
||||
if err != nil {
|
||||
|
@ -402,7 +404,7 @@ func TestErrors(t *testing.T) {
|
|||
}
|
||||
|
||||
password := "badpassword"
|
||||
extKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, password), []byte(Salt))
|
||||
extKey, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
|
@ -533,6 +535,68 @@ func TestBIP44ChildDerivation(t *testing.T) {
|
|||
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) {
|
||||
// mnemonic := NewMnemonic()
|
||||
//
|
||||
|
@ -543,7 +607,7 @@ func TestBIP44ChildDerivation(t *testing.T) {
|
|||
//
|
||||
// password := "badpassword"
|
||||
// mnemonic.salt = "Bitcoin seed"
|
||||
// key, err := NewMaster(mnemonic.MnemonicSeed(phrase, password), []byte(mnemonic.salt))
|
||||
// key, err := NewMaster(mnemonic.MnemonicSeed(phrase, password))
|
||||
// if err != nil {
|
||||
// t.Error(err)
|
||||
// }
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -13,24 +13,18 @@ type VectorsFile struct {
|
|||
}
|
||||
|
||||
type Vector struct {
|
||||
language, salt, password, input, mnemonic, seed, xprv string
|
||||
language, password, input, mnemonic, seed, xprv string
|
||||
}
|
||||
|
||||
func TestNewMnemonic(t *testing.T) {
|
||||
m1 := NewMnemonic("")
|
||||
if m1.salt != Salt {
|
||||
m1 := NewMnemonic()
|
||||
if m1.salt != defaultSalt {
|
||||
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) {
|
||||
m := NewMnemonic(Salt)
|
||||
m := NewMnemonic()
|
||||
_, err := m.WordList(EnglishLanguage)
|
||||
if err != nil {
|
||||
t.Errorf("expected WordList to return WordList without errors, got: %s", err)
|
||||
|
@ -48,7 +42,7 @@ func TestMnemonic_WordList(t *testing.T) {
|
|||
// TestMnemonicPhrase
|
||||
func TestMnemonicPhrase(t *testing.T) {
|
||||
|
||||
mnemonic := NewMnemonic(Salt)
|
||||
mnemonic := NewMnemonic()
|
||||
|
||||
// test strength validation
|
||||
strengths := []entropyStrength{127, 129, 257}
|
||||
|
@ -84,13 +78,22 @@ func TestMnemonicPhrase(t *testing.T) {
|
|||
stats := map[string]int{}
|
||||
for _, vector := range vectorsFile.vectors {
|
||||
stats[vector.language]++
|
||||
mnemonic := NewMnemonic(vector.salt)
|
||||
mnemonic := NewMnemonic()
|
||||
seed := mnemonic.MnemonicSeed(vector.mnemonic, vector.password)
|
||||
if fmt.Sprintf("%x", seed) != vector.seed {
|
||||
t.Errorf("Test failed (%s): incorrect seed (%x) generated (expected: %s)", vector.language, seed, vector.seed)
|
||||
return
|
||||
}
|
||||
//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 {
|
||||
t.Logf("[%s]: %d tests completed", language, count)
|
||||
|
@ -111,7 +114,7 @@ func LoadVectorsFile(path string) (*VectorsFile, error) {
|
|||
// load data into Vector structs
|
||||
for language, data := range vectorsFile.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 {
|
||||
return fmt.Sprintf("{salt: %s, password: %s, input: %s, mnemonic: %s, seed: %s, xprv: %s}",
|
||||
v.salt, v.password, v.input, v.mnemonic, v.seed, v.xprv)
|
||||
return fmt.Sprintf("{password: %s, input: %s, mnemonic: %s, seed: %s, xprv: %s}",
|
||||
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)
|
||||
func (m *Manager) CreateAccount(password string) (address, pubKey, mnemonic string, err error) {
|
||||
// generate mnemonic phrase
|
||||
mn := extkeys.NewMnemonic(extkeys.Salt)
|
||||
mn := extkeys.NewMnemonic()
|
||||
mnemonic, err = mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
|
||||
if err != nil {
|
||||
return "", "", "", fmt.Errorf("can not create mnemonic seed: %v", err)
|
||||
}
|
||||
|
||||
// generate extended master key (see BIP32)
|
||||
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
|
||||
// Generate extended master key (see BIP32)
|
||||
// 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 {
|
||||
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).
|
||||
func (m *Manager) RecoverAccount(password, mnemonic string) (address, pubKey string, err error) {
|
||||
// re-create extended key (see BIP32)
|
||||
mn := extkeys.NewMnemonic(extkeys.Salt)
|
||||
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt))
|
||||
mn := extkeys.NewMnemonic()
|
||||
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
|
||||
if err != nil {
|
||||
return "", "", ErrInvalidMasterKeyCreated
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue