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; + } +}