From 4dde53f0e8388a256893e2aa9e815d0e75b95f35 Mon Sep 17 00:00:00 2001 From: woodser Date: Sun, 26 Feb 2023 09:08:10 -0500 Subject: [PATCH] support password prompt in legacy ui Co-authored-by: niyid --- .../bisq/core/api/CoreAccountService.java | 7 ++ .../java/bisq/core/app/HavenoExecutable.java | 43 ++++++-- .../java/bisq/core/app/HavenoHeadlessApp.java | 1 - .../main/java/bisq/core/app/HavenoSetup.java | 90 ++++++---------- .../java/bisq/core/app/WalletAppSetup.java | 13 +-- .../btc/model/EncryptedConnectionList.java | 2 +- .../bisq/core/btc/wallet/WalletsManager.java | 2 - .../core/btc/wallet/XmrWalletService.java | 29 +++-- .../bisq/daemon/app/HavenoDaemonMain.java | 91 +++++++++------- .../main/java/bisq/desktop/app/HavenoApp.java | 5 +- .../java/bisq/desktop/app/HavenoAppMain.java | 100 +++++++++++++++++- .../java/bisq/desktop/main/MainViewModel.java | 97 ++++++++--------- .../content/password/PasswordView.java | 76 +++++++------ .../content/seedwords/SeedWordsView.java | 2 +- .../windows/BtcEmptyWalletWindow.java | 9 +- .../overlays/windows/TradeDetailsWindow.java | 1 - .../windows/WalletPasswordWindow.java | 57 ++++------ .../java/bisq/desktop/util/ImageUtil.java | 4 + desktop/src/main/resources/images/lock.png | Bin 0 -> 22292 bytes 19 files changed, 357 insertions(+), 272 deletions(-) create mode 100644 desktop/src/main/resources/images/lock.png diff --git a/core/src/main/java/bisq/core/api/CoreAccountService.java b/core/src/main/java/bisq/core/api/CoreAccountService.java index 2158c96678..6004222686 100644 --- a/core/src/main/java/bisq/core/api/CoreAccountService.java +++ b/core/src/main/java/bisq/core/api/CoreAccountService.java @@ -116,6 +116,7 @@ public class CoreAccountService { public void changePassword(String oldPassword, String newPassword) { if (!isAccountOpen()) throw new IllegalStateException("Cannot change password on unopened account"); + if ("".equals(oldPassword)) oldPassword = null; // normalize to null if (!StringUtils.equals(this.password, oldPassword)) throw new IllegalStateException("Incorrect password"); if (newPassword != null && newPassword.length() < 8) throw new IllegalStateException("Password must be at least 8 characters"); keyStorage.saveKeyRing(keyRing, oldPassword, newPassword); @@ -125,6 +126,12 @@ public class CoreAccountService { } } + public void verifyPassword(String password) throws IncorrectPasswordException { + if (!StringUtils.equals(this.password, password)) { + throw new IncorrectPasswordException("Incorrect password"); + } + } + public void closeAccount() { if (!isAccountOpen()) throw new IllegalStateException("Cannot close unopened account"); keyRing.lockKeys(); // closed account means the keys are locked diff --git a/core/src/main/java/bisq/core/app/HavenoExecutable.java b/core/src/main/java/bisq/core/app/HavenoExecutable.java index 659e7e221e..f7ad4cea50 100644 --- a/core/src/main/java/bisq/core/app/HavenoExecutable.java +++ b/core/src/main/java/bisq/core/app/HavenoExecutable.java @@ -31,13 +31,14 @@ import bisq.core.trade.HavenoUtils; import bisq.core.trade.TradeManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.txproof.xmr.XmrTxProofService; + import bisq.network.p2p.P2PService; import bisq.common.UserThread; import bisq.common.app.AppModule; -import bisq.common.config.HavenoHelpFormatter; import bisq.common.config.Config; import bisq.common.config.ConfigException; +import bisq.common.config.HavenoHelpFormatter; import bisq.common.crypto.IncorrectPasswordException; import bisq.common.handlers.ResultHandler; import bisq.common.persistence.PersistenceManager; @@ -51,8 +52,11 @@ import com.google.inject.Guice; import com.google.inject.Injector; import java.io.Console; + import java.util.Arrays; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; @@ -135,7 +139,6 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven // thread the application is running and we don't run into thread interference. protected abstract void launchApplication(); - /////////////////////////////////////////////////////////////////////////////////////////// // If application is a JavaFX application we need wait for onApplicationLaunched /////////////////////////////////////////////////////////////////////////////////////////// @@ -165,12 +168,25 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven }); // Attempt to login, subclasses should implement interactive login and or rpc login. - if (!isReadOnly && loginAccount()) { - readAllPersisted(this::startApplication); - } else { - log.warn("Running application in readonly mode"); - startApplication(); - } + CompletableFuture loginFuture = loginAccount(); + loginFuture.whenComplete((result, throwable) -> { + if (throwable != null) { + log.error("Error logging in to account", throwable); + shutDownNoPersist(null, false); + return; + } + try { + if (!isReadOnly && loginFuture.get()) { + readAllPersisted(this::startApplication); + } else { + log.warn("Running application in readonly mode"); + startApplication(); + } + } catch (InterruptedException | ExecutionException e) { + log.error("An error occurred: {}", e.getMessage()); + e.printStackTrace(); + } + }); } /** @@ -199,19 +215,26 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven * * @return true if account is opened successfully. */ - protected boolean loginAccount() { + protected CompletableFuture loginAccount() { + CompletableFuture result = new CompletableFuture<>(); if (accountService.accountExists()) { log.info("Account already exists, attempting to open"); try { accountService.openAccount(null); + result.complete(accountService.isAccountOpen()); } catch (IncorrectPasswordException ipe) { log.info("Account password protected, password required"); + result.complete(false); } } else if (!config.passwordRequired) { log.info("Creating Haveno account with null password"); accountService.createAccount(null); + result.complete(accountService.isAccountOpen()); + } else { + log.info("Account does not exist and password is required"); + result.complete(false); } - return accountService.isAccountOpen(); + return result; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java index 5b22c8760a..4280525ad5 100644 --- a/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/HavenoHeadlessApp.java @@ -83,7 +83,6 @@ public class HavenoHeadlessApp implements HeadlessApp { bisqSetup.setChainFileLockedExceptionHandler(msg -> log.error("onChainFileLockedExceptionHandler: msg={}", msg)); bisqSetup.setLockedUpFundsHandler(msg -> log.info("onLockedUpFundsHandler: msg={}", msg)); bisqSetup.setShowFirstPopupIfResyncSPVRequestedHandler(() -> log.info("onShowFirstPopupIfResyncSPVRequestedHandler")); - bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> log.info("onRequestWalletPasswordHandler")); bisqSetup.setDisplayUpdateHandler((alert, key) -> log.info("onDisplayUpdateHandler")); bisqSetup.setDisplayAlertHandler(alert -> log.info("onDisplayAlertHandler. alert={}", alert)); bisqSetup.setDisplayPrivateNotificationHandler(privateNotification -> log.info("onDisplayPrivateNotificationHandler. privateNotification={}", privateNotification)); diff --git a/core/src/main/java/bisq/core/app/HavenoSetup.java b/core/src/main/java/bisq/core/app/HavenoSetup.java index 3bcdcb3d17..58f5074a0a 100644 --- a/core/src/main/java/bisq/core/app/HavenoSetup.java +++ b/core/src/main/java/bisq/core/app/HavenoSetup.java @@ -83,8 +83,6 @@ import javafx.beans.value.ChangeListener; import javafx.collections.SetChangeListener; -import org.bouncycastle.crypto.params.KeyParameter; - import java.io.File; import java.io.FileNotFoundException; import java.io.FileWriter; @@ -160,9 +158,6 @@ public class HavenoSetup { private Runnable showFirstPopupIfResyncSPVRequestedHandler; @Setter @Nullable - private Consumer> requestWalletPasswordHandler; - @Setter - @Nullable private Consumer displayAlertHandler; @Setter @Nullable @@ -215,30 +210,30 @@ public class HavenoSetup { @Inject public HavenoSetup(DomainInitialisation domainInitialisation, - P2PNetworkSetup p2PNetworkSetup, - WalletAppSetup walletAppSetup, - WalletsManager walletsManager, - WalletsSetup walletsSetup, - XmrWalletService xmrWalletService, - BtcWalletService btcWalletService, - P2PService p2PService, - PrivateNotificationManager privateNotificationManager, - SignedWitnessStorageService signedWitnessStorageService, - TradeManager tradeManager, - OpenOfferManager openOfferManager, - Preferences preferences, - User user, - AlertManager alertManager, - Config config, - AccountAgeWitnessService accountAgeWitnessService, - TorSetup torSetup, - @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, - LocalBitcoinNode localBitcoinNode, - AppStartupState appStartupState, - Socks5ProxyProvider socks5ProxyProvider, - MediationManager mediationManager, - RefundManager refundManager, - ArbitrationManager arbitrationManager) { + P2PNetworkSetup p2PNetworkSetup, + WalletAppSetup walletAppSetup, + WalletsManager walletsManager, + WalletsSetup walletsSetup, + XmrWalletService xmrWalletService, + BtcWalletService btcWalletService, + P2PService p2PService, + PrivateNotificationManager privateNotificationManager, + SignedWitnessStorageService signedWitnessStorageService, + TradeManager tradeManager, + OpenOfferManager openOfferManager, + Preferences preferences, + User user, + AlertManager alertManager, + Config config, + AccountAgeWitnessService accountAgeWitnessService, + TorSetup torSetup, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, + LocalBitcoinNode localBitcoinNode, + AppStartupState appStartupState, + Socks5ProxyProvider socks5ProxyProvider, + MediationManager mediationManager, + RefundManager refundManager, + ArbitrationManager arbitrationManager) { this.domainInitialisation = domainInitialisation; this.p2PNetworkSetup = p2PNetworkSetup; this.walletAppSetup = walletAppSetup; @@ -264,6 +259,8 @@ public class HavenoSetup { this.refundManager = refundManager; this.arbitrationManager = arbitrationManager; + xmrWalletService.setHavenoSetup(this); + MemPoolSpaceTxBroadcaster.init(socks5ProxyProvider, preferences, localBitcoinNode); } @@ -411,9 +408,10 @@ public class HavenoSetup { return; } log.warn("startupTimeout called"); - if (walletsManager.areWalletsEncrypted()) - walletInitialized.addListener(walletInitializedListener); - else if (displayTorNetworkSettingsHandler != null) + //TODO (niyid) This has a part to play in the display of the password prompt +// if (walletsManager.areWalletsEncrypted()) +// walletInitialized.addListener(walletInitializedListener); + if (displayTorNetworkSettingsHandler != null) displayTorNetworkSettingsHandler.accept(true); log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG to show more details for " + @@ -453,35 +451,11 @@ public class HavenoSetup { private void initWallet() { log.info("Init wallet"); havenoSetupListeners.forEach(HavenoSetupListener::onInitWallet); - Runnable walletPasswordHandler = () -> { - log.info("Wallet password required"); - havenoSetupListeners.forEach(HavenoSetupListener::onRequestWalletPassword); - if (p2pNetworkReady.get()) - p2PNetworkSetup.setSplashP2PNetworkAnimationVisible(true); - - if (requestWalletPasswordHandler != null) { - requestWalletPasswordHandler.accept(aesKey -> { - walletsManager.setAesKey(aesKey); - walletsSetup.getWalletConfig().maybeAddSegwitKeychain(walletsSetup.getWalletConfig().btcWallet(), - aesKey); - if (getResyncSpvSemaphore()) { - if (showFirstPopupIfResyncSPVRequestedHandler != null) - showFirstPopupIfResyncSPVRequestedHandler.run(); - } else { - // TODO no guarantee here that the wallet is really fully initialized - // We would need a new walletInitializedButNotEncrypted state to track - // Usually init is fast and we have our wallet initialized at that state though. - walletInitialized.set(true); - } - }); - } - }; walletAppSetup.init(chainFileLockedExceptionHandler, spvFileCorruptedHandler, getResyncSpvSemaphore(), showFirstPopupIfResyncSPVRequestedHandler, showPopupIfInvalidBtcConfigHandler, - walletPasswordHandler, () -> { if (allBasicServicesInitialized) { checkForLockedUpFunds(); @@ -867,5 +841,7 @@ public class HavenoSetup { return p2PNetworkSetup.getP2pNetworkLabelId(); } - + public BooleanProperty getWalletInitialized() { + return walletInitialized; + } } diff --git a/core/src/main/java/bisq/core/app/WalletAppSetup.java b/core/src/main/java/bisq/core/app/WalletAppSetup.java index 3f0c13c787..c934817ac3 100644 --- a/core/src/main/java/bisq/core/app/WalletAppSetup.java +++ b/core/src/main/java/bisq/core/app/WalletAppSetup.java @@ -108,7 +108,6 @@ public class WalletAppSetup { boolean isSpvResyncRequested, @Nullable Runnable showFirstPopupIfResyncSPVRequestedHandler, @Nullable Runnable showPopupIfInvalidBtcConfigHandler, - Runnable walletPasswordHandler, Runnable downloadCompleteHandler, Runnable walletInitializedHandler) { log.info("Initialize WalletAppSetup with BitcoinJ version {} and hash of BitcoinJ commit {}", @@ -171,17 +170,7 @@ public class WalletAppSetup { walletsSetup.initialize(null, () -> { - // We only check one wallet as we apply encryption to all or none - if (walletsManager.areWalletsEncrypted() && !coreContext.isApiUser()) { - walletPasswordHandler.run(); - } else { - if (isSpvResyncRequested && !coreContext.isApiUser()) { - if (showFirstPopupIfResyncSPVRequestedHandler != null) - showFirstPopupIfResyncSPVRequestedHandler.run(); - } else { - walletInitializedHandler.run(); - } - } + walletInitializedHandler.run(); }, exception -> { if (exception instanceof InvalidHostException && showPopupIfInvalidBtcConfigHandler != null) { diff --git a/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java b/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java index 15f323498d..5e4a979ea0 100644 --- a/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java +++ b/core/src/main/java/bisq/core/btc/model/EncryptedConnectionList.java @@ -307,7 +307,7 @@ public class EncryptedConnectionList implements PersistableEnvelope, PersistedDa try { return Encryption.decrypt(encrypted, secret); } catch (CryptoException e) { - throw new IllegalArgumentException("Illegal old password", e); + throw new IllegalArgumentException("Incorrect password", e); } } diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java index 6bb8e44591..6a6ec321e5 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletsManager.java @@ -23,8 +23,6 @@ import bisq.common.crypto.ScryptUtil; import bisq.common.handlers.ExceptionHandler; import bisq.common.handlers.ResultHandler; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Transaction; import org.bitcoinj.crypto.KeyCrypter; import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.wallet.DeterministicSeed; diff --git a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java index 7c81379b00..b5a4b5e832 100644 --- a/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/XmrWalletService.java @@ -10,6 +10,7 @@ import bisq.common.util.Utilities; import bisq.core.api.AccountServiceListener; import bisq.core.api.CoreAccountService; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.app.HavenoSetup; import bisq.core.btc.listeners.XmrBalanceListener; import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntryList; @@ -92,6 +93,7 @@ public class XmrWalletService { private final CoreMoneroConnectionsService connectionsService; private final XmrAddressEntryList xmrAddressEntryList; private final WalletsSetup walletsSetup; + private final File walletDir; private final File xmrWalletFile; private final int rpcBindPort; @@ -103,6 +105,8 @@ public class XmrWalletService { private final Map> txCache = new HashMap>(); private boolean isShutDown = false; + private HavenoSetup havenoSetup; + @Inject XmrWalletService(CoreAccountService accountService, CoreMoneroConnectionsService connectionsService, @@ -148,8 +152,8 @@ public class XmrWalletService { @Override public void onPasswordChanged(String oldPassword, String newPassword) { log.info(getClass() + "accountservice.onPasswordChanged()"); - if (oldPassword == null) oldPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; - if (newPassword == null) newPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; + if (oldPassword == null || oldPassword.isEmpty()) oldPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; + if (newPassword == null || newPassword.isEmpty()) newPassword = MONERO_WALLET_RPC_DEFAULT_PASSWORD; changeWalletPasswords(oldPassword, newPassword); } }); @@ -383,41 +387,41 @@ public class XmrWalletService { // verify tx not submitted to pool MoneroTx tx = daemon.getTx(txHash); if (tx != null) throw new RuntimeException("Tx is already submitted"); - + // submit tx to pool MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency? if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result)); tx = getTx(txHash); - + // verify key images if (keyImages != null) { Set txKeyImages = new HashSet(); for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex()); if (!txKeyImages.equals(new HashSet(keyImages))) throw new Error("Tx inputs do not match claimed key images"); } - + // verify unlock height if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0"); - + // verify trade fee String feeAddress = HavenoUtils.getTradeFeeAddress(); MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress); if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee"); if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount()); - + // verify miner fee BigInteger feeEstimate = getFeeEstimate(tx.getWeight()); double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal? if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee()); log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff); - + // verify sufficient security deposit check = wallet.checkTxKey(txHash, txKey, address); if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount"); BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger(); BigInteger actualSecurityDeposit = check.getReceivedAmount().subtract(sendAmount); if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit); - + // verify deposit amount + miner fee within dust tolerance BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger()); BigInteger actualDepositAndFee = check.getReceivedAmount().add(tx.getFee()); @@ -558,6 +562,9 @@ public class XmrWalletService { System.out.println("Monero wallet balance: " + wallet.getBalance(0)); System.out.println("Monero wallet unlocked balance: " + wallet.getUnlockedBalance(0)); + // notify setup that main wallet is initialized + havenoSetup.getWalletInitialized().set(true); // TODO: change to listener pattern? + // register internal listener to notify external listeners wallet.addListener(new XmrWalletListener()); } @@ -961,6 +968,10 @@ public class XmrWalletService { log.info("\n" + tracePrefix + ":" + sb.toString()); } + public void setHavenoSetup(HavenoSetup havenoSetup) { + this.havenoSetup = havenoSetup; + } + // -------------------------------- HELPERS ------------------------------- /** diff --git a/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java b/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java index e58ed5aa9e..e26bccb9f7 100644 --- a/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java +++ b/daemon/src/main/java/bisq/daemon/app/HavenoDaemonMain.java @@ -17,11 +17,10 @@ package bisq.daemon.app; -import bisq.core.app.ConsoleInput; -import bisq.core.app.HavenoHeadlessAppMain; -import bisq.core.app.HavenoSetup; import bisq.core.api.AccountServiceListener; +import bisq.core.app.ConsoleInput; import bisq.core.app.CoreModule; +import bisq.core.app.HavenoHeadlessAppMain; import bisq.common.UserThread; import bisq.common.app.AppModule; @@ -32,7 +31,10 @@ import bisq.common.persistence.PersistenceManager; import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.Console; + import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -43,7 +45,7 @@ import lombok.extern.slf4j.Slf4j; import bisq.daemon.grpc.GrpcServer; @Slf4j -public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSetup.HavenoSetupListener { +public class HavenoDaemonMain extends HavenoHeadlessAppMain { private GrpcServer grpcServer; @@ -139,53 +141,60 @@ public class HavenoDaemonMain extends HavenoHeadlessAppMain implements HavenoSet * Start the grpcServer to allow logging in remotely. */ @Override - protected boolean loginAccount() { - boolean opened = super.loginAccount(); + protected CompletableFuture loginAccount() { + CompletableFuture opened = super.loginAccount(); // Start rpc server in case login is coming in from rpc grpcServer = injector.getInstance(GrpcServer.class); - if (!opened) { - // Nonblocking, we need to stop if the login occurred through rpc. - // TODO: add a mode to mask password - ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); - Thread t = new Thread(() -> { - interactiveLogin(reader); - }); + CompletableFuture inputResult = new CompletableFuture(); + try { + if (opened.get()) { + grpcServer.start(); + return opened; + } else { - // Handle asynchronous account opens. - // Will need to also close and reopen account. - AccountServiceListener accountListener = new AccountServiceListener() { - @Override public void onAccountCreated() { onLogin(); } - @Override public void onAccountOpened() { onLogin(); } - private void onLogin() { - log.info("Logged in successfully"); - reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread + // Nonblocking, we need to stop if the login occurred through rpc. + // TODO: add a mode to mask password + ConsoleInput reader = new ConsoleInput(Integer.MAX_VALUE, Integer.MAX_VALUE, TimeUnit.MILLISECONDS); + Thread t = new Thread(() -> { + interactiveLogin(reader); + }); + + // Handle asynchronous account opens. + // Will need to also close and reopen account. + AccountServiceListener accountListener = new AccountServiceListener() { + @Override public void onAccountCreated() { onLogin(); } + @Override public void onAccountOpened() { onLogin(); } + private void onLogin() { + log.info("Logged in successfully"); + reader.cancel(); // closing the reader will stop all read attempts and end the interactive login thread + } + }; + accountService.addListener(accountListener); + + // start server after the listener is registered + grpcServer.start(); + + try { + // Wait until interactive login or rpc. Check one more time if account is open to close race condition. + if (!accountService.isAccountOpen()) { + log.info("Interactive login required"); + t.start(); + t.join(); + } + } catch (InterruptedException e) { + // expected } - }; - accountService.addListener(accountListener); - // start server after the listener is registered - grpcServer.start(); - - try { - // Wait until interactive login or rpc. Check one more time if account is open to close race condition. - if (!accountService.isAccountOpen()) { - log.info("Interactive login required"); - t.start(); - t.join(); - } - } catch (InterruptedException e) { - // expected + accountService.removeListener(accountListener); + inputResult.complete(accountService.isAccountOpen()); } - - accountService.removeListener(accountListener); - opened = accountService.isAccountOpen(); - } else { - grpcServer.start(); + } catch (InterruptedException | ExecutionException e) { + inputResult.completeExceptionally(e); } - return opened; + return inputResult; } /** diff --git a/desktop/src/main/java/bisq/desktop/app/HavenoApp.java b/desktop/src/main/java/bisq/desktop/app/HavenoApp.java index ffcbbf46b8..b9d32ce727 100644 --- a/desktop/src/main/java/bisq/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/bisq/desktop/app/HavenoApp.java @@ -67,8 +67,8 @@ import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; -import javafx.geometry.Rectangle2D; import javafx.geometry.BoundingBox; +import javafx.geometry.Rectangle2D; import java.util.ArrayList; import java.util.List; @@ -128,6 +128,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { } public void startApplication(Runnable onApplicationStartedHandler) { + log.info("Running startApplication..."); try { mainView = loadMainView(injector); mainView.setOnApplicationStartedHandler(onApplicationStartedHandler); @@ -149,7 +150,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { .show(); new Thread(() -> { gracefulShutDownHandler.gracefulShutDown(() -> { - log.debug("App shutdown complete"); + log.info("App shutdown complete"); }); }).start(); shutDownRequested = true; diff --git a/desktop/src/main/java/bisq/desktop/app/HavenoAppMain.java b/desktop/src/main/java/bisq/desktop/app/HavenoAppMain.java index 0258513d9d..1bc6738835 100644 --- a/desktop/src/main/java/bisq/desktop/app/HavenoAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/HavenoAppMain.java @@ -20,6 +20,7 @@ package bisq.desktop.app; import bisq.desktop.common.UITimer; import bisq.desktop.common.view.guice.InjectorViewFactory; import bisq.desktop.setup.DesktopPersistedDataHost; +import bisq.desktop.util.ImageUtil; import bisq.core.app.AvoidStandbyModeService; import bisq.core.app.HavenoExecutable; @@ -27,10 +28,25 @@ import bisq.core.app.HavenoExecutable; import bisq.common.UserThread; import bisq.common.app.AppModule; import bisq.common.app.Version; - +import bisq.common.crypto.IncorrectPasswordException; import javafx.application.Application; import javafx.application.Platform; +import javafx.stage.Stage; + +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + import lombok.extern.slf4j.Slf4j; @Slf4j @@ -139,4 +155,86 @@ public class HavenoAppMain extends HavenoExecutable { // Therefore, calling this as part of onApplicationStarted() log.info("Using JavaFX {}", System.getProperty("javafx.version")); } + + @Override + protected CompletableFuture loginAccount() { + + // attempt default login + CompletableFuture result = super.loginAccount(); + try { + if (result.get()) return result; + } catch (InterruptedException | ExecutionException e) { + throw new IllegalStateException(e); + } + + // login using dialog + CompletableFuture dialogResult = new CompletableFuture<>(); + Platform.setImplicitExit(false); + Platform.runLater(() -> { + + // show password dialog until account open + String errorMessage = null; + while (!accountService.isAccountOpen()) { + + // create the password dialog + PasswordDialog passwordDialog = new PasswordDialog(errorMessage); + + // wait for user to enter password + Optional passwordResult = passwordDialog.showAndWait(); + if (passwordResult.isPresent()) { + try { + accountService.openAccount(passwordResult.get()); + dialogResult.complete(accountService.isAccountOpen()); + } catch (IncorrectPasswordException e) { + errorMessage = "Incorrect password"; + } + } else { + // if the user cancelled the dialog, complete the passwordFuture exceptionally + dialogResult.completeExceptionally(new Exception("Password dialog cancelled")); + break; + } + } + }); + return dialogResult; + } + + private class PasswordDialog extends Dialog { + + public PasswordDialog(String errorMessage) { + setTitle("Enter Password"); + setHeaderText("Please enter your Haveno password:"); + + // Add an icon to the dialog + Stage stage = (Stage) getDialogPane().getScene().getWindow(); + stage.getIcons().add(ImageUtil.getImageByPath("lock.png")); + + // Create the password field + PasswordField passwordField = new PasswordField(); + passwordField.setPromptText("Password"); + + // Create the error message field + Label errorMessageField = new Label(errorMessage); + errorMessageField.setTextFill(Color.color(1, 0, 0)); + + // Set the dialog content + VBox vbox = new VBox(10); + vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField); + getDialogPane().setContent(vbox); + + // Add OK and Cancel buttons + ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE); + ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE); + getDialogPane().getButtonTypes().addAll(okButton, cancelButton); + + // Convert the result to a string when the OK button is clicked + setResultConverter(buttonType -> { + if (buttonType == okButton) { + return passwordField.getText(); + } else { + new Thread(() -> HavenoApp.getShutDownHandler().run()).start(); + return null; + } + }); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index f61ef28a78..28771a2f38 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -18,7 +18,6 @@ package bisq.desktop.main; import bisq.desktop.Navigation; -import bisq.desktop.app.HavenoApp; import bisq.desktop.common.model.ViewModel; import bisq.desktop.components.TxIdTextField; import bisq.desktop.main.account.AccountView; @@ -109,7 +108,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener { - private final HavenoSetup bisqSetup; + private final HavenoSetup havenoSetup; private final CoreMoneroConnectionsService connectionService; private final User user; private final BalancePresentation balancePresentation; @@ -154,7 +153,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public MainViewModel(HavenoSetup bisqSetup, + public MainViewModel(HavenoSetup havenoSetup, CoreMoneroConnectionsService connectionService, XmrWalletService xmrWalletService, User user, @@ -179,7 +178,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener TorNetworkSettingsWindow torNetworkSettingsWindow, CorruptedStorageFileHandler corruptedStorageFileHandler, Navigation navigation) { - this.bisqSetup = bisqSetup; + this.havenoSetup = havenoSetup; this.connectionService = connectionService; this.user = user; this.balancePresentation = balancePresentation; @@ -211,7 +210,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener GUIUtil.setPreferences(preferences); setupHandlers(); - bisqSetup.addHavenoSetupListener(this); + havenoSetup.addHavenoSetupListener(this); } @@ -303,7 +302,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener } void onOpenDownloadWindow() { - bisqSetup.displayAlertIfPresent(user.getDisplayedAlert(), true); + havenoSetup.displayAlertIfPresent(user.getDisplayedAlert(), true); } void setPriceFeedComboBoxItem(PriceFeedComboBoxItem item) { @@ -316,34 +315,30 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener /////////////////////////////////////////////////////////////////////////////////////////// private void setupHandlers() { - bisqSetup.setDisplayTacHandler(acceptedHandler -> UserThread.runAfter(() -> { + havenoSetup.setDisplayTacHandler(acceptedHandler -> UserThread.runAfter(() -> { //noinspection FunctionalExpressionCanBeFolded tacWindow.onAction(acceptedHandler::run).show(); }, 1)); - bisqSetup.setDisplayTorNetworkSettingsHandler(show -> { + havenoSetup.setDisplayTorNetworkSettingsHandler(show -> { if (show) { torNetworkSettingsWindow.show(); } else if (torNetworkSettingsWindow.isDisplayed()) { torNetworkSettingsWindow.hide(); } }); - bisqSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) + havenoSetup.setSpvFileCorruptedHandler(msg -> new Popup().warning(msg) .actionButtonText(Res.get("settings.net.reSyncSPVChainButton")) .onAction(() -> GUIUtil.reSyncSPVChain(preferences)) .show()); - bisqSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) + havenoSetup.setChainFileLockedExceptionHandler(msg -> new Popup().warning(msg) .useShutDownButton() .show()); - bisqSetup.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); - bisqSetup.setShowFirstPopupIfResyncSPVRequestedHandler(this::showFirstPopupIfResyncSPVRequested); - bisqSetup.setRequestWalletPasswordHandler(aesKeyHandler -> walletPasswordWindow - .onAesKey(aesKeyHandler::accept) - .onClose(() -> HavenoApp.getShutDownHandler().run()) - .show()); + havenoSetup.setLockedUpFundsHandler(msg -> new Popup().width(850).warning(msg).show()); + havenoSetup.setShowFirstPopupIfResyncSPVRequestedHandler(this::showFirstPopupIfResyncSPVRequested); - bisqSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config) + havenoSetup.setDisplayUpdateHandler((alert, key) -> new DisplayUpdateDownloadWindow(alert, config) .actionButtonText(Res.get("displayUpdateDownloadWindow.button.downloadLater")) .onAction(() -> { preferences.dontShowAgain(key, false); // update later @@ -353,19 +348,19 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener preferences.dontShowAgain(key, true); // ignore update }) .show()); - bisqSetup.setDisplayAlertHandler(alert -> new DisplayAlertMessageWindow() + havenoSetup.setDisplayAlertHandler(alert -> new DisplayAlertMessageWindow() .alertMessage(alert) .closeButtonText(Res.get("shared.close")) .onClose(() -> user.setDisplayedAlert(alert)) .show()); - bisqSetup.setDisplayPrivateNotificationHandler(privateNotification -> + havenoSetup.setDisplayPrivateNotificationHandler(privateNotification -> new Popup().headLine(Res.get("popup.privateNotification.headline")) .attention(privateNotification.getMessage()) .onClose(privateNotificationManager::removePrivateNotification) .useIUnderstandButton() .show()); - bisqSetup.setDisplaySecurityRecommendationHandler(key -> {}); - bisqSetup.setDisplayLocalhostHandler(key -> { + havenoSetup.setDisplaySecurityRecommendationHandler(key -> {}); + havenoSetup.setDisplayLocalhostHandler(key -> { if (!DevEnv.isDevMode()) { Popup popup = new Popup().backgroundInfo(Res.get("popup.bitcoinLocalhostNode.msg")) .dontShowAgainId(key); @@ -373,31 +368,31 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener popupQueue.add(popup); } }); - bisqSetup.setDisplaySignedByArbitratorHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + havenoSetup.setDisplaySignedByArbitratorHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.signedByArbitrator")); - bisqSetup.setDisplaySignedByPeerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + havenoSetup.setDisplaySignedByPeerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.signedByPeer", String.valueOf(SignedWitnessService.SIGNER_AGE_DAYS))); - bisqSetup.setDisplayPeerLimitLiftedHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + havenoSetup.setDisplayPeerLimitLiftedHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.peerLimitLifted")); - bisqSetup.setDisplayPeerSignerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( + havenoSetup.setDisplayPeerSignerHandler(key -> accountPresentation.showOneTimeAccountSigningPopup( key, "popup.accountSigning.peerSigner")); - bisqSetup.setWrongOSArchitectureHandler(msg -> new Popup().warning(msg).show()); + havenoSetup.setWrongOSArchitectureHandler(msg -> new Popup().warning(msg).show()); - bisqSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); + havenoSetup.setRejectedTxErrorMessageHandler(msg -> new Popup().width(850).warning(msg).show()); - bisqSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); + havenoSetup.setShowPopupIfInvalidBtcConfigHandler(this::showPopupIfInvalidBtcConfig); - bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { + havenoSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> { // We copy the array as we will mutate it later showRevolutAccountUpdateWindow(new ArrayList<>(revolutAccountList)); }); - bisqSetup.setAmazonGiftCardAccountsUpdateHandler(amazonGiftCardAccountList -> { + havenoSetup.setAmazonGiftCardAccountsUpdateHandler(amazonGiftCardAccountList -> { // We copy the array as we will mutate it later showAmazonGiftCardAccountUpdateWindow(new ArrayList<>(amazonGiftCardAccountList)); }); - bisqSetup.setOsxKeyLoggerWarningHandler(() -> { }); - bisqSetup.setQubesOSInfoHandler(() -> { + havenoSetup.setOsxKeyLoggerWarningHandler(() -> { }); + havenoSetup.setQubesOSInfoHandler(() -> { String key = "qubesOSSetupInfo"; if (preferences.showAgain(key)) { new Popup().information(Res.get("popup.info.qubesOSSetupInfo")) @@ -407,14 +402,14 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener } }); - bisqSetup.setDownGradePreventionHandler(lastVersion -> { + havenoSetup.setDownGradePreventionHandler(lastVersion -> { new Popup().warning(Res.get("popup.warn.downGradePrevention", lastVersion, Version.VERSION)) .useShutDownButton() .hideCloseButton() .show(); }); - bisqSetup.setTorAddressUpgradeHandler(() -> new Popup().information(Res.get("popup.info.torMigration.msg")) + havenoSetup.setTorAddressUpgradeHandler(() -> new Popup().information(Res.get("popup.info.torMigration.msg")) .actionButtonTextWithGoTo("navigation.account.backup") .onAction(() -> { navigation.setReturnPath(navigation.getCurrentPath()); @@ -430,9 +425,9 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener .warning(Res.get("popup.error.takeOfferRequestFailed", errorMessage)) .show()); - bisqSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); + havenoSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> updateBtcSyncProgress()); - bisqSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); + havenoSetup.setFilterWarningHandler(warning -> new Popup().warning(warning).show()); this.footerVersionInfo.setValue("v" + Version.VERSION); this.getNewVersionAvailableProperty().addListener((observable, oldValue, newValue) -> { @@ -538,10 +533,10 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener private void showFirstPopupIfResyncSPVRequested() { Popup firstPopup = new Popup(); firstPopup.information(Res.get("settings.net.reSyncSPVAfterRestart")).show(); - if (bisqSetup.getBtcSyncProgress().get() == 1) { + if (havenoSetup.getBtcSyncProgress().get() == 1) { showSecondPopupIfResyncSPVRequested(firstPopup); } else { - bisqSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> { + havenoSetup.getBtcSyncProgress().addListener((observable, oldValue, newValue) -> { if ((double) newValue == 1) showSecondPopupIfResyncSPVRequested(firstPopup); }); @@ -596,7 +591,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener } private void updateBtcSyncProgress() { - final DoubleProperty btcSyncProgress = bisqSetup.getBtcSyncProgress(); + final DoubleProperty btcSyncProgress = havenoSetup.getBtcSyncProgress(); combinedSyncProgress.set(btcSyncProgress.doubleValue()); } @@ -635,7 +630,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener /////////////////////////////////////////////////////////////////////////////////////////// BooleanProperty getNewVersionAvailableProperty() { - return bisqSetup.getNewVersionAvailableProperty(); + return havenoSetup.getNewVersionAvailableProperty(); } StringProperty getNumOpenSupportTickets() { @@ -670,7 +665,7 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener // Wallet StringProperty getBtcInfo() { final StringProperty combinedInfo = new SimpleStringProperty(); - combinedInfo.bind(bisqSetup.getBtcInfo()); + combinedInfo.bind(havenoSetup.getBtcInfo()); return combinedInfo; } @@ -685,44 +680,44 @@ public class MainViewModel implements ViewModel, HavenoSetup.HavenoSetupListener } StringProperty getWalletServiceErrorMsg() { - return bisqSetup.getWalletServiceErrorMsg(); + return havenoSetup.getWalletServiceErrorMsg(); } StringProperty getBtcSplashSyncIconId() { - return bisqSetup.getBtcSplashSyncIconId(); + return havenoSetup.getBtcSplashSyncIconId(); } BooleanProperty getUseTorForBTC() { - return bisqSetup.getUseTorForBTC(); + return havenoSetup.getUseTorForBTC(); } // P2P StringProperty getP2PNetworkInfo() { - return bisqSetup.getP2PNetworkInfo(); + return havenoSetup.getP2PNetworkInfo(); } BooleanProperty getSplashP2PNetworkAnimationVisible() { - return bisqSetup.getSplashP2PNetworkAnimationVisible(); + return havenoSetup.getSplashP2PNetworkAnimationVisible(); } StringProperty getP2pNetworkWarnMsg() { - return bisqSetup.getP2pNetworkWarnMsg(); + return havenoSetup.getP2pNetworkWarnMsg(); } StringProperty getP2PNetworkIconId() { - return bisqSetup.getP2PNetworkIconId(); + return havenoSetup.getP2PNetworkIconId(); } StringProperty getP2PNetworkStatusIconId() { - return bisqSetup.getP2PNetworkStatusIconId(); + return havenoSetup.getP2PNetworkStatusIconId(); } BooleanProperty getUpdatedDataReceived() { - return bisqSetup.getUpdatedDataReceived(); + return havenoSetup.getUpdatedDataReceived(); } StringProperty getP2pNetworkLabelId() { - return bisqSetup.getP2pNetworkLabelId(); + return havenoSetup.getP2pNetworkLabelId(); } // marketPricePresentation diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java index 937a8b3801..9ea856830a 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/password/PasswordView.java @@ -27,16 +27,15 @@ import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.MainView; import bisq.desktop.main.account.AccountView; import bisq.desktop.main.account.content.backup.BackupView; -import bisq.desktop.main.account.content.seedwords.SeedWordsView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.Layout; import bisq.desktop.util.validation.PasswordValidator; + +import bisq.core.api.CoreAccountService; import bisq.core.btc.wallet.WalletsManager; import bisq.core.locale.Res; -import bisq.common.crypto.ScryptUtil; -import bisq.common.util.Tuple4; -import org.bitcoinj.crypto.KeyCrypterScrypt; +import bisq.common.util.Tuple4; import javax.inject.Inject; @@ -61,6 +60,7 @@ public class PasswordView extends ActivatableView { private final WalletsManager walletsManager; private final PasswordValidator passwordValidator; private final Navigation navigation; + private final CoreAccountService accountService; private PasswordTextField passwordField; private PasswordTextField repeatedPasswordField; @@ -77,10 +77,11 @@ public class PasswordView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - private PasswordView(WalletsManager walletsManager, PasswordValidator passwordValidator, Navigation navigation) { + private PasswordView(CoreAccountService accountService, WalletsManager walletsManager, PasswordValidator passwordValidator, Navigation navigation) { this.walletsManager = walletsManager; this.passwordValidator = passwordValidator; this.navigation = navigation; + this.accountService = accountService; } @Override @@ -141,41 +142,38 @@ public class PasswordView extends ActivatableView { deriveStatusLabel.setText(Res.get("password.deriveKey")); busyAnimation.play(); - KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); - ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> { - deriveStatusLabel.setText(""); - busyAnimation.stop(); - - if (walletsManager.areWalletsEncrypted()) { - if (walletsManager.checkAESKey(aesKey)) { - walletsManager.decryptWallets(aesKey); - new Popup() - .feedback(Res.get("password.walletDecrypted")) - .show(); - backupWalletAndResetFields(); - } else { - pwButton.setDisable(false); - new Popup() - .warning(Res.get("password.wrongPw")) - .show(); - } - } else { - try { - walletsManager.encryptWallets(keyCrypterScrypt, aesKey); - new Popup() - .feedback(Res.get("password.walletEncrypted")) - .show(); - backupWalletAndResetFields(); - walletsManager.clearBackup(); - } catch (Throwable t) { - new Popup() - .warning(Res.get("password.walletEncryptionFailed")) - .show(); - } + if (walletsManager.areWalletsEncrypted()) { + try { + accountService.changePassword(password, null); + new Popup() + .feedback(Res.get("password.walletDecrypted")) + .show(); + backupWalletAndResetFields(); + } catch (Throwable t) { + pwButton.setDisable(false); + new Popup() + .warning(Res.get("password.wrongPw")) + .show(); } - setText(); - updatePasswordListeners(); - }); + } else { + try { + accountService.changePassword(accountService.getPassword(), password); + new Popup() + .feedback(Res.get("password.walletEncrypted")) + .show(); + backupWalletAndResetFields(); + walletsManager.clearBackup(); + } catch (Throwable t) { + new Popup() + .warning(Res.get("password.walletEncryptionFailed")) + .show(); + } + } + setText(); + updatePasswordListeners(); + + deriveStatusLabel.setText(""); + busyAnimation.stop(); } private void backupWalletAndResetFields() { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java index 569c7fe737..4224c37e7d 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/seedwords/SeedWordsView.java @@ -218,7 +218,7 @@ public class SeedWordsView extends ActivatableView { } private void askForPassword() { - walletPasswordWindow.headLine(Res.get("account.seed.enterPw")).onAesKey(aesKey -> { + walletPasswordWindow.headLine(Res.get("account.seed.enterPw")).onSuccess(() -> { initSeedWords(xmrWalletService.getWallet().getMnemonic()); showSeedScreen(); }).hideForgotPasswordButton().show(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java index 9b55d67b49..05b9364746 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/BtcEmptyWalletWindow.java @@ -121,14 +121,7 @@ public final class BtcEmptyWalletWindow extends Overlay { emptyWalletButton.setDisable(!isBalanceSufficient && addressInputTextField.getText().length() > 0); emptyWalletButton.setOnAction(e -> { if (addressInputTextField.getText().length() > 0 && isBalanceSufficient) { - if (btcWalletService.isEncrypted()) { - walletPasswordWindow - .onAesKey(this::doEmptyWallet) - .onClose(this::blurAgain) - .show(); - } else { - doEmptyWallet(null); - } + log.warn(getClass().getSimpleName() + ".addContent() needs updated for XMR"); } }); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index 81128f68e3..c96b1d88e7 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -29,7 +29,6 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; -import bisq.core.support.dispute.agent.DisputeAgentLookupMap; import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.trade.Contract; import bisq.core.trade.Trade; diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java index e1d6bdba0e..8d33d1e920 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/WalletPasswordWindow.java @@ -25,18 +25,15 @@ import bisq.desktop.main.SharedPresentation; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.Layout; -import bisq.desktop.util.Transitions; - +import bisq.core.api.CoreAccountService; import bisq.core.btc.wallet.WalletsManager; import bisq.core.locale.Res; import bisq.core.offer.OpenOfferManager; -import bisq.common.UserThread; import bisq.common.config.Config; -import bisq.common.crypto.ScryptUtil; +import bisq.common.crypto.IncorrectPasswordException; import bisq.common.util.Tuple2; -import org.bitcoinj.crypto.KeyCrypterScrypt; import org.bitcoinj.crypto.MnemonicCode; import org.bitcoinj.crypto.MnemonicException; import org.bitcoinj.wallet.DeterministicSeed; @@ -65,8 +62,6 @@ import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; -import org.bouncycastle.crypto.params.KeyParameter; - import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; @@ -75,8 +70,6 @@ import java.time.ZoneOffset; import java.io.File; import java.io.IOException; -import java.util.concurrent.TimeUnit; - import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.addPasswordTextField; @@ -88,12 +81,13 @@ import static javafx.beans.binding.Bindings.createBooleanBinding; @Slf4j public class WalletPasswordWindow extends Overlay { + private final CoreAccountService accountService; private final WalletsManager walletsManager; private final OpenOfferManager openOfferManager; private File storageDir; private Button unlockButton; - private AesKeyHandler aesKeyHandler; + private WalletPasswordHandler passwordHandler; private PasswordTextField passwordTextField; private Button forgotPasswordButton; private Button restoreButton; @@ -111,14 +105,16 @@ public class WalletPasswordWindow extends Overlay { // Interface /////////////////////////////////////////////////////////////////////////////////////////// - public interface AesKeyHandler { - void onAesKey(KeyParameter aesKey); + public interface WalletPasswordHandler { + void onSuccess(); } @Inject - private WalletPasswordWindow(WalletsManager walletsManager, + private WalletPasswordWindow(CoreAccountService accountService, + WalletsManager walletsManager, OpenOfferManager openOfferManager, @Named(Config.STORAGE_DIR) File storageDir) { + this.accountService = accountService; this.walletsManager = walletsManager; this.openOfferManager = openOfferManager; this.storageDir = storageDir; @@ -149,8 +145,8 @@ public class WalletPasswordWindow extends Overlay { display(); } - public WalletPasswordWindow onAesKey(AesKeyHandler aesKeyHandler) { - this.aesKeyHandler = aesKeyHandler; + public WalletPasswordWindow onSuccess(WalletPasswordHandler passwordHandler) { + this.passwordHandler = passwordHandler; return this; } @@ -213,27 +209,16 @@ public class WalletPasswordWindow extends Overlay { unlockButton.setOnAction(e -> { String password = passwordTextField.getText(); checkArgument(password.length() < 500, Res.get("password.tooLong")); - KeyCrypterScrypt keyCrypterScrypt = walletsManager.getKeyCrypterScrypt(); - if (keyCrypterScrypt != null) { - busyAnimation.play(); - deriveStatusLabel.setText(Res.get("password.deriveKey")); - ScryptUtil.deriveKeyWithScrypt(keyCrypterScrypt, password, aesKey -> { - if (walletsManager.checkAESKey(aesKey)) { - if (aesKeyHandler != null) - aesKeyHandler.onAesKey(aesKey); - - hide(); - } else { - busyAnimation.stop(); - deriveStatusLabel.setText(""); - - UserThread.runAfter(() -> new Popup() - .warning(Res.get("password.wrongPw")) - .onClose(this::blurAgain).show(), Transitions.DEFAULT_DURATION, TimeUnit.MILLISECONDS); - } - }); - } else { - log.error("wallet.getKeyCrypter() is null, that must not happen."); + try { + accountService.verifyPassword(password); + if (passwordHandler != null) passwordHandler.onSuccess(); + hide(); + } catch (IncorrectPasswordException e2) { + busyAnimation.stop(); + deriveStatusLabel.setText(""); + new Popup() + .warning(Res.get("password.wrongPw")) + .onClose(this::blurAgain).show(); } }); diff --git a/desktop/src/main/java/bisq/desktop/util/ImageUtil.java b/desktop/src/main/java/bisq/desktop/util/ImageUtil.java index a844d01d9f..1b87c30af2 100644 --- a/desktop/src/main/java/bisq/desktop/util/ImageUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/ImageUtil.java @@ -70,6 +70,10 @@ public class ImageUtil { } } + public static Image getImageByPath(String imagePath) { + return getImageByUrl("/images/" + imagePath); + } + // determine if this is a MacOS retina display // https://stackoverflow.com/questions/20767708/how-do-you-detect-a-retina-display-in-java#20767802 public static boolean isRetina() { diff --git a/desktop/src/main/resources/images/lock.png b/desktop/src/main/resources/images/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..3a4bba5d91e1919cd73c6b5830508436da6a946b GIT binary patch literal 22292 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 D;@nZw literal 0 HcmV?d00001