mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
show lifehash for master fingerprint in settings and passphrase dialog
This commit is contained in:
parent
d1a1bd5751
commit
8038298485
6 changed files with 141 additions and 7 deletions
|
@ -1,20 +1,23 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.scene.control.Dialog;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.scene.control.DialogPane;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||||
import org.controlsfx.control.textfield.TextFields;
|
|
||||||
import org.controlsfx.glyphfont.Glyph;
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
public class KeystorePassphraseDialog extends Dialog<String> {
|
public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
private final CustomPasswordField passphrase;
|
private final CustomPasswordField passphrase;
|
||||||
|
private final ObjectProperty<byte[]> masterFingerprint = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
public KeystorePassphraseDialog(Keystore keystore) {
|
public KeystorePassphraseDialog(Keystore keystore) {
|
||||||
this(null, keystore);
|
this(null, keystore);
|
||||||
|
@ -45,10 +48,38 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
content.setPrefHeight(50);
|
content.setPrefHeight(50);
|
||||||
content.getChildren().add(passphrase);
|
content.getChildren().add(passphrase);
|
||||||
|
|
||||||
|
passphrase.textProperty().addListener((observable, oldValue, passphrase) -> {
|
||||||
|
masterFingerprint.set(getMasterFingerprint(keystore, passphrase));
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox fingerprintBox = new HBox(10);
|
||||||
|
fingerprintBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
Label fingerprintLabel = new Label("Master fingerprint:");
|
||||||
|
TextField fingerprintHex = new TextField();
|
||||||
|
fingerprintHex.setDisable(true);
|
||||||
|
fingerprintHex.setPrefWidth(90);
|
||||||
|
fingerprintHex.getStyleClass().addAll("fixed-width");
|
||||||
|
fingerprintHex.setStyle("-fx-opacity: 0.6");
|
||||||
|
masterFingerprint.addListener((observable, oldValue, newValue) -> {
|
||||||
|
if(newValue != null) {
|
||||||
|
fingerprintHex.setText(Utils.bytesToHex(newValue));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
LifeHashIcon lifeHashIcon = new LifeHashIcon();
|
||||||
|
lifeHashIcon.dataProperty().bind(masterFingerprint);
|
||||||
|
HelpLabel helpLabel = new HelpLabel();
|
||||||
|
helpLabel.setHelpText("All passphrases create valid wallets." +
|
||||||
|
"\nThe master fingerprint identifies the keystore and changes as the passphrase changes." +
|
||||||
|
"\n" + (confirm ? "Take a moment to identify it before proceeding." : "Make sure you recognise it before proceeding."));
|
||||||
|
fingerprintBox.getChildren().addAll(fingerprintLabel, fingerprintHex, lifeHashIcon, helpLabel);
|
||||||
|
content.getChildren().add(fingerprintBox);
|
||||||
|
|
||||||
|
masterFingerprint.set(getMasterFingerprint(keystore, ""));
|
||||||
|
|
||||||
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||||
warnGlyph.getStyleClass().add("warn-icon");
|
warnGlyph.getStyleClass().add("warn-icon");
|
||||||
warnGlyph.setFontSize(12);
|
warnGlyph.setFontSize(12);
|
||||||
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
|
Label warnLabel = new Label((confirm ? "Note" : "Check") + " the master fingerprint before proceeding!", warnGlyph);
|
||||||
warnLabel.setGraphicTextGap(5);
|
warnLabel.setGraphicTextGap(5);
|
||||||
content.getChildren().add(warnLabel);
|
content.getChildren().add(warnLabel);
|
||||||
|
|
||||||
|
@ -57,4 +88,14 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||||
|
|
||||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
|
||||||
|
try {
|
||||||
|
Keystore copyKeystore = keystore.copy();
|
||||||
|
copyKeystore.getSeed().setPassphrase(passphrase);
|
||||||
|
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||||
|
import com.sparrowwallet.toucan.LifeHash;
|
||||||
|
import com.sparrowwallet.toucan.LifeHashVersion;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.scene.Group;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.paint.Color;
|
||||||
|
import javafx.scene.paint.ImagePattern;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class LifeHashIcon extends Group {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LifeHashIcon.class);
|
||||||
|
|
||||||
|
private static final int SIZE = 24;
|
||||||
|
|
||||||
|
private final ObjectProperty<byte[]> dataProperty = new SimpleObjectProperty<>(null);
|
||||||
|
|
||||||
|
public LifeHashIcon() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
dataProperty.addListener((observable, oldValue, data) -> {
|
||||||
|
if(data == null) {
|
||||||
|
getChildren().clear();
|
||||||
|
} else if(oldValue == null || !Arrays.equals(oldValue, data)) {
|
||||||
|
LifeHash.Image lifeHashImage = LifeHash.makeFromData(data, LifeHashVersion.VERSION2, 1, false);
|
||||||
|
BufferedImage bufferedImage = LifeHash.getBufferedImage(lifeHashImage);
|
||||||
|
BufferedImage resizedImage = ImageUtils.resizeToImage(bufferedImage, SIZE, SIZE);
|
||||||
|
Image image = SwingFXUtils.toFXImage(resizedImage, null);
|
||||||
|
setImage(image);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setImage(Image image) {
|
||||||
|
getChildren().clear();
|
||||||
|
Rectangle rectangle = new Rectangle(SIZE, SIZE);
|
||||||
|
rectangle.setArcWidth(6);
|
||||||
|
rectangle.setArcHeight(6);
|
||||||
|
rectangle.setFill(new ImagePattern(image));
|
||||||
|
rectangle.setStroke(Color.rgb(65, 72, 77));
|
||||||
|
rectangle.setStrokeWidth(1.0);
|
||||||
|
getChildren().add(rectangle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getData() {
|
||||||
|
return dataProperty.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<byte[]> dataProperty() {
|
||||||
|
return dataProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setData(byte[] data) {
|
||||||
|
this.dataProperty.set(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHex(String hex) {
|
||||||
|
setData(hex == null ? null : Utils.hexToBytes(hex));
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,14 @@ public class ImageUtils {
|
||||||
resize(Thumbnails.of(image), outputStream, width, height);
|
resize(Thumbnails.of(image), outputStream, width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static BufferedImage resizeToImage(BufferedImage image, int width, int height) {
|
||||||
|
try {
|
||||||
|
return Thumbnails.of(image).size(width, height).outputQuality(1).asBufferedImage();
|
||||||
|
} catch(IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] resize(File file, int width, int height) throws IOException {
|
public static byte[] resize(File file, int width, int height) throws IOException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
resize(file, baos, width, height);
|
resize(file, baos, width, height);
|
||||||
|
|
|
@ -73,6 +73,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
@FXML
|
@FXML
|
||||||
private TextField fingerprint;
|
private TextField fingerprint;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private LifeHashIcon fingerprintIcon;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button scanXpubQR;
|
private Button scanXpubQR;
|
||||||
|
|
||||||
|
@ -134,6 +137,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
if(keystore.getKeyDerivation() != null) {
|
if(keystore.getKeyDerivation() != null) {
|
||||||
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||||
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
fingerprintIcon.setHex(fingerprint.getText());
|
||||||
} else {
|
} else {
|
||||||
keystore.setKeyDerivation(new KeyDerivation("",""));
|
keystore.setKeyDerivation(new KeyDerivation("",""));
|
||||||
}
|
}
|
||||||
|
@ -144,8 +148,16 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
});
|
});
|
||||||
fingerprint.textProperty().addListener((observable, oldValue, newValue) -> {
|
fingerprint.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
keystore.setKeyDerivation(new KeyDerivation(newValue, keystore.getKeyDerivation().getDerivationPath()));
|
keystore.setKeyDerivation(new KeyDerivation(newValue, keystore.getKeyDerivation().getDerivationPath()));
|
||||||
|
fingerprintIcon.setHex(newValue.length() == 8 ? newValue : null);
|
||||||
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_FINGERPRINT));
|
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_FINGERPRINT));
|
||||||
});
|
});
|
||||||
|
fingerprint.setTextFormatter(new TextFormatter<>(change -> {
|
||||||
|
String input = change.getText();
|
||||||
|
if(input.matches("[0-9a-fA-F]*")) {
|
||||||
|
return change;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
derivation.textProperty().addListener((observable, oldValue, newValue) -> {
|
derivation.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if(KeyDerivation.isValid(newValue) && !walletForm.getWallet().derivationMatchesAnotherScriptType(newValue)) {
|
if(KeyDerivation.isValid(newValue) && !walletForm.getWallet().derivationMatchesAnotherScriptType(newValue)) {
|
||||||
keystore.setKeyDerivation(new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), newValue));
|
keystore.setKeyDerivation(new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), newValue));
|
||||||
|
|
|
@ -48,4 +48,5 @@ open module com.sparrowwallet.sparrow {
|
||||||
requires com.googlecode.lanterna;
|
requires com.googlecode.lanterna;
|
||||||
requires net.coobird.thumbnailator;
|
requires net.coobird.thumbnailator;
|
||||||
requires com.github.hervegirod;
|
requires com.github.hervegirod;
|
||||||
|
requires com.sparrowwallet.toucan;
|
||||||
}
|
}
|
|
@ -14,6 +14,8 @@
|
||||||
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
|
<?import com.sparrowwallet.drongo.wallet.KeystoreSource?>
|
||||||
<?import org.controlsfx.glyphfont.Glyph?>
|
<?import org.controlsfx.glyphfont.Glyph?>
|
||||||
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
|
||||||
|
<?import com.sparrowwallet.sparrow.control.LifeHashIcon?>
|
||||||
|
|
||||||
<StackPane stylesheets="@keystore.css, @settings.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.KeystoreController">
|
<StackPane stylesheets="@keystore.css, @settings.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.wallet.KeystoreController">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets left="25.0" right="25.0" />
|
<Insets left="25.0" right="25.0" />
|
||||||
|
@ -49,7 +51,7 @@
|
||||||
<TextField fx:id="label" maxWidth="160"/>
|
<TextField fx:id="label" maxWidth="160"/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field text="Master fingerprint:">
|
<Field text="Master fingerprint:">
|
||||||
<TextField fx:id="fingerprint" maxWidth="80" promptText="00000000"/> <HelpLabel helpText="The master fingerprint uniquely identifies this keystore using the first 4 bytes of the master public key hash.\nIt is safe to use any valid value (00000000) for Watch Only Wallets." />
|
<TextField fx:id="fingerprint" maxWidth="80" promptText="00000000"/> <Region style="-fx-max-width: 5" /> <LifeHashIcon fx:id="fingerprintIcon" /> <HelpLabel helpText="The master fingerprint uniquely identifies this keystore using the first 4 bytes of the master public key hash.\nIt is safe to use any valid value (00000000) for Watch Only Wallets." />
|
||||||
</Field>
|
</Field>
|
||||||
<Field text="Derivation:">
|
<Field text="Derivation:">
|
||||||
<TextField fx:id="derivation" maxWidth="200"/> <HelpLabel helpText="The derivation path to the xpub from the master private key.\nFor safety, derivations that match defaults for other script types are not valid.\nThis validation can be turned off in the General Preferences." />
|
<TextField fx:id="derivation" maxWidth="200"/> <HelpLabel helpText="The derivation path to the xpub from the master private key.\nFor safety, derivations that match defaults for other script types are not valid.\nThis validation can be turned off in the General Preferences." />
|
||||||
|
|
Loading…
Reference in a new issue