Implement walletpassphrasechange RPC call.

Closes #62.
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.
func (a *Account) Unlock(passphrase []byte, timeout int64) error {
func (a *Account) Unlock(passphrase []byte) error {
a.mtx.Lock()
defer a.mtx.Unlock()

View file

@ -226,6 +226,75 @@ func (store *AccountStore) CreateEncryptedWallet(name, desc string, passphrase [
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
// accounts. All wallets must be unlocked for this operation to succeed.
func (store *AccountStore) DumpKeys() ([]string, error) {

17
cmd.go
View file

@ -248,6 +248,21 @@ func main() {
// OpenAccounts attempts to open all saved accounts.
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
// wallets/accounts have been created yet.
if err := accountstore.OpenAccount("", cfg); err != nil {
@ -264,7 +279,7 @@ func OpenAccounts() {
// Read all filenames in the account directory, and look for any
// filenames matching '*-wallet.bin'. These are wallets for
// additional saved accounts.
accountDir, err := os.Open(networkDir(cfg.Net()))
accountDir, err := os.Open(netDir)
if err != nil {
// Can't continue.
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{
// Standard bitcoind methods (implemented)
"dumpprivkey": DumpPrivKey,
"getaccount": GetAccount,
"getaccountaddress": GetAccountAddress,
"getaddressesbyaccount": GetAddressesByAccount,
"getbalance": GetBalance,
"getnewaddress": GetNewAddress,
"importprivkey": ImportPrivKey,
"keypoolrefill": KeypoolRefill,
"listaccounts": ListAccounts,
"listtransactions": ListTransactions,
"sendfrom": SendFrom,
"sendmany": SendMany,
"settxfee": SetTxFee,
"walletlock": WalletLock,
"walletpassphrase": WalletPassphrase,
"dumpprivkey": DumpPrivKey,
"getaccount": GetAccount,
"getaccountaddress": GetAccountAddress,
"getaddressesbyaccount": GetAddressesByAccount,
"getbalance": GetBalance,
"getnewaddress": GetNewAddress,
"importprivkey": ImportPrivKey,
"keypoolrefill": KeypoolRefill,
"listaccounts": ListAccounts,
"listtransactions": ListTransactions,
"sendfrom": SendFrom,
"sendmany": SendMany,
"settxfee": SetTxFee,
"walletlock": WalletLock,
"walletpassphrase": WalletPassphrase,
"walletpassphrasechange": WalletPassphraseChange,
// Standard bitcoind methods (currently unimplemented)
"addmultisigaddress": Unimplemented,
"backupwallet": Unimplemented,
"createmultisig": Unimplemented,
"dumpwallet": Unimplemented,
"getblocktemplate": Unimplemented,
"getrawchangeaddress": Unimplemented,
"getreceivedbyaccount": Unimplemented,
"getreceivedbyaddress": Unimplemented,
"gettransaction": Unimplemented,
"gettxout": Unimplemented,
"gettxoutsetinfo": Unimplemented,
"getwork": Unimplemented,
"importwallet": Unimplemented,
"listaddressgroupings": Unimplemented,
"listlockunspent": Unimplemented,
"listreceivedbyaccount": Unimplemented,
"listsinceblock": Unimplemented,
"listreceivedbyaddress": Unimplemented,
"listunspent": Unimplemented,
"lockunspent": Unimplemented,
"move": Unimplemented,
"sendtoaddress": Unimplemented,
"setaccount": Unimplemented,
"signmessage": Unimplemented,
"signrawtransaction": Unimplemented,
"validateaddress": Unimplemented,
"verifymessage": Unimplemented,
"walletpassphrasechange": Unimplemented,
"addmultisigaddress": Unimplemented,
"backupwallet": Unimplemented,
"createmultisig": Unimplemented,
"dumpwallet": Unimplemented,
"getblocktemplate": Unimplemented,
"getrawchangeaddress": Unimplemented,
"getreceivedbyaccount": Unimplemented,
"getreceivedbyaddress": Unimplemented,
"gettransaction": Unimplemented,
"gettxout": Unimplemented,
"gettxoutsetinfo": Unimplemented,
"getwork": Unimplemented,
"importwallet": Unimplemented,
"listaddressgroupings": Unimplemented,
"listlockunspent": Unimplemented,
"listreceivedbyaccount": Unimplemented,
"listsinceblock": Unimplemented,
"listreceivedbyaddress": Unimplemented,
"listunspent": Unimplemented,
"lockunspent": Unimplemented,
"move": Unimplemented,
"sendtoaddress": Unimplemented,
"setaccount": Unimplemented,
"signmessage": Unimplemented,
"signrawtransaction": Unimplemented,
"validateaddress": Unimplemented,
"verifymessage": Unimplemented,
// Standard bitcoind methods which won't be implemented by btcwallet.
"encryptwallet": Unsupported,
@ -1277,7 +1277,7 @@ func WalletPassphrase(icmd btcjson.Cmd) (interface{}, *btcjson.Error) {
return nil, &e
}
switch err := a.Unlock([]byte(cmd.Passphrase), cmd.Timeout); err {
switch err := a.Unlock([]byte(cmd.Passphrase)); err {
case nil:
go func(timeout int64) {
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
// about a account for a wallet frontend.
//

View file

@ -36,8 +36,8 @@ var (
}
)
// networkDir returns the base directory name for the bitcoin network
// net.
// networkDir returns the directory name of a network directory to hold account
// files.
func networkDir(net btcwire.BitcoinNet) string {
var netname string
if net == btcwire.MainNet {
@ -48,6 +48,11 @@ func networkDir(net btcwire.BitcoinNet) string {
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.
// If path does not exist, it is created.
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,
// txstore, and utxostore, writing them to disk if any are dirty.
func (a *Account) writeDirtyToDisk() error {

View file

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

View file

@ -75,6 +75,22 @@ func updateOldFileLocations() {
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.
netdir := filepath.Join(cfg.DataDir, "testnet")
if err := checkCreateDir(netdir); err != nil {

View file

@ -61,6 +61,7 @@ var (
ErrWalletDoesNotExist = errors.New("non-existant wallet")
ErrWalletIsWatchingOnly = errors.New("wallet is watching-only")
ErrWalletLocked = errors.New("wallet is locked")
ErrWrongPassphrase = errors.New("wrong passphrase")
)
var (
@ -845,6 +846,37 @@ func (w *Wallet) Passphrase() ([]byte, error) {
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) {
for i := range b {
b[i] = 0
@ -2151,7 +2183,7 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
}
x, y := btcec.S256().ScalarBaseMult(privkey)
if x.Cmp(pubKey.X) != 0 || y.Cmp(pubKey.Y) != 0 {
return nil, errors.New("decryption failed")
return nil, ErrWrongPassphrase
}
privkeyCopy := make([]byte, 32)
@ -2160,9 +2192,36 @@ func (a *btcAddress) unlock(key []byte) (privKeyCT []byte, err error) {
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 {
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.

View file

@ -667,3 +667,78 @@ func TestWatchingWalletExport(t *testing.T) {
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
}
}