Commit 4b738739 authored by David Vorick's avatar David Vorick Committed by GitHub

Merge pull request #1820 from NebulousLabs/wallet-changepw

add `/wallet/changekey` API endpoint for change the wallet's encryption key
parents 413dae08 f5dd396e
......@@ -257,6 +257,7 @@ func New(requiredUserAgent string, requiredPassword string, cs modules.Consensus
router.GET("/wallet/transactions/:addr", api.walletTransactionsAddrHandler)
router.GET("/wallet/verify/address/:addr", api.walletVerifyAddressHandler)
router.POST("/wallet/unlock", RequirePassword(api.walletUnlockHandler, requiredPassword))
router.POST("/wallet/changepassword", RequirePassword(api.walletChangePasswordHandler, requiredPassword))
}
// Apply UserAgent middleware and return the API
......
......@@ -532,6 +532,35 @@ func (api *API) walletUnlockHandler(w http.ResponseWriter, req *http.Request, _
WriteError(w, Error{"error when calling /wallet/unlock: " + modules.ErrBadEncryptionKey.Error()}, http.StatusBadRequest)
}
// walletChangePasswordHandler handles API calls to /wallet/changepassword
func (api *API) walletChangePasswordHandler(w http.ResponseWriter, req *http.Request, _ httprouter.Params) {
var newKey crypto.TwofishKey
newPassword := req.FormValue("newpassword")
if newPassword == "" {
WriteError(w, Error{"a password must be provided to newpassword"}, http.StatusBadRequest)
return
}
newKey = crypto.TwofishKey(crypto.HashObject(newPassword))
originalKeys := encryptionKeys(req.FormValue("encryptionpassword"))
if len(originalKeys) != 1 {
WriteError(w, Error{"expected one encryption key passed to encryptionpassword"}, http.StatusBadRequest)
return
}
for _, key := range originalKeys {
err := api.wallet.ChangeKey(key, newKey)
if err == nil {
WriteSuccess(w)
return
}
if err != nil && err != modules.ErrBadEncryptionKey {
WriteError(w, Error{"error when calling /wallet/changepassword: " + err.Error()}, http.StatusBadRequest)
return
}
}
WriteError(w, Error{"error when calling /wallet/changepassword: " + modules.ErrBadEncryptionKey.Error()}, http.StatusBadRequest)
}
// walletVerifyAddressHandler handles API calls to /wallet/verify/address/:addr.
func (api *API) walletVerifyAddressHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) {
addrString := ps.ByName("addr")
......
......@@ -238,6 +238,143 @@ func TestWalletRescanning(t *testing.T) {
<-doneChan
}
// TestWalletChangePasswordDeep is a more through validation test of the
// /wallet/changepassword endpoint.
func TestWalletChangePasswordDeep(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
t.Parallel()
st, err := createServerTester(t.Name())
if err != nil {
t.Fatal(err)
}
defer st.server.panicClose()
st2, err := blankServerTester(t.Name() + "-wallet1")
if err != nil {
t.Fatal(err)
}
defer st2.server.Close()
st3, err := blankServerTester(t.Name() + "-wallet2")
if err != nil {
t.Fatal(err)
}
defer st3.server.Close()
st4, err := blankServerTester(t.Name() + "-wallet3")
if err != nil {
t.Fatal(err)
}
defer st4.server.Close()
st5, err := blankServerTester(t.Name() + "-wallet4")
if err != nil {
t.Fatal(err)
}
defer st5.server.Close()
wallets := []*serverTester{st, st2, st3, st4, st5}
err = fullyConnectNodes(wallets)
if err != nil {
t.Fatal(err)
}
// send 10KS to each of the blank wallets
sendSiacoins := func(srcST *serverTester, destST *serverTester, amount uint64) {
var wag WalletAddressGET
err = destST.getAPI("/wallet/address", &wag)
if err != nil {
t.Fatal(err)
}
sendValue := types.SiacoinPrecision.Mul64(amount)
sendSiacoinsValues := url.Values{}
sendSiacoinsValues.Set("amount", sendValue.String())
sendSiacoinsValues.Set("destination", wag.Address.String())
if err = srcST.stdPostAPI("/wallet/siacoins", sendSiacoinsValues); err != nil {
t.Fatal(err)
}
_, err = st.miner.AddBlock()
if err != nil {
t.Fatal(err)
}
}
for _, wallet := range wallets {
sendSiacoins(st, wallet, 10000)
}
// mine a few blocks
for i := 0; i < 15; i++ {
_, err = st.miner.AddBlock()
if err != nil {
t.Fatal(err)
}
}
st2seed, _, err := st2.wallet.PrimarySeed()
if err != nil {
t.Fatal(err)
}
st3seed, _, err := st3.wallet.PrimarySeed()
if err != nil {
t.Fatal(err)
}
// close 2 of the 3 blank wallets
err = st2.server.Close()
if err != nil {
t.Fatal(err)
}
err = st3.server.Close()
if err != nil {
t.Fatal(err)
}
// load their seeds into the third wallet
loadSeed := func(seed modules.Seed, st *serverTester) {
err = st.wallet.LoadSeed(st.walletKey, seed)
if err != nil {
t.Fatal(err)
}
}
loadSeed(st2seed, st4)
loadSeed(st3seed, st4)
// restart the third wallet
err = st4.server.Close()
if err != nil {
t.Fatal(err)
}
st4, err = assembleServerTester(st4.walletKey, st4.dir)
if err != nil {
t.Fatal(err)
}
// changekey the third wallet
newKey := crypto.TwofishKey(crypto.HashObject("newpassword"))
err = st4.wallet.ChangeKey(st4.walletKey, newKey)
if err != nil {
t.Fatal(err)
}
// send all of the money from the third wallet to the fourth wallet
sendSiacoins(st4, st5, 5000)
sendSiacoins(st4, st5, 5000)
sendSiacoins(st4, st5, 5000)
sendSiacoins(st4, st5, 5000)
sendSiacoins(st4, st5, 5000)
sendSiacoins(st4, st5, 4000)
// verify the money went through
minExpectedBalance := types.SiacoinPrecision.Mul64(26900)
balance, _, _ := st5.wallet.ConfirmedBalance()
if balance.Cmp(minExpectedBalance) < 0 {
t.Fatalf("balance should end up in the final wallet, wanted %v got %v\n", minExpectedBalance.Div(types.SiacoinPrecision), balance.Div(types.SiacoinPrecision))
}
}
// TestWalletEncrypt tries to encrypt and unlock the wallet through the api
// using a provided encryption key.
func TestWalletEncrypt(t *testing.T) {
......@@ -1340,3 +1477,98 @@ func TestWalletVerifyAddress(t *testing.T) {
t.Fatal("expected /wallet/verify to pass a valid address")
}
}
// TestWalletChangePassword verifies that the /wallet/changepassword endpoint
// works correctly and changes a wallet password.
func TestWalletChangePassword(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
t.Parallel()
testdir := build.TempDir("api", t.Name())
originalPassword := "testpass"
newPassword := "newpass"
originalKey := crypto.TwofishKey(crypto.HashObject(originalPassword))
newKey := crypto.TwofishKey(crypto.HashObject(newPassword))
st, err := assembleServerTester(originalKey, testdir)
if err != nil {
t.Fatal(err)
}
// lock the wallet
err = st.stdPostAPI("/wallet/lock", nil)
if err != nil {
t.Fatal(err)
}
// Use the password to call /wallet/unlock.
unlockValues := url.Values{}
unlockValues.Set("encryptionpassword", originalPassword)
err = st.stdPostAPI("/wallet/unlock", unlockValues)
if err != nil {
t.Fatal(err)
}
// Check that the wallet actually unlocked.
if !st.wallet.Unlocked() {
t.Error("wallet is not unlocked")
}
// change the wallet key
changeKeyValues := url.Values{}
changeKeyValues.Set("encryptionpassword", originalPassword)
changeKeyValues.Set("newpassword", newPassword)
err = st.stdPostAPI("/wallet/changepassword", changeKeyValues)
if err != nil {
t.Fatal(err)
}
// wallet should still be unlocked
if !st.wallet.Unlocked() {
t.Fatal("changepassword locked the wallet")
}
// lock the wallet and verify unlocking works with the new password
err = st.stdPostAPI("/wallet/lock", nil)
if err != nil {
t.Fatal(err)
}
unlockValues.Set("encryptionpassword", newPassword)
err = st.stdPostAPI("/wallet/unlock", unlockValues)
if err != nil {
t.Fatal(err)
}
// Check that the wallet actually unlocked.
if !st.wallet.Unlocked() {
t.Error("wallet is not unlocked")
}
// reload the server and verify unlocking still works
err = st.server.Close()
if err != nil {
t.Fatal(err)
}
st2, err := assembleServerTester(newKey, st.dir)
if err != nil {
t.Fatal(err)
}
defer st2.server.panicClose()
// lock the wallet
err = st2.stdPostAPI("/wallet/lock", nil)
if err != nil {
t.Fatal(err)
}
// Use the password to call /wallet/unlock.
err = st2.stdPostAPI("/wallet/unlock", unlockValues)
if err != nil {
t.Fatal(err)
}
// Check that the wallet actually unlocked.
if !st2.wallet.Unlocked() {
t.Error("wallet is not unlocked")
}
}
......@@ -964,6 +964,7 @@ Wallet
| [/wallet/transactions/___:addr___](#wallettransactionsaddr-get) | GET |
| [/wallet/unlock](#walletunlock-post) | POST |
| [/wallet/verify/address/:___addr___](#walletverifyaddress-get) | GET |
| [/wallet/changepassword](#walletchangepassword-post) | POST |
For examples and detailed descriptions of request and response parameters,
refer to [Wallet.md](/doc/api/Wallet.md).
......@@ -1339,3 +1340,17 @@ takes the address specified by :addr and returns a JSON response indicating if t
"valid": true
}
```
#### /wallet/changepassword [POST]
changes the wallet's encryption key.
###### Query String Parameters [(with comments)](/doc/api/Wallet.md#query-string-parameters-12)
```
encryptionpassword
newpassword
```
###### Response
standard success or error response. See
[#standard-responses](#standard-responses).
......@@ -48,6 +48,7 @@ Index
| [/wallet/transactions/___:addr___](#wallettransactionsaddr-get) | GET |
| [/wallet/unlock](#walletunlock-post) | POST |
| [/wallet/verify/address/:___addr___](#walletverifyaddress-get) | GET |
| [/wallet/changepassword](#walletchangepassword-post) | POST |
#### /wallet [GET]
......@@ -615,3 +616,19 @@ takes the address specified by :addr and returns a JSON response indicating if t
"valid": true
}
```
#### /wallet/changepassword [POST]
changes the wallet's encryption password.
###### Query String Parameter
```
// encryptionpassword is the wallet's current encryption password.
encryptionpassword
// newpassword is the new password for the wallet.
newpassword
```
###### Response
standard success or error response. See
[#standard-responses](#standard-responses).
......@@ -256,6 +256,10 @@ type (
// derived from the master key.
Unlock(masterKey crypto.TwofishKey) error
// ChangeKey changes the wallet's materKey from masterKey to newKey,
// re-encrypting the wallet with the provided key.
ChangeKey(masterKey crypto.TwofishKey, newKey crypto.TwofishKey) error
// Unlocked returns true if the wallet is currently unlocked, false
// otherwise.
Unlocked() bool
......
......@@ -413,6 +413,146 @@ func (w *Wallet) Lock() error {
return nil
}
// managedChangeKey safely performs the database operations required to change
// the wallet's encryption key.
func (w *Wallet) managedChangeKey(masterKey crypto.TwofishKey, newKey crypto.TwofishKey) error {
w.mu.Lock()
encrypted := w.encrypted
w.mu.Unlock()
if !encrypted {
return errUnencryptedWallet
}
// grab the current seed files
var primarySeedFile seedFile
var auxiliarySeedFiles []seedFile
var unseededKeyFiles []spendableKeyFile
err := func() error {
w.mu.Lock()
defer w.mu.Unlock()
// verify masterKey
err := checkMasterKey(w.dbTx, masterKey)
if err != nil {
return err
}
wb := w.dbTx.Bucket(bucketWallet)
// primarySeedFile
err = encoding.Unmarshal(wb.Get(keyPrimarySeedFile), &primarySeedFile)
if err != nil {
return err
}
// auxiliarySeedFiles
err = encoding.Unmarshal(wb.Get(keyAuxiliarySeedFiles), &auxiliarySeedFiles)
if err != nil {
return err
}
// unseededKeyFiles
err = encoding.Unmarshal(wb.Get(keySpendableKeyFiles), &unseededKeyFiles)
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
// decrypt key files
var primarySeed modules.Seed
var auxiliarySeeds []modules.Seed
var spendableKeys []spendableKey
primarySeed, err = decryptSeedFile(masterKey, primarySeedFile)
if err != nil {
return err
}
for _, sf := range auxiliarySeedFiles {
auxSeed, err := decryptSeedFile(masterKey, sf)
if err != nil {
return err
}
auxiliarySeeds = append(auxiliarySeeds, auxSeed)
}
for _, uk := range unseededKeyFiles {
sk, err := decryptSpendableKeyFile(masterKey, uk)
if err != nil {
return err
}
spendableKeys = append(spendableKeys, sk)
}
// encrypt new keyfiles using newKey
var newPrimarySeedFile seedFile
var newAuxiliarySeedFiles []seedFile
var newUnseededKeyFiles []spendableKeyFile
newPrimarySeedFile = createSeedFile(newKey, primarySeed)
for _, seed := range auxiliarySeeds {
newAuxiliarySeedFiles = append(newAuxiliarySeedFiles, createSeedFile(newKey, seed))
}
for _, sk := range spendableKeys {
var skf spendableKeyFile
fastrand.Read(skf.UID[:])
encryptionKey := uidEncryptionKey(newKey, skf.UID)
skf.EncryptionVerification = encryptionKey.EncryptBytes(verificationPlaintext)
// Encrypt and save the key.
skf.SpendableKey = encryptionKey.EncryptBytes(encoding.Marshal(sk))
newUnseededKeyFiles = append(newUnseededKeyFiles, skf)
}
// put the newly encrypted keys in the database
err = func() error {
w.mu.Lock()
defer w.mu.Unlock()
wb := w.dbTx.Bucket(bucketWallet)
err = wb.Put(keyPrimarySeedFile, encoding.Marshal(newPrimarySeedFile))
if err != nil {
return err
}
err = wb.Put(keyAuxiliarySeedFiles, encoding.Marshal(newAuxiliarySeedFiles))
if err != nil {
return err
}
err = wb.Put(keySpendableKeyFiles, encoding.Marshal(newUnseededKeyFiles))
if err != nil {
return err
}
uk := uidEncryptionKey(newKey, dbGetWalletUID(w.dbTx))
err = wb.Put(keyEncryptionVerification, uk.EncryptBytes(verificationPlaintext))
if err != nil {
return err
}
return nil
}()
if err != nil {
return err
}
return nil
}
// ChangeKey changes the wallet's encryption key from masterKey to newKey.
func (w *Wallet) ChangeKey(masterKey crypto.TwofishKey, newKey crypto.TwofishKey) error {
if err := w.tg.Add(); err != nil {
return err
}
defer w.tg.Done()
return w.managedChangeKey(masterKey, newKey)
}
// Unlock will decrypt the wallet seed and load all of the addresses into
// memory.
func (w *Wallet) Unlock(masterKey crypto.TwofishKey) error {
......
......@@ -430,3 +430,49 @@ func TestReset(t *testing.T) {
}
postEncryptionTesting(wt.miner, wt.wallet, newKey)
}
func TestChangeKey(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
wt, err := createWalletTester(t.Name())
if err != nil {
t.Fatal(err)
}
defer wt.closeWt()
var newKey crypto.TwofishKey
fastrand.Read(newKey[:])
origBal, _, _ := wt.wallet.ConfirmedBalance()
err = wt.wallet.ChangeKey(wt.walletMasterKey, newKey)
if err != nil {
t.Fatal(err)
}
err = wt.wallet.Lock()
if err != nil {
t.Fatal(err)
}
err = wt.wallet.Unlock(wt.walletMasterKey)
if err == nil {
t.Fatal("expected unlock to fail with the original key")
}
err = wt.wallet.Unlock(newKey)
if err != nil {
t.Fatal(err)
}
newBal, _, _ := wt.wallet.ConfirmedBalance()
if newBal.Cmp(origBal) != 0 {
t.Fatal("wallet with changed key did not have the same balance")
}
err = wt.wallet.Lock()
if err != nil {
t.Fatal(err)
}
postEncryptionTesting(wt.miner, wt.wallet, newKey)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment