add download verification dialog supporting pgp signatures and optional sha256 manifests

This commit is contained in:
Craig Raw 2024-02-22 13:35:06 +02:00
parent ff4ff90bb2
commit d73820464e
10 changed files with 628 additions and 6 deletions

View file

@ -703,4 +703,16 @@ extraJavaModuleInfo {
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') { module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander') exports('com.beust.jcommander')
} }
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
exports('org.pgpainless')
exports('org.pgpainless.key')
exports('org.pgpainless.key.parsing')
exports('org.pgpainless.decryption_verification')
exports('org.pgpainless.exception')
exports('org.pgpainless.signature')
exports('org.pgpainless.util')
requires('org.bouncycastle.provider')
requires('org.bouncycastle.pg')
requires('org.slf4j')
}
} }

View file

@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException { private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry(); JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) { while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class")) { if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
outputStream.putNextEntry(jarEntry); outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes()); outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry(); outputStream.closeEntry();

2
drongo

@ -1 +1 @@
Subproject commit dde2dccda44595d97081122b8eae5b0dcc5f8321 Subproject commit 96566039309875c51674a615df4c57eaee6154f1

View file

@ -1468,6 +1468,11 @@ public class AppController implements Initializable {
} }
} }
public void verifyDownload(ActionEvent event) {
DownloadVerifierDialog downloadVerifierDialog = new DownloadVerifierDialog();
downloadVerifierDialog.showAndWait();
}
public void minimizeToTray(ActionEvent event) { public void minimizeToTray(ActionEvent event) {
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow()); AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
} }

View file

