add delete wallet functionality, overwriting wallet file data first

This commit is contained in:
Craig Raw 2022-05-06 10:37:15 +02:00
parent 7fb230e56b
commit b5fa8f0ee0
7 changed files with 123 additions and 3 deletions

View file

@ -111,6 +111,12 @@ public class AppController implements Initializable {
@FXML
private MenuItem exportWallet;
@FXML
private MenuItem deleteWallet;
@FXML
private MenuItem closeTab;
@FXML
private Menu fileMenu;
@ -294,11 +300,13 @@ public class AppController implements Initializable {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
closeTab.setDisable(tabs.getTabs().isEmpty());
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage)tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
exportWallet.setDisable(true);
}
}
});
@ -349,6 +357,8 @@ public class AppController implements Initializable {
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true);
deleteWallet.disableProperty().bind(exportWallet.disableProperty());
closeTab.setDisable(true);
lockWallet.setDisable(true);
searchWallet.disableProperty().bind(exportWallet.disableProperty());
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
@ -792,6 +802,10 @@ public class AppController implements Initializable {
}
}
public void deleteWallet(ActionEvent event) {
deleteWallet(getSelectedWalletForm());
}
public void closeTab(ActionEvent event) {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
}
@ -1851,10 +1865,65 @@ public class AppController implements Initializable {
tabs.getTabs().removeAll(tabs.getTabs());
});
contextMenu.getItems().addAll(close, closeOthers, closeAll);
MenuItem delete = new MenuItem("Delete...");
delete.setOnAction(event -> {
deleteWallet(getSelectedWalletForm());
});
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
return contextMenu;
}
private void deleteWallet(WalletForm selectedWalletForm) {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
Storage storage = selectedWalletForm.getStorage();
if(selectedWalletForm.getMasterWallet().isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(selectedWalletForm.getWallet().getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
try {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
deleteStorage(storage);
} finally {
encryptionFullKey.clear();
password.get().clear();
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> deleteWallet(getSelectedWalletForm()));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
deleteStorage(storage);
}
}
}
private void deleteStorage(Storage storage) {
if(storage.isClosed()) {
Platform.runLater(storage::delete);
} else {
Platform.runLater(() -> deleteStorage(storage));
}
}
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");

View file

@ -1,5 +1,8 @@
package com.sparrowwallet.sparrow.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
@ -9,11 +12,14 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class IOUtils {
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
public static FileType getFileType(File file) {
try {
String type = Files.probeContentType(file.toPath());
@ -120,4 +126,28 @@ public class IOUtils {
return true;
}
public static boolean secureDelete(File file) {
if(file.exists()) {
long length = file.length();
SecureRandom random = new SecureRandom();
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
raf.seek(0);
raf.getFilePointer();
byte[] data = new byte[64];
int pos = 0;
while(pos < length) {
random.nextBytes(data);
raf.write(data);
pos += data.length;
}
} catch(IOException e) {
log.warn("Error overwriting file for deletion " + file.getName(), e);
}
return file.delete();
}
return false;
}
}

View file

@ -305,6 +305,11 @@ public class JsonPersistence implements Persistence {
com.google.common.io.Files.copy(walletFile, outputStream);
}
@Override
public boolean isClosed() {
return true;
}
@Override
public void close() {
//Nothing required

View file

@ -25,5 +25,6 @@ public interface Persistence {
String getWalletId(Storage storage, Wallet wallet);
String getWalletName(File walletFile, Wallet wallet);
void copyWallet(File walletFile, OutputStream outputStream) throws IOException;
boolean isClosed();
void close();
}

View file

@ -126,6 +126,10 @@ public class Storage {
return persistence.isPersisted(this, wallet);
}
public boolean isClosed() {
return persistence.isClosed();
}
public void close() {
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
closePersistenceService.start();
@ -163,6 +167,11 @@ public class Storage {
persistence.copyWallet(walletFile, outputStream);
}
public void delete() {
deleteBackups();
IOUtils.secureDelete(walletFile);
}
public void deleteBackups() {
deleteBackups(null);
}
@ -181,7 +190,7 @@ public class Storage {
private void deleteBackups(String prefix) {
File[] backups = getBackups(prefix);
for(File backup : backups) {
backup.delete();
IOUtils.secureDelete(backup);
}
}

View file

@ -568,6 +568,11 @@ public class DbPersistence implements Persistence {
com.google.common.io.Files.copy(walletFile, outputStream);
}
@Override
public boolean isClosed() {
return dataSource.isClosed();
}
@Override
public void close() {
EventManager.get().unregister(this);

View file

@ -48,7 +48,8 @@
<SeparatorMenuItem styleClass="osxHide" />
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Preferences..." accelerator="Shortcut+P" onAction="#openPreferences"/>
<SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Close Tab" accelerator="Shortcut+W" onAction="#closeTab"/>
<MenuItem fx:id="deleteWallet" mnemonicParsing="false" text="Delete Wallet..." onAction="#deleteWallet"/>
<MenuItem fx:id="closeTab" mnemonicParsing="false" text="Close Tab" accelerator="Shortcut+W" onAction="#closeTab"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Quit" accelerator="Shortcut+Q" onAction="#quit"/>
</items>
</Menu>