support password prompt in legacy ui

Co-authored-by: niyid <neeyeed@gmail.com>
This commit is contained in:
woodser 2023-02-26 09:08:10 -05:00
parent 99653fe40d
commit 4dde53f0e8
19 changed files with 357 additions and 272 deletions

View file

@ -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

View file

@ -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<Boolean> 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<Boolean> loginAccount() {
CompletableFuture<Boolean> 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;
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -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));

View file

@ -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<Consumer<KeyParameter>> requestWalletPasswordHandler;
@Setter
@Nullable
private Consumer<Alert> 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;
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
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<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(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 -------------------------------
/**