@ -0,0 +1,569 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.pgp.PGPUtils;
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.net.VersionCheckService;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tornadofx.control.Field;
import tornadofx.control.Fieldset;
import tornadofx.control.Form;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class);
private static final DateFormat signatureDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z");
private static final long MAX_VALID_MANIFEST_SIZE = 100 * 1024;
private final ObjectProperty<File> signature = new SimpleObjectProperty<>();
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
private final Label signedBy;
private final Label releaseHash;
private final Label releaseVerified;
private final Hyperlink releaseLink;
private static File lastFileParent;
public DownloadVerifierDialog() {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
dialogPane.setHeader(new Header());
VBox vBox = new VBox();
vBox.setSpacing(20);
vBox.setPadding(new Insets(20, 10, 10, 20));
Form form = new Form();
Fieldset filesFieldset = new Fieldset();
filesFieldset.setText("Files");
filesFieldset.setSpacing(10);
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
Field signatureField = setupField(signature, "Signature", List.of("asc", "sig", "gpg"), false, "sparrow-" + version + "-manifest.txt", null);
Field manifestField = setupField(manifest, "Manifest", List.of("txt"), false, "sparrow-" + version + "-manifest", null);
Field publicKeyField = setupField(publicKey, "Public Key", List.of("asc"), true, "pgp_keys", publicKeyDisabled);
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
filesFieldset.getChildren().addAll(signatureField, manifestField, publicKeyField, releaseFileField);
form.getChildren().add(filesFieldset);
Fieldset resultsFieldset = new Fieldset();
resultsFieldset.setText("Results");
resultsFieldset.setSpacing(10);
signedBy = new Label();
Field signedByField = setupResultField(signedBy, "Signed By");
releaseHash = new Label();
Field hashMatchedField = setupResultField(releaseHash, "Release Hash");
releaseVerified = new Label();
Field releaseVerifiedField = setupResultField(releaseVerified, "Verified");
releaseLink = new Hyperlink("");
releaseVerifiedField.getInputs().add(releaseLink);
releaseLink.setOnAction(event -> {
if(release.get() != null && release.get().exists()) {
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
}
});
resultsFieldset.getChildren().addAll(signedByField, hashMatchedField, releaseVerifiedField);
form.getChildren().add(resultsFieldset);
vBox.getChildren().addAll(form);
dialogPane.setContent(vBox);
ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear", ButtonBar.ButtonData.CANCEL_CLOSE);
ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(clearButtonType, closeButtonType);
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.CANCEL_CLOSE.equals(getResult())) {
signature.set(null);
manifest.set(null);
publicKey.set(null);
release.set(null);
signedBy.setText("");
signedBy.setGraphic(null);
releaseHash.setText("");
releaseHash.setGraphic(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
event.consume();
}
});
setResultConverter(ButtonType::getButtonData);
AppServices.moveToActiveWindowScreen(this);
dialogPane.setPrefWidth(900);
setResizable(true);
signature.addListener((observable, oldValue, signatureFile) -> {
if(signatureFile != null) {
boolean verify = true;
if(PGPUtils.signatureContainsManifest(signatureFile)) {
manifest.set(signatureFile);
verify = false;
} else {
String signatureName = signatureFile.getName();
if(signatureName.length() > 4) {
File manifestFile = new File(signatureFile.getParent(), signatureName.substring(0, signatureName.length() - 4));
if(manifestFile.exists() && !manifestFile.equals(manifest.get())) {
manifest.set(manifestFile);
verify = false;
}
}
}
if(verify) {
verify();
}
}
});
manifest.addListener((observable, oldValue, manifestFile) -> {
if(manifestFile != null) {
boolean verify = true;
try {
Map<File, String> manifestMap = getManifest(manifestFile);
List<String> releaseExtensions = getReleaseFileExtensions();
for(File file : manifestMap.keySet()) {
if(releaseExtensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
File releaseFile = new File(manifestFile.getParent(), file.getName());
if(releaseFile.exists() && !releaseFile.equals(release.get())) {
release.set(releaseFile);
verify = false;
break;
}
}
}
} catch(IOException e) {
log.debug("Error reading manifest file", e);
verify = false;
} catch(InvalidManifestException e) {
release.set(manifestFile);
verify = false;
}
if(verify) {
verify();
}
}
});
publicKey.addListener((observable, oldValue, newValue) -> {
verify();
});
release.addListener((observable, oldValue, releaseFile) -> {
verify();
});
}
public void verify() {
boolean signatureVerified = verifySignature();
if(signatureVerified) {
if(manifest.get().equals(release.get())) {
releaseHash.setText("No hash required, signature signs release file directly");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("Ready to install ");
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
releaseLink.setText(release.get().getName());
} else {
verifyManifest();
}
} else {
releaseHash.setText("");
releaseHash.setGraphic(null);
releaseHash.setTooltip(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
}
}
private boolean verifySignature() {
publicKeyDisabled.set(false);
if(signature.get() == null || manifest.get() == null) {
return false;
}
boolean detachedSignature = !manifest.get().equals(signature.get());
try(InputStream publicKeyStream = publicKey.get() == null ? null : new FileInputStream(publicKey.get());
InputStream contentStream = new BufferedInputStream(new FileInputStream(manifest.get()));
InputStream detachedSignatureStream = detachedSignature ? new FileInputStream(signature.get()) : null) {
PGPVerificationResult result = PGPUtils.verify(publicKeyStream, contentStream, detachedSignatureStream);
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
signedBy.setText(message);
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
if(!result.expired()) {
publicKeyDisabled.set(true);
}
return true;
} catch(IOException | PGPVerificationException e) {
signedBy.setText(getDisplayMessage(e));
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
}
return false;
}
private void verifyManifest() {
File releaseFile = release.get();
if(releaseFile != null && releaseFile.exists()) {
FileSha256Service hashService = new FileSha256Service(releaseFile);
hashService.setOnRunning(event -> {
releaseHash.setText("Calculating...");
releaseHash.setGraphic(GlyphUtils.getBusyGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("");
releaseVerified.setGraphic(null);
releaseLink.setText("");
});
hashService.setOnSucceeded(event -> {
String calculatedHash = hashService.getValue();
try {
Map<File, String> manifestMap = getManifest(manifest.get());
String manifestHash = getManifestHash(releaseFile.getName(), manifestMap);
if(calculatedHash.equalsIgnoreCase(manifestHash)) {
releaseHash.setText("Matched manifest hash");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
releaseHash.setTooltip(new Tooltip(calculatedHash));
releaseVerified.setText("Ready to install ");
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
releaseLink.setText(releaseFile.getName());
} else if(manifestHash == null) {
releaseHash.setText("Could not find manifest hash for " + releaseFile.getName());
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip("Manifest hashes provided for:\n" + manifestMap.keySet().stream().map(File::getName).collect(Collectors.joining("\n"))));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
} else {
releaseHash.setText("Did not match manifest hash");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip("Calculated Hash: " + calculatedHash + "\nManifest Hash: " + manifestHash));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
} catch(IOException | InvalidManifestException e) {
releaseHash.setText("Could not read manifest");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip(e.getMessage()));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
});
hashService.setOnFailed(event -> {
releaseHash.setText("Could not calculate manifest");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip(event.getSource().getException().getMessage()));
releaseVerified.setText("Cannot verify " + releaseFile.getName());
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
});
hashService.start();
} else {
releaseHash.setText("No release file");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(null);
releaseVerified.setText("Not verified");
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
releaseLink.setText("");
}
}
private Field setupField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
Field field = new Field();
field.setText(title + ":");
FileField fileField = new FileField(fileProperty, title, extensions, optional, example, disabledProperty);
field.getInputs().add(fileField);
return field;
}
private Field setupResultField(Label label, String title) {
Field field = new Field();
field.setText(title + ":");
field.getInputs().add(label);
label.setGraphicTextGap(8);
return field;
}
public Map<File, String> getManifest(File manifest) throws IOException, InvalidManifestException {
if(manifest.length() > MAX_VALID_MANIFEST_SIZE) {
throw new InvalidManifestException();
}
try(InputStream manifestStream = new FileInputStream(manifest)) {
return getManifest(manifestStream);
}
}
public Map<File, String> getManifest(InputStream manifestStream) throws IOException {
Map<File, String> manifest = new HashMap<>();
BufferedReader reader = new BufferedReader(new InputStreamReader(manifestStream, StandardCharsets.UTF_8));
String line;
while((line = reader.readLine()) != null) {
String[] parts = line.split("\\s+");
if(parts.length > 1 && parts[0].length() == 64) {
String manifestHash = parts[0];
String manifestFileName = parts[1];
if(manifestFileName.startsWith("*") || manifestFileName.startsWith("U") || manifestFileName.startsWith("^")) {
manifestFileName = manifestFileName.substring(1);
}
manifest.put(new File(manifestFileName), manifestHash);
}
}
return manifest;
}
private String getManifestHash(String contentFileName, Map<File, String> manifest) {
for(Map.Entry<File, String> entry : manifest.entrySet()) {
if(contentFileName.equalsIgnoreCase(entry.getKey().getName())) {
return entry.getValue();
}
}
return null;
}
private List<String> getReleaseFileExtensions() {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
return List.of("dmg");
}
case WINDOWS -> {
return List.of("exe", "zip");
}
default -> {
return List.of("deb", "rpm", "tar.gz");
}
}
}
private String getReleaseFileExample(String version) {
Platform platform = Platform.getCurrent();
String arch = System.getProperty("os.arch");
switch(platform) {
case OSX -> {
return "Sparrow-" + version + "-" + arch;
}
case WINDOWS -> {
return "Sparrow-" + version;
}
default -> {
return "sparrow_" + version + "-1_" + (arch.equals("aarch64") ? "arm64" : arch);
}
}
}
private String getDisplayMessage(Exception e) {
String message = e.getMessage();
message = message.substring(0, 1).toUpperCase(Locale.ROOT) + message.substring(1);
if(message.endsWith(".")) {
message = message.substring(0, message.length() - 1);
}
if(message.equals("Invalid header encountered")) {
message += ", not a valid signature file";
}
if(message.startsWith("Malformed message")) {
message = "Not a valid signature file";
}
return message;
}
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
getStyleClass().add("header-panel");
VBox vBox = new VBox();
vBox.setPadding(new Insets(10, 0, 0, 0));
Label headerLabel = new Label("Verify Download");
headerLabel.setWrapText(true);
headerLabel.setAlignment(Pos.CENTER_LEFT);
headerLabel.setMaxWidth(Double.MAX_VALUE);
headerLabel.setMaxHeight(Double.MAX_VALUE);
CopyableLabel descriptionLabel = new CopyableLabel("Download the release file, GPG signature and optional manifest of a project to verify the download integrity");
descriptionLabel.setAlignment(Pos.CENTER_LEFT);
vBox.getChildren().addAll(headerLabel, descriptionLabel);
add(vBox, 0, 0);
StackPane graphicContainer = new StackPane();
graphicContainer.getStyleClass().add("graphic-container");
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
if (!image.isError()) {
ImageView imageView = new ImageView();
imageView.setSmooth(false);
imageView.setImage(image);
graphicContainer.getChildren().add(imageView);
}
add(graphicContainer, 1, 0);
ColumnConstraints textColumn = new ColumnConstraints();
textColumn.setFillWidth(true);
textColumn.setHgrow(Priority.ALWAYS);
ColumnConstraints graphicColumn = new ColumnConstraints();
graphicColumn.setFillWidth(false);
graphicColumn.setHgrow(Priority.NEVER);
getColumnConstraints().setAll(textColumn , graphicColumn);
}
}
private static class FileField extends HBox {
private final ObjectProperty<File> fileProperty;
public FileField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
super(10);
this.fileProperty = fileProperty;
TextField textField = new TextField();
textField.setEditable(false);
textField.setPromptText("e.g. " + example + formatExtensionsList(extensions) + (optional ? " (optional)" : ""));
textField.setOnMouseClicked(event -> browseForFile(title, extensions));
Button browseButton = new Button("Browse...");
browseButton.setOnAction(event -> browseForFile(title, extensions));
getChildren().addAll(textField, browseButton);
HBox.setHgrow(textField, Priority.ALWAYS);
fileProperty.addListener((observable, oldValue, file) -> {
textField.setText(file == null ? "" : file.getAbsolutePath());
if(file != null) {
lastFileParent = file.getParentFile();
}
});
if(disabledProperty != null) {
disabledProperty.addListener((observable, oldValue, disabled) -> {
textField.setDisable(disabled);
browseButton.setDisable(disabled);
});
}
}
private void browseForFile(String title, List<String> extensions) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open File");
File userDir = new File(System.getProperty("user.home"));
File downloadsDir = new File(userDir, "Downloads");
fileChooser.setInitialDirectory(lastFileParent != null ? lastFileParent : (downloadsDir.exists() ? downloadsDir : userDir));
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(title + " files", extensions));
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
fileProperty.set(file);
}
}
public String formatExtensionsList(List<String> items) {
StringBuilder result = new StringBuilder();
for(int i = 0; i < items.size(); i++) {
result.append(".").append(items.get(i));
if (i < items.size() - 1) {
result.append(", ");
}
if (i == items.size() - 2) {
result.append("or ");
}
}
return result.toString();
}
}
private static class FileSha256Service extends Service<String> {
private final File file;
public FileSha256Service(File file) {
this.file = file;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
protected String call() throws IOException {
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return sha256(inputStream);
}
}
};
}
private String sha256(InputStream stream) throws IOException {
try {
final byte[] buffer = new byte[1024 * 1024];
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
int bytesRead = 0;
while((bytesRead = stream.read(buffer)) >= 0) {
if (bytesRead > 0) {
sha256.update(buffer, 0, bytesRead);
}
}
return Utils.bytesToHex(sha256.digest());
} catch(NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}
private static class InvalidManifestException extends Exception { }
}

