Implement walletpassphrasechange RPC call.

Closes .
This commit is contained in:
Josh Rickmar 2014-01-27 09:30:42 -05:00
parent 3f6133e44b
commit 6ad3f8786e
9 changed files with 398 additions and 51 deletions

View file

@ -130,7 +130,7 @@ func (a *Account) Lock() error {
} }
// Unlock unlocks the underlying wallet for an account. // Unlock unlocks the underlying wallet for an account.
func (a *Account) Unlock(passphrase []byte, timeout int64) error { func (a *Account) Unlock(passphrase []byte) error {
a.mtx.Lock() a.mtx.Lock()
defer a.mtx.Unlock() defer a.mtx.Unlock()

View file

@ -226,6 +226,75 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [
return nil return nil
} }
// ChangePassphrase unlocks all account wallets with the old
// passphrase, and re-encrypts each using the new passphrase.
func (store *AccountStore) ChangePassphrase(old, new []byte) error {
store.RLock()
defer store.RUnlock()
// Check that each account can be unlocked with the old passphrase.
// Each's account's wallet mutex is unlocked with a defer so they
// will be held for the duration of this function. This prevents
// a wallet from being locked after some timeout after a RPC call
// to walletpassphrase.
for _, a := range store.accounts {
a.mtx.Lock()
defer a.mtx.Unlock()
if locked := a.Wallet.IsLocked(); !locked {
if err := a.Wallet.Lock(); err != nil {
return err
}
}
if err := a.Wallet.Unlock(old); err != nil {
return err
}
defer a.Wallet.Lock()
}
// Change passphrase for each unlocked wallet.
for _, a := range store.accounts {
if err := a.Wallet.ChangePassphrase(new); err != nil {
return err
}
a.dirty = true
}
// Immediately write out to disk. Create a new temporary network
// directory to write to, write all account files there, then move
// to the real network directory. This provides an safe
// replacement of all account files and ensures that all wallets
// are using either the old or new passphrase, but never two wallets
// with different passphrases.
netDir := networkDir(cfg.Net())
tmpNetDir := tmpNetworkDir(cfg.Net())
for _, a := range store.accounts {
// Writer locks must be held for the tx and utxo stores as well,
// to unset the dirty flag.
a.UtxoStore.Lock()
defer a.UtxoStore.Unlock()
a.TxStore.Lock()
defer a.TxStore.Unlock()
if err := a.writeAllToFreshDir(tmpNetDir); err != nil {
return err
}
}
// This is technically NOT an atomic operation, but at startup, if the
// network directory is missing but the temporary network directory
// exists, the temporary is moved before accounts are opened.
if err := os.RemoveAll(netDir); err != nil {
return err
}
if err := Rename(tmpNetDir, netDir); err != nil {
return err
}
return nil
}
// DumpKeys returns all WIF-encoded private keys associated with all // DumpKeys returns all WIF-encoded private keys associated with all
// accounts. All wallets must be unlocked for this operation to succeed. // accounts. All wallets must be unlocked for this operation to succeed.
func (store *AccountStore) DumpKeys() ([]string, error) { func (store *AccountStore) DumpKeys() ([]string, error) {

17
cmd.go
View file

@ -248,6 +248,21 @@ func main() {
// OpenAccounts attempts to open all saved accounts. // OpenAccounts attempts to open all saved accounts.
func OpenAccounts() { func OpenAccounts() {
// If the network (account) directory is missing, but the temporary
// directory exists, move it. This is unlikely to happen, but possible,
// if writing out every account file at once to a tmp directory (as is
// done for changing a wallet passphrase) and btcwallet closes after
// removing the network directory but before renaming the temporary
// directory.
netDir := networkDir(cfg.Net())
tmpNetDir := tmpNetworkDir(cfg.Net())
if !fileExists(netDir) && fileExists(tmpNetDir) {
if err := Rename(tmpNetDir, netDir); err != nil {
log.Errorf("Cannot move temporary network dir: %v", err)
return
}
}
// The default account must exist, or btcwallet acts as if no // The default account must exist, or btcwallet acts as if no
// wallets/accounts have been created yet. // wallets/accounts have been created yet.
if err := accountstore.OpenAccount("", cfg); err != nil { if err := accountstore.OpenAccount("", cfg); err != nil {
@ -264,7 +279,7 @@ func OpenAccounts() {
// Read all filenames in the account directory, and look for any // Read all filenames in the account directory, and look for any
// filenames matching '*-wallet.bin'. These are wallets for // filenames matching '*-wallet.bin'. These are wallets for
// additional saved accounts. // additional saved accounts.
accountDir, err := os.Open(networkDir(cfg.Net())) accountDir, err := os.Open(netDir)
if err != nil { if err != nil {
// Can't continue. // Can't continue.
log.Errorf("Unable to open account directory: %v", err) log.Errorf("Unable to open account directory: %v", err)

119
cmdmgr.go
View file

@ -32,51 +32,51 @@ type cmdHandler func(btcjson.Cmd) (interface{}, *btcjson.Error)
var rpcHandlers = map[string]cmdHandler{ var rpcHandlers = map[string]cmdHandler{
// Standard bitcoind methods (implemented) // Standard bitcoind methods (implemented)
"dumpprivkey": DumpPrivKey, "dumpprivkey": DumpPrivKey,
"getaccount": GetAccount, "getaccount": GetAccount,
"getaccountaddress": GetAccountAddress, "getaccountaddress": GetAccountAddress,
"getaddressesbyaccount": GetAddressesByAccount, "getaddressesbyaccount": GetAddressesByAccount,
"getbalance": GetBalance, "getbalance": GetBalance,
"getnewaddress": GetNewAddress, "getnewaddress": GetNewAddress,
"importprivkey": ImportPrivKey, "importprivkey": ImportPrivKey,
"keypoolrefill": KeypoolRefill, "keypoolrefill": KeypoolRefill,
"listaccounts": ListAccounts, "listaccounts": ListAccounts,
"listtransactions": ListTransactions, "listtransactions": ListTransactions,
"sendfrom": SendFrom, "sendfrom": SendFrom,
"sendmany": SendMany, "sendmany": SendMany,
"settxfee": SetTxFee, "settxfee": SetTxFee,
"walletlock": WalletLock, "walletlock": WalletLock,
"walletpassphrase": WalletPassphrase, "walletpassphrase": WalletPassphrase,
"walletpassphrasechange": WalletPassphraseChange,
// Standard bitcoind methods (currently unimplemented) // Standard bitcoind methods (currently unimplemented)
"addmultisigaddress": Unimplemented, "addmultisigaddress": Unimplemented,
"backupwallet": Unimplemented, "backupwallet": Unimplemented,
"createmultisig": Unimplemented, "createmultisig": Unimplemented,
"dumpwallet": Unimplemented, "dumpwallet": Unimplemented,
"getblocktemplate": Unimplemented, "getblocktemplate": Unimplemented,
"getrawchangeaddress": Unimplemented, "getrawchangeaddress": Unimplemented,
"getreceivedbyaccount": Unimplemented, "getreceivedbyaccount": Unimplemented,
"getreceivedbyaddress": Unimplemented, "getreceivedbyaddress": Unimplemented,
"gettransaction": Unimplemented, "gettransaction": Unimplemented,
"gettxout": Unimplemented, "gettxout": Unimplemented,
"gettxoutsetinfo": Unimplemented, "gettxoutsetinfo": Unimplemented,
"getwork": Unimplemented, "getwork": Unimplemented,
"importwallet": Unimplemented, "importwallet": Unimplemented,
"listaddressgroupings": Unimplemented, "listaddressgroupings": Unimplemented,
"listlockunspent": Unimplemented, "listlockunspent": Unimplemented,
"listreceivedbyaccount": Unimplemented, "listreceivedbyaccount": Unimplemented,
"listsinceblock": Unimplemented, "listsinceblock": Unimplemented,
"listreceivedbyaddress": Unimplemented, "listreceivedbyaddress": Unimplemented,
"listunspent": Unimplemented, "listunspent": Unimplemented,
"lockunspent": Unimplemented, "lockunspent": Unimplemented,
"move": Unimplemented, "move": Unimplemented,
"sendtoaddress": Unimplemented, "sendtoaddress": Unimplemented,
"setaccount": Unimplemented, "setaccount": Unimplemented,
"signmessage": Unimplemented, "signmessage": Unimplemented,
"signrawtransaction": Unimplemented, "signrawtransaction": Unimplemented,
"validateaddress": Unimplemented, "validateaddress": Unimplemented,
"verifymessage": Unimplemented, "verifymessage": Unimplemented,
"walletpassphrasechange": Unimplemented,
// Standard bitcoind methods which won't be implemented by btcwallet. // Standard bitcoind methods which won't be implemented by btcwallet.
"encryptwallet": Unsupported, "encryptwallet": Unsupported,
@ -1277,7 +1277,7 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &e return nil, &e
} }
switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err { switch err := a.Unlock([]byte(cmd.Passphrase)); err {
case nil: case nil:
go func(timeout int64) { go func(timeout int64) {
time.Sleep(time.Second * time.Duration(timeout)) time.Sleep(time.Second * time.Duration(timeout))
@ -1293,6 +1293,37 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
} }
} }
// WalletPassphraseChange responds to the walletpassphrasechange request
// by unlocking all accounts with the provided old passphrase, and
// re-encrypting each private key with an AES key derived from the new
// passphrase.
//
// If the old passphrase is correct and the passphrase is changed, all
// wallets will be immediately locked.
func WalletPassphraseChange(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
cmd, ok := icmd.(*btcjson.WalletPassphraseChangeCmd)
if !ok {
return nil, &btcjson.ErrInternal
}
err := accountstore.ChangePassphrase([]byte(cmd.OldPassphrase),
[]byte(cmd.NewPassphrase))
switch err {
case nil:
return nil, nil
case wallet.ErrWrongPassphrase:
return nil, &btcjson.ErrWalletPassphraseIncorrect
default: // all other non-nil errors
e := btcjson.Error{
Code: btcjson.ErrWallet.Code,
Message: err.Error(),
}
return nil, &e
}
}
// AccountNtfn is a struct for marshalling any generic notification // AccountNtfn is a struct for marshalling any generic notification
// about a account for a wallet frontend. // about a account for a wallet frontend.
// //

View file

@ -36,8 +36,8 @@ var (
} }
) )
// networkDir returns the base directory name for the bitcoin network // networkDir returns the directory name of a network directory to hold account
// net. // files.
func networkDir(net btcwire.BitcoinNet) string { func networkDir(net btcwire.BitcoinNet) string {
var netname string var netname string
if net == btcwire.MainNet { if net == btcwire.MainNet {
@ -48,6 +48,11 @@ func networkDir(net btcwire.BitcoinNet) string {
return filepath.Join(cfg.DataDir, netname) return filepath.Join(cfg.DataDir, netname)
} }
// tmpNetworkDir returns the temporary directory name for a given network.
func tmpNetworkDir(net btcwire.BitcoinNet) string {
return networkDir(net) + "_tmp"
}
// checkCreateDir checks that the path exists and is a directory. // checkCreateDir checks that the path exists and is a directory.
// If path does not exist, it is created. // If path does not exist, it is created.
func checkCreateDir(path string) error { func checkCreateDir(path string) error {
@ -109,6 +114,82 @@ func DirtyAccountSyncer() {
} }
} }
// freshDir creates a new directory specified by path if it does not
// exist. If the directory already exists, all files contained in the
// directory are removed.
func freshDir(path string) error {
if err := checkCreateDir(path); err != nil {
return err
}
// Remove all files in the directory.
fd, err := os.Open(path)
if err != nil {
return err
}
defer fd.Close()
names, err := fd.Readdirnames(0)
if err != nil {
return err
}
for _, name := range names {
if err := os.RemoveAll(name); err != nil {
return err
}
}
return nil
}
// writeAllToFreshDir writes all account files to the specified directory.
// If dir already exists, any old files are removed. If dir does not
// exist, it is created.
//
// It is a runtime error to call this function while not holding each
// wallet, tx store, and utxo store writer lock.
func (a *Account) writeAllToFreshDir(dir string) error {
if err := freshDir(dir); err != nil {
return err
}
wfilepath := accountFilename("wallet.bin", a.name, dir)
txfilepath := accountFilename("tx.bin", a.name, dir)
utxofilepath := accountFilename("utxo.bin", a.name, dir)
wfile, err := os.Create(wfilepath)
if err != nil {
return err
}
defer wfile.Close()
txfile, err := os.Create(txfilepath)
if err != nil {
return err
}
defer txfile.Close()
utxofile, err := os.Create(utxofilepath)
if err != nil {
return err
}
defer utxofile.Close()
if _, err := a.Wallet.WriteTo(wfile); err != nil {
return err
}
a.dirty = false
if _, err := a.TxStore.s.WriteTo(txfile); err != nil {
return err
}
a.TxStore.dirty = false
if _, err := a.UtxoStore.s.WriteTo(utxofile); err != nil {
return err
}
a.UtxoStore.dirty = false
return nil
}
// writeDirtyToDisk checks for the dirty flag on an account's wallet, // writeDirtyToDisk checks for the dirty flag on an account's wallet,
// txstore, and utxostore, writing them to disk if any are dirty. // txstore, and utxostore, writing them to disk if any are dirty.
func (a *Account) writeDirtyToDisk() error { func (a *Account) writeDirtyToDisk() error {

View file

@ -109,6 +109,7 @@ type Tx interface {
ReadFromVersion(uint32, io.Reader) (int64, error) ReadFromVersion(uint32, io.Reader) (int64, error)
TxInfo(string, int32, btcwire.BitcoinNet) []map[string]interface{} TxInfo(string, int32, btcwire.BitcoinNet) []map[string]interface{}
} }
// TxStore is a slice holding RecvTx and SendTx pointers. // TxStore is a slice holding RecvTx and SendTx pointers.
type TxStore []Tx type TxStore []Tx

View file

@ -75,6 +75,22 @@ func updateOldFileLocations() {
os.Exit(1) os.Exit(1)
} }
acctsExist := false
for i := range fi {
// Ignore non-directories.
if !fi[i].IsDir() {
continue
}
if strings.HasPrefix(fi[i].Name(), "btcwallet") {
acctsExist = true
break
}
}
if !acctsExist {
return
}
// Create testnet directory, if it doesn't already exist. // Create testnet directory, if it doesn't already exist.
netdir := filepath.Join(cfg.DataDir, "testnet") netdir := filepath.Join(cfg.DataDir, "testnet")
if err := checkCreateDir(netdir); err != nil { if err := checkCreateDir(netdir); err != nil {

View file

@ -61,6 +61,7 @@ var (
ErrWalletDoesNotExist = errors.New("non-existant wallet") ErrWalletDoesNotExist = errors.New("non-existant wallet")
ErrWalletIsWatchingOnly = errors.New("wallet is watching-only") ErrWalletIsWatchingOnly = errors.New("wallet is watching-only")
ErrWalletLocked = errors.New("wallet is locked") ErrWalletLocked = errors.New("wallet is locked")
ErrWrongPassphrase = errors.New("wrong passphrase")
) )
var ( var (
@ -845,6 +846,37 @@ func (w *Wallet) Passphrase() ([]byte, error) {
return nil, ErrWalletLocked return nil, ErrWalletLocked
} }
// ChangePassphrase creates a new AES key from a new passphrase and
// re-encrypts all encrypted private keys with the new key.
func (w *Wallet) ChangePassphrase(new []byte) error {
if w.flags.watchingOnly {
return ErrWalletIsWatchingOnly
}
if len(w.secret) != 32 {
return ErrWalletLocked
}
oldkey := w.secret
newkey := Key(new, &w.kdfParams)
for _, a := range w.addrMap {
if err := a.changeEncryptionKey(oldkey, newkey); err != nil {
return err
}
}
// zero old secrets.
zero(w.passphrase)
zero(w.secret)
// Save new secrets.
w.passphrase = new
w.secret = newkey
return nil
}
func zero(b []byte) { func zero(b []byte) {
for i := range b { for i := range b {
b[i] = 0 b[i] = 0
@ -2151,7 +2183,7 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
} }
x, y := btcec.S256().ScalarBaseMult(privkey) x, y := btcec.S256().ScalarBaseMult(privkey)
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 { if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
return nil, errors.New("decryption failed") return nil, ErrWrongPassphrase
} }
privkeyCopy := make([]byte, 32) privkeyCopy := make([]byte, 32)
@ -2160,9 +2192,36 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
return privkeyCopy, nil return privkeyCopy, nil
} }
// TODO(jrick) // changeEncryptionKey re-encrypts the private keys for an address
// with a new AES encryption key. oldkey must be the old AES encryption key
// and is used to decrypt the private key.
func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error { func (a *btcAddress) changeEncryptionKey(oldkey, newkey []byte) error {
return errors.New("unimplemented") // Address must have a private key and be encrypted to continue.
if !a.flags.hasPrivKey {
return errors.New("no private key")
}
if !a.flags.encrypted {
return errors.New("address is not encrypted")
}
privKeyCT, err := a.unlock(oldkey)
if err != nil {
return err
}
aesBlockEncrypter, err := aes.NewCipher(newkey)
if err != nil {
return err
}
newIV := make([]byte, len(a.initVector))
if _, err := rand.Read(newIV); err != nil {
return err
}
copy(a.initVector[:], newIV)
aesEncrypter := cipher.NewCFBEncrypter(aesBlockEncrypter, a.initVector[:])
aesEncrypter.XORKeyStream(a.privKey[:], privKeyCT)
return nil
} }
// address returns a btcutil.AddressPubKeyHash for a btcAddress. // address returns a btcutil.AddressPubKeyHash for a btcAddress.

View file

@ -667,3 +667,78 @@ func TestWatchingWalletExport(t *testing.T) {
return return
} }
} }
func TestChangePassphrase(t *testing.T) {
const keypoolSize = 10
createdAt := &BlockStamp{}
w, err := NewWallet("banana wallet", "A wallet for testing.",
[]byte("banana"), btcwire.MainNet, createdAt, keypoolSize)
if err != nil {
t.Error("Error creating new wallet: " + err.Error())
return
}
// Changing the passphrase with a locked wallet must fail with ErrWalletLocked.
if err := w.ChangePassphrase([]byte("potato")); err != ErrWalletLocked {
t.Errorf("Changing passphrase on a locked wallet did not fail correctly: %v", err)
return
}
// Unlock wallet so the passphrase can be changed.
if err := w.Unlock([]byte("banana")); err != nil {
t.Errorf("Cannot unlock: %v", err)
return
}
// Get root address and its private key. This is compared to the private
// key post passphrase change.
rootAddr := w.LastChainedAddress()
rootPrivKey, err := w.AddressKey(rootAddr)
if err != nil {
t.Errorf("Cannot get root address' private key: %v", err)
return
}
// Change passphrase.
if err := w.ChangePassphrase([]byte("potato")); err != nil {
t.Errorf("Changing passhprase failed: %v", err)
return
}
// Wallet should still be unlocked.
if w.IsLocked() {
t.Errorf("Wallet should be unlocked after passphrase change.")
return
}
// Lock it.
if err := w.Lock(); err != nil {
t.Errorf("Cannot lock wallet after passphrase change: %v", err)
return
}
// Unlock with old passphrase. This must fail with ErrWrongPassphrase.
if err := w.Unlock([]byte("banana")); err != ErrWrongPassphrase {
t.Errorf("Unlocking with old passphrases did not fail correctly: %v", err)
return
}
// Unlock with new passphrase. This must succeed.
if err := w.Unlock([]byte("potato")); err != nil {
t.Errorf("Unlocking with new passphrase failed: %v", err)
return
}
// Get root address' private key again.
rootPrivKey2, err := w.AddressKey(rootAddr)
if err != nil {
t.Errorf("Cannot get root address' private key after passphrase change: %v", err)
return
}
// Private keys must match.
if !reflect.DeepEqual(rootPrivKey, rootPrivKey2) {
t.Errorf("Private keys before and after unlock differ.")
return
}
}