11 changed files with 387 additions and 36 deletions
@ -0,0 +1,113 @@ |
|||
package com.sparrowwallet.sparrow.terminal.wallet; |
|||
|
|||
import com.googlecode.lanterna.TerminalSize; |
|||
import com.googlecode.lanterna.gui2.*; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.sparrow.io.Storage; |
|||
import com.sparrowwallet.sparrow.wallet.WalletForm; |
|||
|
|||
import java.text.DateFormat; |
|||
import java.text.ParseException; |
|||
import java.text.SimpleDateFormat; |
|||
import java.util.Date; |
|||
import java.util.List; |
|||
import java.util.regex.Pattern; |
|||
|
|||
public class AdvancedDialog extends WalletDialog { |
|||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); |
|||
|
|||
private final TextBox birthDate; |
|||
private final TextBox gapLimit; |
|||
private final Button apply; |
|||
|
|||
private Result result = Result.CANCEL; |
|||
|
|||
public AdvancedDialog(WalletForm walletForm) { |
|||
super(walletForm.getWallet().getFullDisplayName() + " Advanced Settings", walletForm); |
|||
|
|||
setHints(List.of(Hint.CENTERED)); |
|||
|
|||
Wallet wallet = getWalletForm().getWallet(); |
|||
|
|||
Panel mainPanel = new Panel(); |
|||
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1)); |
|||
|
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
|
|||
mainPanel.addComponent(new Label("Birth date")); |
|||
birthDate = new TextBox().setValidationPattern(Pattern.compile("[0-9\\-/]*")); |
|||
mainPanel.addComponent(birthDate); |
|||
|
|||
mainPanel.addComponent(new Label("Gap limit")); |
|||
gapLimit = new TextBox().setValidationPattern(Pattern.compile("[0-9]*")); |
|||
mainPanel.addComponent(gapLimit); |
|||
|
|||
Panel buttonPanel = new Panel(); |
|||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); |
|||
buttonPanel.addComponent(new Button("Cancel", this::onCancel)); |
|||
apply = new Button("Apply", this::onApply).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false)); |
|||
apply.setEnabled(false); |
|||
buttonPanel.addComponent(apply); |
|||
|
|||
boolean noPassword = Storage.NO_PASSWORD_KEY.equals(walletForm.getStorage().getEncryptionPubKey()); |
|||
mainPanel.addComponent(new Button(noPassword ? "Add Password" : "Change Password", this::onChangePassword)); |
|||
|
|||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); |
|||
setComponent(mainPanel); |
|||
|
|||
if(wallet.getBirthDate() != null) { |
|||
birthDate.setText(DATE_FORMAT.format(wallet.getBirthDate())); |
|||
} |
|||
|
|||
gapLimit.setText(Integer.toString(wallet.getGapLimit())); |
|||
|
|||
birthDate.setTextChangeListener((newText, changedByUserInteraction) -> { |
|||
try { |
|||
Date newDate = DATE_FORMAT.parse(newText); |
|||
wallet.setBirthDate(newDate); |
|||
apply.setEnabled(true); |
|||
} catch(ParseException e) { |
|||
//ignore
|
|||
} |
|||
}); |
|||
|
|||
gapLimit.setTextChangeListener((newText, changedByUserInteraction) -> { |
|||
try { |
|||
int newValue = Integer.parseInt(newText); |
|||
if(newValue < 0 || newValue > 1000000) { |
|||
return; |
|||
} |
|||
|
|||
wallet.setGapLimit(newValue); |
|||
apply.setEnabled(true); |
|||
} catch(NumberFormatException e) { |
|||
return; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
private void onChangePassword() { |
|||
result = Result.CHANGE_PASSWORD; |
|||
close(); |
|||
} |
|||
|
|||
private void onApply() { |
|||
result = Result.APPLY; |
|||
close(); |
|||
} |
|||
|
|||
private void onCancel() { |
|||
close(); |
|||
} |
|||
|
|||
@Override |
|||
public Object showDialog(WindowBasedTextGUI textGUI) { |
|||
super.showDialog(textGUI); |
|||
return result; |
|||
} |
|||
|
|||
public enum Result { |
|||
CANCEL, APPLY, CHANGE_PASSWORD |
|||
} |
|||
} |
@ -0,0 +1,208 @@ |
|||
package com.sparrowwallet.sparrow.terminal.wallet; |
|||
|
|||
import com.googlecode.lanterna.TerminalSize; |
|||
import com.googlecode.lanterna.gui2.*; |
|||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder; |
|||
import com.sparrowwallet.drongo.KeyPurpose; |
|||
import com.sparrowwallet.drongo.OutputDescriptor; |
|||
import com.sparrowwallet.drongo.SecureString; |
|||
import com.sparrowwallet.drongo.crypto.ECKey; |
|||
import com.sparrowwallet.drongo.crypto.EncryptionType; |
|||
import com.sparrowwallet.drongo.crypto.Key; |
|||
import com.sparrowwallet.drongo.wallet.Wallet; |
|||
import com.sparrowwallet.sparrow.AppServices; |
|||
import com.sparrowwallet.sparrow.EventManager; |
|||
import com.sparrowwallet.sparrow.event.RequestOpenWalletsEvent; |
|||
import com.sparrowwallet.sparrow.event.StorageEvent; |
|||
import com.sparrowwallet.sparrow.event.TimedEvent; |
|||
import com.sparrowwallet.sparrow.io.Storage; |
|||
import com.sparrowwallet.sparrow.io.StorageException; |
|||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal; |
|||
import com.sparrowwallet.sparrow.wallet.Function; |
|||
import com.sparrowwallet.sparrow.wallet.WalletForm; |
|||
import javafx.application.Platform; |
|||
import org.slf4j.Logger; |
|||
import org.slf4j.LoggerFactory; |
|||
|
|||
import java.io.IOException; |
|||
import java.util.ArrayList; |
|||
import java.util.List; |
|||
|
|||
public class SettingsDialog extends WalletDialog { |
|||
private static final Logger log = LoggerFactory.getLogger(SettingsDialog.class); |
|||
|
|||
private final Label scriptType; |
|||
private final TextBox outputDescriptor; |
|||
|
|||
public SettingsDialog(WalletForm walletForm) { |
|||
super(walletForm.getWallet().getFullDisplayName() + " Settings", walletForm); |
|||
|
|||
setHints(List.of(Hint.CENTERED)); |
|||
|
|||
Panel mainPanel = new Panel(new GridLayout(2).setHorizontalSpacing(2).setVerticalSpacing(0).setTopMarginSize(1)); |
|||
|
|||
mainPanel.addComponent(new Label("Script Type")); |
|||
scriptType = new Label(getWalletForm().getWallet().getScriptType().getDescription()).addTo(mainPanel); |
|||
|
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
|
|||
TerminalSize screenSize = SparrowTerminal.get().getScreen().getTerminalSize(); |
|||
int descriptorWidth = Math.min(Math.max(20, screenSize.getColumns() - 20), 120); |
|||
|
|||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(getWalletForm().getWallet(), KeyPurpose.DEFAULT_PURPOSES, null); |
|||
String outputDescriptorString = descriptor.toString(true); |
|||
List<String> outputDescriptorLines = splitString(outputDescriptorString, descriptorWidth); |
|||
|
|||
mainPanel.addComponent(new Label("Output Descriptor")); |
|||
outputDescriptor = new TextBox(new TerminalSize(descriptorWidth, Math.min(outputDescriptorLines.size(), 10))); |
|||
outputDescriptor.setReadOnly(true); |
|||
outputDescriptor.setText(outputDescriptorLines.stream().reduce((s1, s2) -> s1 + "\n" + s2).get()); |
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
|
|||
mainPanel.addComponent(outputDescriptor, GridLayout.createLayoutData(GridLayout.Alignment.BEGINNING, GridLayout.Alignment.CENTER, true, true, 2, 1)); |
|||
|
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
|
|||
Panel buttonPanel = new Panel(); |
|||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1)); |
|||
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.SETTINGS))); |
|||
buttonPanel.addComponent(new Button("Advanced", this::showAdvanced).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false))); |
|||
|
|||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE)); |
|||
|
|||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel); |
|||
setComponent(mainPanel); |
|||
} |
|||
|
|||
private void showAdvanced() { |
|||
AdvancedDialog advancedDialog = new AdvancedDialog(getWalletForm()); |
|||
AdvancedDialog.Result result = (AdvancedDialog.Result)advancedDialog.showDialog(SparrowTerminal.get().getGui()); |
|||
|
|||
if(result != AdvancedDialog.Result.CANCEL) { |
|||
saveWallet(false, result == AdvancedDialog.Result.CHANGE_PASSWORD); |
|||
} |
|||
} |
|||
|
|||
private void saveWallet(boolean changePassword, boolean suggestChangePassword) { |
|||
WalletForm walletForm = getWalletForm(); |
|||
ECKey existingPubKey = walletForm.getStorage().getEncryptionPubKey(); |
|||
|
|||
PasswordRequirement requirement; |
|||
if(existingPubKey == null) { |
|||
if(changePassword) { |
|||
requirement = PasswordRequirement.UPDATE_CHANGE; |
|||
} else { |
|||
requirement = PasswordRequirement.UPDATE_NEW; |
|||
} |
|||
} else if(Storage.NO_PASSWORD_KEY.equals(existingPubKey)) { |
|||
requirement = PasswordRequirement.UPDATE_EMPTY; |
|||
} else { |
|||
requirement = PasswordRequirement.UPDATE_SET; |
|||
} |
|||
|
|||
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password"); |
|||
builder.setDescription(requirement.description); |
|||
builder.setPasswordInput(true); |
|||
|
|||
String password = builder.build().showDialog(SparrowTerminal.get().getGui()); |
|||
if(password != null) { |
|||
Platform.runLater(() -> { |
|||
if(password.length() == 0 && requirement != PasswordRequirement.UPDATE_SET) { |
|||
try { |
|||
walletForm.getStorage().setEncryptionPubKey(Storage.NO_PASSWORD_KEY); |
|||
walletForm.saveAndRefresh(); |
|||
EventManager.get().post(new RequestOpenWalletsEvent()); |
|||
} catch (IOException | StorageException e) { |
|||
log.error("Error saving wallet", e); |
|||
AppServices.showErrorDialog("Error saving wallet", e.getMessage()); |
|||
} |
|||
} else { |
|||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), new SecureString(password)); |
|||
keyDerivationService.setOnSucceeded(workerStateEvent -> { |
|||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.END, "Done")); |
|||
ECKey encryptionFullKey = keyDerivationService.getValue(); |
|||
Key key = null; |
|||
|
|||
try { |
|||
ECKey encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); |
|||
|
|||
if(existingPubKey != null && !Storage.NO_PASSWORD_KEY.equals(existingPubKey) && !existingPubKey.equals(encryptionPubKey)) { |
|||
AppServices.showErrorDialog("Incorrect Password", "The password was incorrect."); |
|||
return; |
|||
} |
|||
|
|||
key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); |
|||
|
|||
Wallet masterWallet = walletForm.getWallet().isMasterWallet() ? walletForm.getWallet() : walletForm.getWallet().getMasterWallet(); |
|||
if(suggestChangePassword && requirement == PasswordRequirement.UPDATE_SET) { |
|||
walletForm.getStorage().setEncryptionPubKey(null); |
|||
masterWallet.decrypt(key); |
|||
for(Wallet childWallet : masterWallet.getChildWallets()) { |
|||
if(!childWallet.isNested()) { |
|||
childWallet.decrypt(key); |
|||
} |
|||
} |
|||
SparrowTerminal.get().getGuiThread().invokeLater(() -> saveWallet(true, false)); |
|||
return; |
|||
} |
|||
|
|||
masterWallet.encrypt(key); |
|||
for(Wallet childWallet : masterWallet.getChildWallets()) { |
|||
if(!childWallet.isNested()) { |
|||
childWallet.encrypt(key); |
|||
} |
|||
} |
|||
walletForm.getStorage().setEncryptionPubKey(encryptionPubKey); |
|||
walletForm.saveAndRefresh(); |
|||
EventManager.get().post(new RequestOpenWalletsEvent()); |
|||
} catch (Exception e) { |
|||
log.error("Error saving wallet", e); |
|||
AppServices.showErrorDialog("Error saving wallet", e.getMessage()); |
|||
} finally { |
|||
encryptionFullKey.clear(); |
|||
if(key != null) { |
|||
key.clear(); |
|||
} |
|||
} |
|||
}); |
|||
keyDerivationService.setOnFailed(workerStateEvent -> { |
|||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.END, "Failed")); |
|||
AppServices.showErrorDialog("Error saving wallet", keyDerivationService.getException().getMessage()); |
|||
}); |
|||
EventManager.get().post(new StorageEvent(walletForm.getWalletId(), TimedEvent.Action.START, "Encrypting wallet...")); |
|||
keyDerivationService.start(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public static List<String> splitString(String stringToSplit, int maxLength) { |
|||
String text = stringToSplit; |
|||
List<String> lines = new ArrayList<>(); |
|||
while(text.length() > maxLength) { |
|||
int breakAt = maxLength - 1; |
|||
lines.add(text.substring(0, breakAt)); |
|||
text = text.substring(breakAt + 1); |
|||
} |
|||
|
|||
lines.add(text); |
|||
return lines; |
|||
} |
|||
|
|||
public enum PasswordRequirement { |
|||
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"), |
|||
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"), |
|||
UPDATE_SET("Re-enter the wallet password:", "Verify Password"), |
|||
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password"); |
|||
|
|||
private final String description; |
|||
private final String okButtonText; |
|||
|
|||
PasswordRequirement(String description, String okButtonText) { |
|||
this.description = description; |
|||
this.okButtonText = okButtonText; |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue