diff --git a/hdkeychain/extendedkey.go b/hdkeychain/extendedkey.go index 648ee3e..c0e3e2b 100644 --- a/hdkeychain/extendedkey.go +++ b/hdkeychain/extendedkey.go @@ -360,6 +360,10 @@ func (k *ExtendedKey) Address(net *btcnet.Params) (*btcutil.AddressPubKeyHash, e // String returns the extended key as a human-readable base58-encoded string. func (k *ExtendedKey) String() string { + if len(k.key) == 0 { + return "zeroed extended key" + } + var childNumBytes [4]byte depthByte := byte(k.depth % 256) binary.BigEndian.PutUint32(childNumBytes[:], k.childNum) @@ -402,6 +406,31 @@ func (k *ExtendedKey) SetNet(net *btcnet.Params) { } } +// zero sets all bytes in the passed slice to zero. This is used to +// explicitly clear private key material from memory. +func zero(b []byte) { + lenb := len(b) + for i := 0; i < lenb; i++ { + b[i] = 0 + } +} + +// Zero manually clears all fields and bytes in the extended key. This can be +// used to explicitly clear key material from memory for enhanced security +// against memory scraping. This function only clears this particular key and +// not any children that have already been derived. +func (k *ExtendedKey) Zero() { + zero(k.key) + zero(k.pubKey) + zero(k.chainCode) + zero(k.parentFP) + zero(k.version) + k.key = nil + k.depth = 0 + k.childNum = 0 + k.isPrivate = false +} + // NewMaster creates a new master node for use in creating a hierarchical // deterministic key chain. The seed must be between 128 and 512 bits and // should be generated by a cryptographically secure random generation source. diff --git a/hdkeychain/extendedkey_test.go b/hdkeychain/extendedkey_test.go index ab0cff4..5527c65 100644 --- a/hdkeychain/extendedkey_test.go +++ b/hdkeychain/extendedkey_test.go @@ -674,3 +674,82 @@ func TestErrors(t *testing.T) { } } } + +// TestZero ensures that zeroing an extended key works as intended. +func TestZero(t *testing.T) { + tests := []struct { + name string + extKey string + }{ + // Test vector 1 + { + name: "test vector 1 chain m", + extKey: "xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi", + }, + } + + for i, test := range tests { + key, err := hdkeychain.NewKeyFromString(test.extKey) + if err != nil { + t.Errorf("NewKeyFromString #%d (%s): unexpected "+ + "error: %v", i, test.name, err) + continue + } + key.Zero() + + // Zeroing a key should result in it no longer being private + if key.IsPrivate() != false { + t.Errorf("IsPrivate #%d (%s): mismatched key type -- "+ + "want private %v, got private %v", i, test.name, + false, key.IsPrivate()) + continue + } + + parentFP := key.ParentFingerprint() + if parentFP != 0 { + t.Errorf("ParentFingerprint #%d (%s): mismatched "+ + "parent fingerprint -- want %d, got %d", i, + test.name, 0, parentFP) + continue + } + + wantKey := "zeroed extended key" + serializedKey := key.String() + if serializedKey != wantKey { + t.Errorf("String #%d (%s): mismatched serialized key "+ + "-- want %s, got %s", i, test.name, wantKey, + serializedKey) + continue + } + + wantErr := hdkeychain.ErrNotPrivExtKey + _, err = key.ECPrivKey() + if !reflect.DeepEqual(err, wantErr) { + t.Errorf("ECPrivKey #%d (%s): mismatched error: want "+ + "%v, got %v", i, test.name, wantErr, err) + continue + } + + wantErr = errors.New("pubkey string is empty") + _, err = key.ECPubKey() + if !reflect.DeepEqual(err, wantErr) { + t.Errorf("ECPubKey #%d (%s): mismatched error: want "+ + "%v, got %v", i, test.name, wantErr, err) + continue + } + + wantAddr := "1HT7xU2Ngenf7D4yocz2SAcnNLW7rK8d4E" + addr, err := key.Address(&btcnet.MainNetParams) + if err != nil { + t.Errorf("Addres s #%d (%s): unexpected error: %v", i, + test.name, err) + continue + } + if addr.EncodeAddress() != wantAddr { + t.Errorf("Address #%d (%s): mismatched address -- want "+ + "%s, got %s", i, test.name, wantAddr, + addr.EncodeAddress()) + continue + } + } +}