diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index e9d0fd0e..fff2ceac 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -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 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 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 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"); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java index 161f15a4..12a135e1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java @@ -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; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java index 5dde6bda..16501027 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/JsonPersistence.java @@ -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 diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java b/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java index 61c75623..d30a8bf7 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Persistence.java @@ -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(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java index 5dee1efc..34dfb52c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Storage.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Storage.java @@ -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); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index 5dd46005..ef61c20d 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -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); diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 72644711..3573b23c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -48,7 +48,8 @@ - + +