diff --git a/VERSION b/VERSION index 60a54513c..964b0a805 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.79.15 +0.80.00 diff --git a/account/accounts.go b/account/accounts.go index 08d348c1d..99273aa03 100644 --- a/account/accounts.go +++ b/account/accounts.go @@ -12,6 +12,9 @@ import ( "github.com/pborman/uuid" + gethkeystore "github.com/ethereum/go-ethereum/accounts/keystore" + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/status-im/status-go/account/generator" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/keystore" @@ -477,3 +480,116 @@ func (m *Manager) MigrateKeyStoreDir(oldDir, newDir string, addresses []string) return nil } + +func (m *Manager) ReEncryptKey(rawKey []byte, pass string, newPass string) (reEncryptedKey []byte, e error) { + cryptoJSON, e := keystore.RawKeyToCryptoJSON(rawKey) + if e != nil { + return reEncryptedKey, fmt.Errorf("convert to crypto json error: %v", e) + } + + decryptedKey, e := keystore.DecryptKey(rawKey, pass) + if e != nil { + return reEncryptedKey, fmt.Errorf("decryption error: %v", e) + } + + if cryptoJSON.KDFParams["n"] == nil || cryptoJSON.KDFParams["p"] == nil { + return reEncryptedKey, fmt.Errorf("Unable to determine `n` or `p`: %v", e) + } + n := int(cryptoJSON.KDFParams["n"].(float64)) + p := int(cryptoJSON.KDFParams["p"].(float64)) + + gethKey := gethkeystore.Key{ + Id: decryptedKey.ID, + Address: gethcommon.Address(decryptedKey.Address), + PrivateKey: decryptedKey.PrivateKey, + ExtendedKey: decryptedKey.ExtendedKey, + SubAccountIndex: decryptedKey.SubAccountIndex, + } + + return gethkeystore.EncryptKey(&gethKey, newPass, n, p) +} + +func (m *Manager) ReEncryptKeyStoreDir(keyDirPath, oldPass, newPass string) error { + rencryptFileAtPath := func(tempKeyDirPath, path string, fileInfo os.FileInfo) error { + if fileInfo.IsDir() { + return nil + } + + rawKeyFile, e := ioutil.ReadFile(path) + if e != nil { + return fmt.Errorf("invalid account key file: %v", e) + } + + reEncryptedKey, e := m.ReEncryptKey(rawKeyFile, oldPass, newPass) + if e != nil { + return fmt.Errorf("unable to re-encrypt key file: %v, path: %s, name: %s", e, path, fileInfo.Name()) + } + + tempWritePath := filepath.Join(tempKeyDirPath, fileInfo.Name()) + e = ioutil.WriteFile(tempWritePath, reEncryptedKey, fileInfo.Mode().Perm()) + if e != nil { + return fmt.Errorf("unable write key file: %v", e) + } + + return nil + } + + keyParent, keyDirName := filepath.Split(keyDirPath) + + // backupKeyDirName used to store existing keys before final write + backupKeyDirName := keyDirName + "-backup" + // tempKeyDirName used to put re-encrypted keys + tempKeyDirName := keyDirName + "-re-encrypted" + backupKeyDirPath := filepath.Join(keyParent, backupKeyDirName) + tempKeyDirPath := filepath.Join(keyParent, tempKeyDirName) + + // create temp key dir + err := os.MkdirAll(tempKeyDirPath, os.ModePerm) + if err != nil { + return fmt.Errorf("mkdirall error: %v, tempKeyDirPath: %s", err, tempKeyDirPath) + } + + err = filepath.Walk(keyDirPath, func(path string, fileInfo os.FileInfo, err error) error { + if err != nil { + os.RemoveAll(tempKeyDirPath) + return fmt.Errorf("walk callback error: %v", err) + } + + return rencryptFileAtPath(tempKeyDirPath, path, fileInfo) + }) + if err != nil { + os.RemoveAll(tempKeyDirPath) + return fmt.Errorf("walk error: %v", err) + } + + // move existing keys + err = os.Rename(keyDirPath, backupKeyDirPath) + if err != nil { + os.RemoveAll(tempKeyDirPath) + return fmt.Errorf("unable to rename keyDirPath to backupKeyDirPath: %v", err) + } + + // move tempKeyDirPath to keyDirPath + err = os.Rename(tempKeyDirPath, keyDirPath) + if err != nil { + // if this happens, then the app is probably bricked, because the keystore won't exist anymore + // try to restore from backup + _ = os.Rename(backupKeyDirPath, keyDirPath) + return fmt.Errorf("unable to rename tempKeyDirPath to keyDirPath: %v", err) + } + + // remove temp and backup folders and their contents + err = os.RemoveAll(tempKeyDirPath) + if err != nil { + // the re-encryption is complete so we don't throw + log.Error("unable to delete tempKeyDirPath, manual cleanup required") + } + + err = os.RemoveAll(backupKeyDirPath) + if err != nil { + // the re-encryption is complete so we don't throw + log.Error("unable to delete backupKeyDirPath, manual cleanup required") + } + + return nil +} diff --git a/account/accounts_test.go b/account/accounts_test.go index d58e846dd..a6b2afdc7 100644 --- a/account/accounts_test.go +++ b/account/accounts_test.go @@ -1,6 +1,7 @@ package account import ( + "encoding/json" "errors" "fmt" "io/ioutil" @@ -10,6 +11,7 @@ import ( "testing" "github.com/status-im/status-go/eth-node/crypto" + "github.com/status-im/status-go/eth-node/keystore" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/t/utils" @@ -17,6 +19,9 @@ import ( "github.com/stretchr/testify/suite" ) +const testPassword = "test-password" +const newTestPassword = "new-test-password" + func TestVerifyAccountPassword(t *testing.T) { accManager := NewGethManager() keyStoreDir, err := ioutil.TempDir(os.TempDir(), "accounts") @@ -143,8 +148,6 @@ func (s *ManagerTestSuite) SetupTest() { s.Require().NoError(s.accManager.InitKeystore(keyStoreDir)) s.keydir = keyStoreDir - testPassword := "test-password" - // Initial test - create test account _, accountInfo, mnemonic, err := s.accManager.CreateAccount(testPassword) s.Require().NoError(err) @@ -348,3 +351,72 @@ func (s *ManagerTestSuite) TestMigrateKeyStoreDir() { files, _ = ioutil.ReadDir(newKeyDir) s.Equal(1, len(files)) } + +func (s *ManagerTestSuite) TestReEncryptKey() { + var firstKeyPath string + files, _ := ioutil.ReadDir(s.keydir) + + // thiere is only one file in this dir, + // is there a better way to reference it? + for _, f := range files { + firstKeyPath = filepath.Join(s.keydir, f.Name()) + } + + rawKey, _ := ioutil.ReadFile(firstKeyPath) + reEncryptedKey, _ := s.accManager.ReEncryptKey(rawKey, testPassword, newTestPassword) + + type Key struct { + Address string `json:"address"` + } + + var unmarshaledRaw, unmarshaledReEncrypted Key + _ = json.Unmarshal(rawKey, &unmarshaledRaw) + _ = json.Unmarshal(reEncryptedKey, &unmarshaledReEncrypted) + + oldCrypto, _ := keystore.RawKeyToCryptoJSON(rawKey) + newCrypto, _ := keystore.RawKeyToCryptoJSON(reEncryptedKey) + + // Test address is same post re-encryption + s.Equal(unmarshaledRaw.Address, unmarshaledReEncrypted.Address) + + // Test cipher changes after re-encryption + s.NotEqual(oldCrypto.CipherText, newCrypto.CipherText) + + // Test re-encrypted key cannot be decrypted using old testPasswordword + _, decryptOldError := keystore.DecryptKey(reEncryptedKey, testPassword) + s.Require().Error(decryptOldError) + + // Test re-encrypted key can be decrypted using new testPassword + _, decryptNewError := keystore.DecryptKey(reEncryptedKey, newTestPassword) + s.Require().NoError(decryptNewError) +} + +func (s *ManagerTestSuite) TestReEncryptKeyStoreDir() { + + err := s.accManager.ReEncryptKeyStoreDir(s.keydir, testPassword, newTestPassword) + s.Require().NoError(err) + + err = filepath.Walk(s.keydir, func(path string, fileInfo os.FileInfo, err error) error { + if fileInfo.IsDir() { + return nil + } + + // walk should not throw callback errors + s.Require().NoError(err) + + rawKeyFile, err := ioutil.ReadFile(path) + s.Require().NoError(err) + + // should not decrypt with old password + _, decryptError := keystore.DecryptKey(rawKeyFile, testPassword) + s.Require().Error(decryptError) + + // should decrypt with new password + _, decryptError = keystore.DecryptKey(rawKeyFile, newTestPassword) + s.Require().NoError(decryptError) + + return nil + }) + + s.Require().NoError(err) +} diff --git a/api/geth_backend.go b/api/geth_backend.go index 54f0e52a1..bbcddff35 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -459,6 +459,25 @@ func (b *GethStatusBackend) ImportUnencryptedDatabase(acc multiaccounts.Account, return nil } +func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error { + dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID)) + config := b.StatusNode().Config() + keyDir := config.KeyStoreDir + + err := b.accountManager.ReEncryptKeyStoreDir(keyDir, password, newPassword) + if err != nil { + return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err) + } + + err = appdatabase.ChangeDatabasePassword(dbPath, password, newPassword) + if err != nil { + // couldn't change db password so undo keystore changes to mainitain consistency + _ = b.accountManager.ReEncryptKeyStoreDir(keyDir, newPassword, password) + return err + } + return nil +} + func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(acc multiaccounts.Account, password string, settings accounts.Settings, nodecfg *params.NodeConfig, subaccs []accounts.Account, keyHex string) error { err := b.SaveAccount(acc) if err != nil { diff --git a/appdatabase/database.go b/appdatabase/database.go index 9ed0f801b..f9b53e71c 100644 --- a/appdatabase/database.go +++ b/appdatabase/database.go @@ -31,3 +31,7 @@ func DecryptDatabase(oldPath, newPath, password string) error { func EncryptDatabase(oldPath, newPath, password string) error { return sqlite.EncryptDB(oldPath, newPath, password) } + +func ChangeDatabasePassword(path, password, newPassword string) error { + return sqlite.ChangeEncryptionKey(path, password, newPassword) +} diff --git a/eth-node/keystore/passphrase.go b/eth-node/keystore/passphrase.go index 0eec754d3..c7c331e0a 100644 --- a/eth-node/keystore/passphrase.go +++ b/eth-node/keystore/passphrase.go @@ -328,3 +328,13 @@ func pkcs7Unpad(in []byte) []byte { } return in[:len(in)-int(padding)] } + + +func RawKeyToCryptoJSON(rawKeyFile []byte) (cj CryptoJSON, e error){ + var keyJSON encryptedKeyJSONV3 + if e := json.Unmarshal(rawKeyFile, &keyJSON); e != nil { + return cj, fmt.Errorf("failed to read key file: %s", e) + } + + return keyJSON.Crypto, e +} diff --git a/mobile/status.go b/mobile/status.go index 4c74c6f3e..571c3792d 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -687,3 +687,11 @@ func ImportUnencryptedDatabase(accountData, password, databasePath string) strin } return makeJSONResponse(nil) } + +func ChangeDatabasePassword(keyUID, password, newPassword string) string { + err := statusBackend.ChangeDatabasePassword(keyUID, password, newPassword) + if err != nil { + return makeJSONResponse(err) + } + return makeJSONResponse(nil) +} diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 17869b99e..5ab7a87d0 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -137,3 +137,18 @@ func OpenUnecryptedDB(path string) (*sql.DB, error) { return db, nil } + +func ChangeEncryptionKey(path, key, newKey string) error { + db, err := openDB(path, key) + + if err != nil { + return err + } + + resetKeyString := fmt.Sprintf("PRAGMA rekey = '%s'", newKey) + if _, err = db.Exec(resetKeyString); err != nil { + return errors.New("failed to set rekey pragma") + } + + return nil +}