feat: Make DB re-encryption fault proof (#3607)

Improve the error management in order to avoid DB corruption in case the process is killed while encrypting the DB.

Changes:
Use sqlcipher_export instead of rekey to change the DB password. The advantage is that sqlcipher_export will operate on a new DB file and we don't need to modify the current account unless the export is successful.
Keeping the rekey requires to create a DB copy before trying to re-encrypt the DB, but the DB copy is risky in case the DB file changes wile the copy is in progress. It could also lead to DB corruption.
This commit is contained in:
Alex Jbanca 2023-06-14 13:12:23 +03:00 committed by GitHub
parent 43b2c3b7ce
commit 3978048afa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 71 additions and 9 deletions

View File

@ -749,8 +749,7 @@ 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-v4.db", keyUID))
func (b *GethStatusBackend) reEncryptKeyStoreDir(currentPassword string, newPassword string) error {
config := b.StatusNode().Config()
keyDir := ""
if config == nil {
@ -760,25 +759,74 @@ func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password strin
}
if keyDir != "" {
err := b.accountManager.ReEncryptKeyStoreDir(keyDir, password, newPassword)
err := b.accountManager.ReEncryptKeyStoreDir(keyDir, currentPassword, newPassword)
if err != nil {
return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err)
}
}
return nil
}
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(keyUID)
func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error {
dbPath := filepath.Join(b.rootDataDir, fmt.Sprintf("%s-v4.db", keyUID))
account, err := b.multiaccountsDB.GetAccount(keyUID)
if err != nil {
return err
}
err = appdatabase.ChangeDatabasePassword(dbPath, password, kdfIterations, newPassword)
file, err := os.CreateTemp("", "*-v4.db")
if err != nil {
if config != nil {
keyDir := config.KeyStoreDir
// couldn't change db password so undo keystore changes to mainitain consistency
_ = b.accountManager.ReEncryptKeyStoreDir(keyDir, newPassword, password)
return err
}
newDBPath := file.Name()
defer func() {
_ = file.Close()
_ = os.Remove(newDBPath)
_ = os.Remove(newDBPath + "-wal")
_ = os.Remove(newDBPath + "-shm")
_ = os.Remove(newDBPath + "-journal")
}()
// Exporting database to a temporary file with a new password
err = appdatabase.ExportDB(dbPath, password, account.KDFIterations, newDBPath, newPassword)
if err != nil {
return err
}
err = b.reEncryptKeyStoreDir(password, newPassword)
if err != nil {
return err
}
// Replacing the old database with the new one requires closing all connections to the database
// This is done by stopping the node and restarting it with the new DB
appDBPath, _ := appdatabase.GetDBFilename(b.appDB)
changeCurrentAccountPassword := appDBPath == dbPath
if changeCurrentAccountPassword {
_ = b.Logout()
}
// Replacing the old database files with the new ones, ignoring the wal and shm errors
err = os.Rename(newDBPath, dbPath)
if err != nil {
// Restore the old account
_ = b.reEncryptKeyStoreDir(newPassword, password)
if changeCurrentAccountPassword {
_ = b.startNodeWithAccount(*account, password, nil)
}
return err
}
_ = os.Remove(dbPath + "-wal")
_ = os.Remove(dbPath + "-shm")
_ = os.Rename(newDBPath+"-wal", dbPath+"-wal")
_ = os.Rename(newDBPath+"-shm", dbPath+"-shm")
if changeCurrentAccountPassword {
return b.startNodeWithAccount(*account, newPassword, nil)
}
return nil
}

View File

@ -72,6 +72,10 @@ func EncryptDatabase(oldPath, newPath, password string, kdfIterationsNumber int)
return sqlite.EncryptDB(oldPath, newPath, password, kdfIterationsNumber)
}
func ExportDB(path string, password string, kdfIterationsNumber int, newDbPAth string, newPassword string) error {
return sqlite.ExportDB(path, password, kdfIterationsNumber, newDbPAth, newPassword)
}
func ChangeDatabasePassword(path string, password string, kdfIterationsNumber int, newPassword string) error {
return sqlite.ChangeEncryptionKey(path, password, kdfIterationsNumber, newPassword)
}

View File

@ -102,6 +102,16 @@ func EncryptDB(unencryptedPath string, encryptedPath string, key string, kdfIter
return encryptDB(db, encryptedPath, key, kdfIterationsNumber)
}
// Export takes an encrypted database and re-encrypts it in a new file, with a new key
func ExportDB(encryptedPath string, key string, kdfIterationsNumber int, newPath string, newKey string) error {
db, err := openDB(encryptedPath, key, kdfIterationsNumber, V4CipherPageSize)
if err != nil {
return err
}
defer db.Close()
return encryptDB(db, newPath, newKey, kdfIterationsNumber)
}
func buildSqlcipherDSN(path string) (string, error) {
if path == InMemoryPath {
return InMemoryPath, nil