View file

@ -476,7 +476,7 @@ public class TransactionDiagram extends GridPane {
tooltip.setText(""); tooltip.setText("");
} else if(input instanceof AddUserBlockTransactionHashIndex) { } else if(input instanceof AddUserBlockTransactionHashIndex) {
tooltip.setText(""); tooltip.setText("");
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph()); label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
label.setOnMouseClicked(event -> { label.setOnMouseClicked(event -> {
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet())); EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
closeExpanded(); closeExpanded();
@ -789,7 +789,7 @@ public class TransactionDiagram extends GridPane {
} }
boolean highFee = (walletTx.getFeePercentage() > 0.1); boolean highFee = (walletTx.getFeePercentage() > 0.1);
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph()); Label feeLabel = highFee ? new Label("High Fee", getFeeWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label"); feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0); String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)"); Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");

View file

@ -44,6 +44,7 @@ public class FontAwesome5 extends GlyphFont {
HAND_HOLDING('\uf4bd'), HAND_HOLDING('\uf4bd'),
HAND_HOLDING_MEDICAL('\ue05c'), HAND_HOLDING_MEDICAL('\ue05c'),
HAND_HOLDING_WATER('\uf4c1'), HAND_HOLDING_WATER('\uf4c1'),
HOURGLASS_HALF('\uf252'),
HISTORY('\uf1da'), HISTORY('\uf1da'),
INFO_CIRCLE('\uf05a'), INFO_CIRCLE('\uf05a'),
KEY('\uf084'), KEY('\uf084'),

View file

@ -141,7 +141,7 @@ public class GlyphUtils {
return feeGlyph; return feeGlyph;
} }
public static Glyph getWarningGlyph() { public static Glyph getFeeWarningGlyph() {
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
feeWarningGlyph.getStyleClass().add("fee-warning-icon"); feeWarningGlyph.getStyleClass().add("fee-warning-icon");
feeWarningGlyph.setFontSize(12); feeWarningGlyph.setFontSize(12);
@ -175,4 +175,32 @@ public class GlyphUtils {
userGlyph.setFontSize(12); userGlyph.setFontSize(12);
return userGlyph; return userGlyph;
} }
public static Glyph getSuccessGlyph() {
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
successGlyph.getStyleClass().add("success");
successGlyph.setFontSize(12);
return successGlyph;
}
public static Glyph getWarningGlyph() {
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
warningGlyph.getStyleClass().add("warn-icon");
warningGlyph.setFontSize(12);
return warningGlyph;
}
public static Glyph getFailureGlyph() {
Glyph failureGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
failureGlyph.getStyleClass().add("failure");
failureGlyph.setFontSize(12);
return failureGlyph;
}
public static Glyph getBusyGlyph() {
Glyph busyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HOURGLASS_HALF);
busyGlyph.getStyleClass().add("busy");
busyGlyph.setFontSize(12);
return busyGlyph;
}
} }

