diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index e8880e2e..2c453a03 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -17,6 +17,8 @@ import com.sparrowwallet.sparrow.control.WalletNameDialog; import com.sparrowwallet.sparrow.event.TabEvent; import com.sparrowwallet.sparrow.event.TransactionTabChangedEvent; import com.sparrowwallet.sparrow.event.TransactionTabSelectedEvent; +import com.sparrowwallet.sparrow.io.FileType; +import com.sparrowwallet.sparrow.io.IOUtils; import com.sparrowwallet.sparrow.io.Storage; import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.wallet.SettingsController; @@ -227,9 +229,10 @@ public class AppController implements Initializable { try { Wallet wallet; ECKey encryptionPubKey = WalletForm.NO_PASSWORD_KEY; - try { + FileType fileType = IOUtils.getFileType(file); + if(FileType.JSON.equals(fileType)) { wallet = Storage.getStorage().loadWallet(file); - } catch(JsonSyntaxException e) { + } else if(FileType.BINARY.equals(fileType)) { Optional optionalFullKey = SettingsController.askForWalletPassword(null, true); if(!optionalFullKey.isPresent()) { return; @@ -238,6 +241,8 @@ public class AppController implements Initializable { ECKey encryptionFullKey = optionalFullKey.get(); wallet = Storage.getStorage().loadWallet(file, encryptionFullKey); encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); + } else { + throw new IOException("Unsupported file type"); } Tab tab = addWalletTab(file, encryptionPubKey, wallet); diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index d903ce19..950b415a 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -38,7 +38,7 @@ public class MainApp extends Application { Wallet wallet = new Wallet(); wallet.setPolicyType(PolicyType.MULTI); - wallet.setScriptType(ScriptType.P2SH); + wallet.setScriptType(ScriptType.P2WPKH); KeystoreImportDialog dlg = new KeystoreImportDialog(wallet); //dlg.showAndWait(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java index 59123418..e748463e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/KeystoreFileImportPane.java @@ -1,16 +1,19 @@ package com.sparrowwallet.sparrow.control; import com.google.gson.JsonParseException; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.io.KeystoreFileImport; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.TitledPane; import javafx.scene.image.Image; @@ -20,7 +23,8 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; import javafx.stage.Stage; -import org.controlsfx.control.HyperlinkLabel; +import org.controlsfx.control.textfield.CustomPasswordField; +import org.controlsfx.control.textfield.TextFields; import java.io.*; @@ -30,7 +34,11 @@ public class KeystoreFileImportPane extends TitledPane { private final KeystoreFileImport importer; private Label mainLabel; - private HyperlinkLabel descriptionLabel; + private Label descriptionLabel; + private Hyperlink showHideLink; + private Button importButton; + + private final SimpleStringProperty password = new SimpleStringProperty(""); public KeystoreFileImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreFileImport importer) { this.importAccordion = importAccordion; @@ -83,21 +91,32 @@ public class KeystoreFileImportPane extends TitledPane { mainLabel.getStyleClass().add("main-label"); labelsBox.getChildren().add(mainLabel); - this.descriptionLabel = new HyperlinkLabel(); + HBox descriptionBox = new HBox(); + descriptionBox.setSpacing(7); + labelsBox.getChildren().add(descriptionBox); - labelsBox.getChildren().add(descriptionLabel); + descriptionLabel = new Label("Keystore file import"); descriptionLabel.getStyleClass().add("description-label"); - descriptionLabel.setText("Keystore file import [View Details...]"); - descriptionLabel.setOnAction(event -> { - setExpanded(true); + showHideLink = new Hyperlink("View Details..."); + showHideLink.managedProperty().bind(showHideLink.visibleProperty()); + showHideLink.setOnAction(event -> { + if(showHideLink.getText().contains("View")) { + setExpanded(true); + showHideLink.setText("Hide Details..."); + } else { + setExpanded(false); + showHideLink.setText("View Details..."); + } }); + descriptionBox.getChildren().addAll(descriptionLabel, showHideLink); + listItem.getChildren().add(labelsBox); HBox.setHgrow(labelsBox, Priority.ALWAYS); HBox buttonBox = new HBox(); buttonBox.setAlignment(Pos.CENTER_RIGHT); - Button importButton = new Button("Import File..."); + importButton = new Button("Import File..."); importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setOnAction(event -> { importFile(); @@ -125,29 +144,44 @@ public class KeystoreFileImportPane extends TitledPane { File file = fileChooser.showOpenDialog(window); if(file != null) { - importFile(file); + importFile(file, null); } } - private void importFile(File file) { + private void importFile(File file, String password) { if(file.exists()) { try { - InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream); - EventManager.get().post(new KeystoreImportEvent(keystore)); + if(importer.isEncrypted(file) && password == null) { + descriptionLabel.getStyleClass().remove("description-error"); + descriptionLabel.getStyleClass().add("description-label"); + descriptionLabel.setText("Password Required"); + showHideLink.setVisible(false); + setContent(getPasswordEntry(file)); + importButton.setDisable(true); + setExpanded(true); + } else { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password); + EventManager.get().post(new KeystoreImportEvent(keystore)); + } } catch (Exception e) { - setExpanded(false); descriptionLabel.getStyleClass().remove("description-label"); descriptionLabel.getStyleClass().add("description-error"); - descriptionLabel.setText("Error Importing [View Details...]"); + descriptionLabel.setText("Import Error"); String errorMessage = e.getMessage(); - if(e.getCause() != null) { + if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) { errorMessage = e.getCause().getMessage(); } + if(e instanceof ECKey.InvalidPasswordException || e.getCause() instanceof ECKey.InvalidPasswordException) { + errorMessage = "Invalid wallet password"; + } if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) { errorMessage = "File was not in JSON format"; } setContent(getContentBox(errorMessage)); + setExpanded(true); + showHideLink.setText("Hide Details..."); + importButton.setDisable(false); } } } @@ -168,4 +202,28 @@ public class KeystoreFileImportPane extends TitledPane { return contentBox; } + + private Node getPasswordEntry(File file) { + CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField(); + passwordField.setPromptText("Wallet password"); + password.bind(passwordField.textProperty()); + HBox.setHgrow(passwordField, Priority.ALWAYS); + + Button importEncryptedButton = new Button("Import"); + importEncryptedButton.setOnAction(event -> { + showHideLink.setVisible(true); + setExpanded(false); + importFile(file, password.get()); + }); + + HBox contentBox = new HBox(); + contentBox.setAlignment(Pos.TOP_RIGHT); + contentBox.setSpacing(20); + contentBox.getChildren().add(passwordField); + contentBox.getChildren().add(importEncryptedButton); + contentBox.setPadding(new Insets(10, 30, 10, 30)); + contentBox.setPrefHeight(60); + + return contentBox; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java index 1c0cc745..ea2f0161 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java @@ -37,7 +37,7 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor } @Override - public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { + public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { InputStreamReader reader = new InputStreamReader(inputStream); ColdcardKeystore cck = Storage.getStorage().getGson().fromJson(reader, ColdcardKeystore.class); @@ -77,7 +77,7 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor } @Override - public Wallet importWallet(InputStream inputStream) throws ImportException { + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { Wallet wallet = new Wallet(); wallet.setPolicyType(PolicyType.MULTI); @@ -193,4 +193,9 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor public String getWalletExportDescription() { return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature"; } + + @Override + public boolean isEncrypted(File file) { + return false; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java index a2db8777..9521de02 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ColdcardSinglesig.java @@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.WalletModel; +import java.io.File; import java.io.InputStream; import java.io.InputStreamReader; import java.util.List; @@ -42,14 +43,14 @@ public class ColdcardSinglesig implements KeystoreFileImport, SinglesigWalletImp } @Override - public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { - Wallet wallet = importWallet(scriptType, inputStream); + public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + Wallet wallet = importWallet(scriptType, inputStream, password); return wallet.getKeystores().get(0); } @Override - public Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException { + public Wallet importWallet(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { if(!ALLOWED_SCRIPT_TYPES.contains(scriptType)) { throw new ImportException("Script type of " + scriptType + " is not allowed"); } @@ -104,4 +105,9 @@ public class ColdcardSinglesig implements KeystoreFileImport, SinglesigWalletImp public String getWalletImportDescription() { return "Import file created by using the Advanced > Dump Summary feature on your Coldcard"; } + + @Override + public boolean isEncrypted(File file) { + return false; + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java index 88869fa7..0b221551 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Electrum.java @@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken; import com.sparrowwallet.drongo.ExtendedPublicKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; @@ -18,6 +19,7 @@ import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; +import java.util.zip.InflaterInputStream; public class Electrum implements KeystoreFileImport, SinglesigWalletImport, MultisigWalletImport, WalletExport { @Override @@ -41,8 +43,8 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult } @Override - public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { - Wallet wallet = importWallet(inputStream); + public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + Wallet wallet = importWallet(inputStream, password); if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) { throw new ImportException("Multisig wallet detected - import it using File > Import > Electrum"); @@ -57,14 +59,25 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult } @Override - public Wallet importWallet(InputStream inputStream) throws ImportException { - InputStreamReader reader = new InputStreamReader(inputStream); + public Wallet importWallet(InputStream inputStream, String password) throws ImportException { + Reader reader; + if(password != null) { + ECKey decryptionKey = ECKey.createKeyPbkdf2HmacSha512(password); + reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(inputStream, decryptionKey))); + } else { + reader = new InputStreamReader(inputStream); + } + try { Gson gson = new Gson(); Type stringStringMap = new TypeToken>(){}.getType(); Map map = gson.fromJson(reader, stringStringMap); ElectrumJsonWallet ew = new ElectrumJsonWallet(); + if(map.get("wallet_type") == null) { + throw new ImportException("This is not a valid Electrum wallet"); + } + ew.wallet_type = map.get("wallet_type").getAsString(); for(String key : map.keySet()) { @@ -137,9 +150,10 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult } @Override - public Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException { - Wallet wallet = importWallet(inputStream); + public Wallet importWallet(ScriptType scriptType, InputStream inputStream, String password) throws ImportException { + Wallet wallet = importWallet(inputStream, password); wallet.setScriptType(scriptType); + //TODO: Check this usage results in a valid wallet return wallet; } @@ -189,6 +203,11 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult } } + @Override + public boolean isEncrypted(File file) { + return FileType.BINARY.equals(IOUtils.getFileType(file)); + } + @Override public String getWalletExportDescription() { return "Export this wallet as an Electrum wallet file"; diff --git a/src/main/java/com/sparrowwallet/sparrow/io/FileType.java b/src/main/java/com/sparrowwallet/sparrow/io/FileType.java new file mode 100644 index 00000000..74ace824 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/FileType.java @@ -0,0 +1,5 @@ +package com.sparrowwallet.sparrow.io; + +public enum FileType { + TEXT, JSON, BINARY, UNKNOWN; +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java new file mode 100644 index 00000000..a885a8d6 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/io/IOUtils.java @@ -0,0 +1,24 @@ +package com.sparrowwallet.sparrow.io; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +public class IOUtils { + public static FileType getFileType(File file) { + try { + String type = Files.probeContentType(file.toPath()); + if (type == null) { + return FileType.BINARY; + } else if (type.equals("application/json")) { + return FileType.JSON; + } else if (type.startsWith("text")) { + return FileType.TEXT; + } + } catch (IOException e) { + //ignore + } + + return FileType.UNKNOWN; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileImport.java b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileImport.java index df4eee9a..b31a56dc 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/KeystoreFileImport.java @@ -3,8 +3,10 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Keystore; +import java.io.File; import java.io.InputStream; public interface KeystoreFileImport extends KeystoreImport { - Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException; + boolean isEncrypted(File file); + Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException; } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/MultisigWalletImport.java b/src/main/java/com/sparrowwallet/sparrow/io/MultisigWalletImport.java index 172ee16a..e12aeb8e 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/MultisigWalletImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/MultisigWalletImport.java @@ -2,9 +2,11 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.wallet.Wallet; +import java.io.File; import java.io.InputStream; public interface MultisigWalletImport extends Import { String getWalletImportDescription(); - Wallet importWallet(InputStream inputStream) throws ImportException; + Wallet importWallet(InputStream inputStream, String password) throws ImportException; + boolean isEncrypted(File file); } diff --git a/src/main/java/com/sparrowwallet/sparrow/io/SinglesigWalletImport.java b/src/main/java/com/sparrowwallet/sparrow/io/SinglesigWalletImport.java index a3424cc5..9d88703f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/SinglesigWalletImport.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/SinglesigWalletImport.java @@ -3,9 +3,11 @@ package com.sparrowwallet.sparrow.io; import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.wallet.Wallet; +import java.io.File; import java.io.InputStream; public interface SinglesigWalletImport extends Import { String getWalletImportDescription(); - Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException; + Wallet importWallet(ScriptType scriptType, InputStream inputStream, String password) throws ImportException; + boolean isEncrypted(File file); } diff --git a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css index 17747c29..6acf4fab 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css +++ b/src/main/resources/com/sparrowwallet/sparrow/keystoreimport/keystoreimport.css @@ -51,15 +51,31 @@ } -.status-label .text, .description-label Text { +.status-label .text, .description-label .text { -fx-fill: #a0a1a7; } -.status-error .text, .description-error Text { +.status-error .text, .description-error .text { -fx-fill: #ca1243; } -.description-label .text, description-error .text { +.description-label, .description-error { + -fx-border-width: 0px; + -fx-border-color: transparent; +} + +.hyperlink { + -fx-padding: 0; + -fx-border-width: 0; -fx-fill: #1e88cf; } +.hyperlink:visited { + -fx-text-fill: #1e88cf; + -fx-underline: false; +} + +.hyperlink:hover:visited { + -fx-underline: true; +} + diff --git a/src/test/java/com/sparrowwallet/sparrow/io/ECIESInputStreamTest.java b/src/test/java/com/sparrowwallet/sparrow/io/ECIESInputStreamTest.java index b483e12d..d3de15e9 100644 --- a/src/test/java/com/sparrowwallet/sparrow/io/ECIESInputStreamTest.java +++ b/src/test/java/com/sparrowwallet/sparrow/io/ECIESInputStreamTest.java @@ -14,7 +14,7 @@ public class ECIESInputStreamTest extends IoTest { public void decrypt() throws ImportException { Electrum electrum = new Electrum(); ECKey decryptionKey = ECKey.createKeyPbkdf2HmacSha512("pass"); - Wallet wallet = electrum.importWallet(new InflaterInputStream(new ECIESInputStream(getInputStream("electrum-encrypted"), decryptionKey))); + Wallet wallet = electrum.importWallet(new InflaterInputStream(new ECIESInputStream(getInputStream("electrum-encrypted"), decryptionKey)), null); Assert.assertEquals(PolicyType.SINGLE, wallet.getPolicyType()); Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());