diff --git a/build.gradle b/build.gradle
index bc04bd62..45aa70ad 100644
--- a/build.gradle
+++ b/build.gradle
@@ -267,7 +267,7 @@ jlink {
appVersion = "${sparrowVersion}"
skipInstaller = os.macOsX || properties.skipInstallers
imageOptions = []
- installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
+ installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
if(os.windows) {
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
diff --git a/src/main/deploy/asc.properties b/src/main/deploy/asc.properties
new file mode 100644
index 00000000..0ddb0a03
--- /dev/null
+++ b/src/main/deploy/asc.properties
@@ -0,0 +1,3 @@
+mime-type=application/pgp-signature
+extension=asc
+description=ASCII Armored Signature
\ No newline at end of file
diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist
index 14b27f57..83cdb555 100644
--- a/src/main/deploy/package/osx/Info.plist
+++ b/src/main/deploy/package/osx/Info.plist
@@ -94,6 +94,21 @@
UTTypeIconFile
sparrow.icns
+
+ UTTypeIdentifier
+ com.sparrowwallet.asc
+ UTTypeTagSpecification
+
+ public.filename-extension
+
+ asc
+
+
+ UTTypeDescription
+ ASCII Armored Signature
+ UTTypeIconFile
+ sparrow.icns
+
CFBundleDocumentTypes
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index f9274aa7..41e765fb 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -77,6 +77,7 @@ import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.*;
+import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(AppController.class);
@@ -992,6 +993,8 @@ public class AppController implements Initializable {
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
+ } else if(isSignatureFile(file)) {
+ verifyDownload(new ActionEvent(file, rootStack));
} else {
openTransactionFile(file);
}
@@ -1476,7 +1479,7 @@ public class AppController implements Initializable {
}
public void verifyDownload(ActionEvent event) {
- DownloadVerifierDialog downloadVerifierDialog = new DownloadVerifierDialog();
+ DownloadVerifierDialog downloadVerifierDialog = new DownloadVerifierDialog(event.getSource() instanceof File file ? file : null);
downloadVerifierDialog.showAndWait();
}
@@ -3041,6 +3044,13 @@ public class AppController implements Initializable {
}
}
+ @Subscribe
+ public void requestVerifyDownloadOpen(RequestVerifyDownloadEvent event) {
+ if(tabs.getScene().getWindow().equals(event.getWindow())) {
+ verifyDownload(new ActionEvent(event.getFile(), rootStack));
+ }
+ }
+
@Subscribe
public void functionAction(FunctionActionEvent event) {
selectTab(event.getWallet());
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
index a290c057..8f25979f 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java
@@ -68,6 +68,8 @@ import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
+import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
+
public class AppServices {
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
@@ -883,6 +885,8 @@ public class AppServices {
for(File file : openFiles) {
if(isWalletFile(file)) {
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
+ } else if(isSignatureFile(file)) {
+ EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file));
} else {
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java
index 8c4695e2..335f4463 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java
@@ -42,6 +42,13 @@ public class DownloadVerifierDialog extends Dialog {
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 static final List SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg");
+ private static final List MANIFEST_EXTENSIONS = List.of("txt");
+ private static final List PUBLIC_KEY_EXTENSIONS = List.of("asc");
+ private static final List MACOS_RELEASE_EXTENSIONS = List.of("dmg");
+ private static final List WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
+ private static final List LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
+
private final ObjectProperty signature = new SimpleObjectProperty<>();
private final ObjectProperty manifest = new SimpleObjectProperty<>();
private final ObjectProperty publicKey = new SimpleObjectProperty<>();
@@ -56,7 +63,7 @@ public class DownloadVerifierDialog extends Dialog {
private static File lastFileParent;
- public DownloadVerifierDialog() {
+ public DownloadVerifierDialog(File initialSignatureFile) {
final DialogPane dialogPane = getDialogPane();
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
@@ -74,9 +81,9 @@ public class DownloadVerifierDialog extends Dialog {
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 signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
+ Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null);
+ Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
filesFieldset.getChildren().addAll(signatureField, manifestField, publicKeyField, releaseFileField);
@@ -146,17 +153,18 @@ public class DownloadVerifierDialog extends Dialog {
signature.addListener((observable, oldValue, signatureFile) -> {
if(signatureFile != null) {
boolean verify = true;
- if(PGPUtils.signatureContainsManifest(signatureFile)) {
+ File actualSignatureFile = findSignatureFile(signatureFile);
+ if(actualSignatureFile != null && !actualSignatureFile.equals(signature.get())) {
+ signature.set(actualSignatureFile);
+ verify = false;
+ } else 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;
- }
+ File manifestFile = findManifestFile(signatureFile);
+ if(manifestFile != null && !manifestFile.equals(manifest.get())) {
+ manifest.set(manifestFile);
+ verify = false;
}
}
@@ -182,6 +190,16 @@ public class DownloadVerifierDialog extends Dialog {
}
}
}
+ if(release.get() == null) {
+ for(File file : manifestMap.keySet()) {
+ 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;
@@ -203,6 +221,10 @@ public class DownloadVerifierDialog extends Dialog {
release.addListener((observable, oldValue, releaseFile) -> {
verify();
});
+
+ if(initialSignatureFile != null) {
+ javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
+ }
}
public void verify() {
@@ -382,17 +404,40 @@ public class DownloadVerifierDialog extends Dialog {
return null;
}
+ private File findSignatureFile(File providedFile) {
+ for(String extension : SIGNATURE_EXTENSIONS) {
+ File signatureFile = new File(providedFile.getParentFile(), providedFile.getName() + "." + extension);
+ if(signatureFile.exists()) {
+ return signatureFile;
+ }
+ }
+
+ return null;
+ }
+
+ private File findManifestFile(File providedFile) {
+ String signatureName = providedFile.getName();
+ if(signatureName.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> signatureName.toLowerCase(Locale.ROOT).endsWith("." + ext))) {
+ File manifestFile = new File(providedFile.getParent(), signatureName.substring(0, signatureName.length() - 4));
+ if(manifestFile.exists()) {
+ return manifestFile;
+ }
+ }
+
+ return null;
+ }
+
private List getReleaseFileExtensions() {
Platform platform = Platform.getCurrent();
switch(platform) {
case OSX -> {
- return List.of("dmg");
+ return MACOS_RELEASE_EXTENSIONS;
}
case WINDOWS -> {
- return List.of("exe", "zip");
+ return WINDOWS_RELEASE_EXTENSIONS;
}
- default -> {
- return List.of("deb", "rpm", "tar.gz");
+ default -> {
+ return LINUX_RELEASE_EXTENSIONS;
}
}
}
@@ -432,6 +477,10 @@ public class DownloadVerifierDialog extends Dialog {
return message;
}
+ public static boolean isSignatureFile(File file) {
+ return file != null && file.getName().length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith("." + ext));
+ }
+
private static class Header extends GridPane {
public Header() {
setMaxWidth(Double.MAX_VALUE);
diff --git a/src/main/java/com/sparrowwallet/sparrow/event/RequestVerifyDownloadEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/RequestVerifyDownloadEvent.java
new file mode 100644
index 00000000..a3ac7caf
--- /dev/null
+++ b/src/main/java/com/sparrowwallet/sparrow/event/RequestVerifyDownloadEvent.java
@@ -0,0 +1,28 @@
+package com.sparrowwallet.sparrow.event;
+
+import javafx.stage.Window;
+
+import java.io.File;
+
+public class RequestVerifyDownloadEvent {
+ private final Window window;
+ private final File file;
+
+ public RequestVerifyDownloadEvent(Window window) {
+ this.window = window;
+ this.file = null;
+ }
+
+ public RequestVerifyDownloadEvent(Window window, File file) {
+ this.window = window;
+ this.file = file;
+ }
+
+ public Window getWindow() {
+ return window;
+ }
+
+ public File getFile() {
+ return file;
+ }
+}