From d73820464ed11c581b5789f03b07989099320a87 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 22 Feb 2024 13:35:06 +0200 Subject: [PATCH] add download verification dialog supporting pgp signatures and optional sha256 manifests --- build.gradle | 12 + .../javamodules/ExtraModuleInfoTransform.java | 2 +- drongo | 2 +- .../sparrowwallet/sparrow/AppController.java | 5 + .../control/DownloadVerifierDialog.java | 569 ++++++++++++++++++ .../sparrow/control/TransactionDiagram.java | 4 +- .../sparrow/glyphfont/FontAwesome5.java | 1 + .../sparrow/glyphfont/GlyphUtils.java | 30 +- .../sparrow/net/VersionCheckService.java | 8 +- .../com/sparrowwallet/sparrow/app.fxml | 1 + 10 files changed, 628 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java diff --git a/build.gradle b/build.gradle index 29eee822..fb184830 100644 --- a/build.gradle +++ b/build.gradle @@ -703,4 +703,16 @@ extraJavaModuleInfo { module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') { 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') + } } \ No newline at end of file diff --git a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java index c892bdb7..1a05d530 100644 --- a/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java +++ b/buildSrc/src/main/java/org/gradle/sample/transform/javamodules/ExtraModuleInfoTransform.java @@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction { + 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 signature = new SimpleObjectProperty<>(); + private final ObjectProperty manifest = new SimpleObjectProperty<>(); + private final ObjectProperty publicKey = new SimpleObjectProperty<>(); + private final ObjectProperty 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 manifestMap = getManifest(manifestFile); + List 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 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 fileProperty, String title, List 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 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 getManifest(InputStream manifestStream) throws IOException { + Map 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 manifest) { + for(Map.Entry entry : manifest.entrySet()) { + if(contentFileName.equalsIgnoreCase(entry.getKey().getName())) { + return entry.getValue(); + } + } + + return null; + } + + private List 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 fileProperty; + + public FileField(ObjectProperty fileProperty, String title, List 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 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 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 { + private final File file; + + public FileSha256Service(File file) { + this.file = file; + } + + @Override + protected Task 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 { } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java index eff5875f..6e3772f8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionDiagram.java @@ -476,7 +476,7 @@ public class TransactionDiagram extends GridPane { tooltip.setText(""); } else if(input instanceof AddUserBlockTransactionHashIndex) { tooltip.setText(""); - label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph()); + label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph()); label.setOnMouseClicked(event -> { EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet())); closeExpanded(); @@ -789,7 +789,7 @@ public class TransactionDiagram extends GridPane { } 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"); 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 + "%)"); diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java index 5616017c..7ff74ff4 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/FontAwesome5.java @@ -44,6 +44,7 @@ public class FontAwesome5 extends GlyphFont { HAND_HOLDING('\uf4bd'), HAND_HOLDING_MEDICAL('\ue05c'), HAND_HOLDING_WATER('\uf4c1'), + HOURGLASS_HALF('\uf252'), HISTORY('\uf1da'), INFO_CIRCLE('\uf05a'), KEY('\uf084'), diff --git a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java index 8ff16602..011d3496 100644 --- a/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java +++ b/src/main/java/com/sparrowwallet/sparrow/glyphfont/GlyphUtils.java @@ -141,7 +141,7 @@ public class GlyphUtils { return feeGlyph; } - public static Glyph getWarningGlyph() { + public static Glyph getFeeWarningGlyph() { Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE); feeWarningGlyph.getStyleClass().add("fee-warning-icon"); feeWarningGlyph.setFontSize(12); @@ -175,4 +175,32 @@ public class GlyphUtils { userGlyph.setFontSize(12); 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; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java index 52de7f47..d62e780c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/VersionCheckService.java @@ -20,12 +20,15 @@ public class VersionCheckService extends ScheduledService { private static final Logger log = LoggerFactory.getLogger(VersionCheckService.class); private static final String VERSION_CHECK_URL = "https://www.sparrowwallet.com/version"; + private static String version; + @Override protected Task createTask() { return new Task<>() { protected VersionUpdatedEvent call() { try { VersionCheck versionCheck = getVersionCheck(); + version = versionCheck.version; if(isNewer(versionCheck) && verifySignature(versionCheck)) { return new VersionUpdatedEvent(versionCheck.version); } @@ -91,9 +94,12 @@ public class VersionCheckService extends ScheduledService { return false; } + public static String getVersion() { + return version; + } + private static class VersionCheck { public String version; public Map signatures; } - } diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index a66c2245..35d4a98c 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -140,6 +140,7 @@ +