View file

@ -20,12 +20,15 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class); private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class);
private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version"; private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version";
private static String version;
@Override @Override
protected Task<VersionUpdatedEvent> createTask() { protected Task<VersionUpdatedEvent> createTask() {
return new Task<>() { return new Task<>() {
protected VersionUpdatedEvent call() { protected VersionUpdatedEvent call() {
try { try {
VersionCheck versionCheck = getVersionCheck(); VersionCheck versionCheck = getVersionCheck();
version = versionCheck.version;
if(isNewer(versionCheck) && verifySignature(versionCheck)) { if(isNewer(versionCheck) && verifySignature(versionCheck)) {
return new VersionUpdatedEvent(versionCheck.version); return new VersionUpdatedEvent(versionCheck.version);
} }
@ -91,9 +94,12 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
return false; return false;
} }
public static String getVersion() {
return version;
}
private static class VersionCheck { private static class VersionCheck {
public String version; public String version;
public Map<String, String> signatures; public Map<String, String> signatures;
} }
} }

View file

@ -140,6 +140,7 @@
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/> <MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<Menu fx:id="switchServer" text="Switch Server"/> <Menu fx:id="switchServer" text="Switch Server"/>
<MenuItem mnemonicParsing="false" text="Verify Download" onAction="#verifyDownload" />
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/> <MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/> <CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
<MenuItem fx:id="restart" mnemonicParsing="false" text="Restart" onAction="#restart" /> <MenuItem fx:id="restart" mnemonicParsing="false" text="Restart" onAction="#restart" />