encrypted wallet and keystore import

This commit is contained in:
Craig Raw 2020-04-30 16:14:49 +02:00
parent 60c1c17d26
commit 2ee9f3d10a
13 changed files with 181 additions and 37 deletions

View file

@ -17,6 +17,8 @@ import com.sparrowwallet.sparrow.control.WalletNameDialog;
import com.sparrowwallet.sparrow.event.TabEvent; import com.sparrowwallet.sparrow.event.TabEvent;
import com.sparrowwallet.sparrow.event.TransactionTabChangedEvent; import com.sparrowwallet.sparrow.event.TransactionTabChangedEvent;
import com.sparrowwallet.sparrow.event.TransactionTabSelectedEvent; 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.io.Storage;
import com.sparrowwallet.sparrow.transaction.TransactionController; import com.sparrowwallet.sparrow.transaction.TransactionController;
import com.sparrowwallet.sparrow.wallet.SettingsController; import com.sparrowwallet.sparrow.wallet.SettingsController;
@ -227,9 +229,10 @@ public class AppController implements Initializable {
try { try {
Wallet wallet; Wallet wallet;
ECKey encryptionPubKey = WalletForm.NO_PASSWORD_KEY; ECKey encryptionPubKey = WalletForm.NO_PASSWORD_KEY;
try { FileType fileType = IOUtils.getFileType(file);
if(FileType.JSON.equals(fileType)) {
wallet = Storage.getStorage().loadWallet(file); wallet = Storage.getStorage().loadWallet(file);
} catch(JsonSyntaxException e) { } else if(FileType.BINARY.equals(fileType)) {
Optional<ECKey> optionalFullKey = SettingsController.askForWalletPassword(null, true); Optional<ECKey> optionalFullKey = SettingsController.askForWalletPassword(null, true);
if(!optionalFullKey.isPresent()) { if(!optionalFullKey.isPresent()) {
return; return;
@ -238,6 +241,8 @@ public class AppController implements Initializable {
ECKey encryptionFullKey = optionalFullKey.get(); ECKey encryptionFullKey = optionalFullKey.get();
wallet = Storage.getStorage().loadWallet(file, encryptionFullKey); wallet = Storage.getStorage().loadWallet(file, encryptionFullKey);
encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey); encryptionPubKey = ECKey.fromPublicOnly(encryptionFullKey);
} else {
throw new IOException("Unsupported file type");
} }
Tab tab = addWalletTab(file, encryptionPubKey, wallet); Tab tab = addWalletTab(file, encryptionPubKey, wallet);

View file

@ -38,7 +38,7 @@ public class MainApp extends Application {
Wallet wallet = new Wallet(); Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI); wallet.setPolicyType(PolicyType.MULTI);
wallet.setScriptType(ScriptType.P2SH); wallet.setScriptType(ScriptType.P2WPKH);
KeystoreImportDialog dlg = new KeystoreImportDialog(wallet); KeystoreImportDialog dlg = new KeystoreImportDialog(wallet);
//dlg.showAndWait(); //dlg.showAndWait();

View file

@ -1,16 +1,19 @@
package com.sparrowwallet.sparrow.control; package com.sparrowwallet.sparrow.control;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent; import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
import com.sparrowwallet.sparrow.io.KeystoreFileImport; import com.sparrowwallet.sparrow.io.KeystoreFileImport;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.TitledPane; import javafx.scene.control.TitledPane;
import javafx.scene.image.Image; import javafx.scene.image.Image;
@ -20,7 +23,8 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import org.controlsfx.control.HyperlinkLabel; import org.controlsfx.control.textfield.CustomPasswordField;
import org.controlsfx.control.textfield.TextFields;
import java.io.*; import java.io.*;
@ -30,7 +34,11 @@ public class KeystoreFileImportPane extends TitledPane {
private final KeystoreFileImport importer; private final KeystoreFileImport importer;
private Label mainLabel; 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) { public KeystoreFileImportPane(KeystoreImportAccordion importAccordion, Wallet wallet, KeystoreFileImport importer) {
this.importAccordion = importAccordion; this.importAccordion = importAccordion;
@ -83,21 +91,32 @@ public class KeystoreFileImportPane extends TitledPane {
mainLabel.getStyleClass().add("main-label"); mainLabel.getStyleClass().add("main-label");
labelsBox.getChildren().add(mainLabel); 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.getStyleClass().add("description-label");
descriptionLabel.setText("Keystore file import [View Details...]"); showHideLink = new Hyperlink("View Details...");
descriptionLabel.setOnAction(event -> { showHideLink.managedProperty().bind(showHideLink.visibleProperty());
showHideLink.setOnAction(event -> {
if(showHideLink.getText().contains("View")) {
setExpanded(true); setExpanded(true);
showHideLink.setText("Hide Details...");
} else {
setExpanded(false);
showHideLink.setText("View Details...");
}
}); });
descriptionBox.getChildren().addAll(descriptionLabel, showHideLink);
listItem.getChildren().add(labelsBox); listItem.getChildren().add(labelsBox);
HBox.setHgrow(labelsBox, Priority.ALWAYS); HBox.setHgrow(labelsBox, Priority.ALWAYS);
HBox buttonBox = new HBox(); HBox buttonBox = new HBox();
buttonBox.setAlignment(Pos.CENTER_RIGHT); buttonBox.setAlignment(Pos.CENTER_RIGHT);
Button importButton = new Button("Import File..."); importButton = new Button("Import File...");
importButton.setAlignment(Pos.CENTER_RIGHT); importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> { importButton.setOnAction(event -> {
importFile(); importFile();
@ -125,29 +144,44 @@ public class KeystoreFileImportPane extends TitledPane {
File file = fileChooser.showOpenDialog(window); File file = fileChooser.showOpenDialog(window);
if(file != null) { if(file != null) {
importFile(file); importFile(file, null);
} }
} }
private void importFile(File file) { private void importFile(File file, String password) {
if(file.exists()) { if(file.exists()) {
try { try {
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)); InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream); Keystore keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
EventManager.get().post(new KeystoreImportEvent(keystore)); EventManager.get().post(new KeystoreImportEvent(keystore));
}
} catch (Exception e) { } catch (Exception e) {
setExpanded(false);
descriptionLabel.getStyleClass().remove("description-label"); descriptionLabel.getStyleClass().remove("description-label");
descriptionLabel.getStyleClass().add("description-error"); descriptionLabel.getStyleClass().add("description-error");
descriptionLabel.setText("Error Importing [View Details...]"); descriptionLabel.setText("Import Error");
String errorMessage = e.getMessage(); String errorMessage = e.getMessage();
if(e.getCause() != null) { if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
errorMessage = e.getCause().getMessage(); 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) { if(e instanceof JsonParseException || e.getCause() instanceof JsonParseException) {
errorMessage = "File was not in JSON format"; errorMessage = "File was not in JSON format";
} }
setContent(getContentBox(errorMessage)); setContent(getContentBox(errorMessage));
setExpanded(true);
showHideLink.setText("Hide Details...");
importButton.setDisable(false);
} }
} }
} }
@ -168,4 +202,28 @@ public class KeystoreFileImportPane extends TitledPane {
return contentBox; 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;
}
} }

View file

@ -37,7 +37,7 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor
} }
@Override @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); InputStreamReader reader = new InputStreamReader(inputStream);
ColdcardKeystore cck = Storage.getStorage().getGson().fromJson(reader, ColdcardKeystore.class); ColdcardKeystore cck = Storage.getStorage().getGson().fromJson(reader, ColdcardKeystore.class);
@ -77,7 +77,7 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor
} }
@Override @Override
public Wallet importWallet(InputStream inputStream) throws ImportException { public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = new Wallet(); Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI); wallet.setPolicyType(PolicyType.MULTI);
@ -193,4 +193,9 @@ public class ColdcardMultisig implements MultisigWalletImport, KeystoreFileImpor
public String getWalletExportDescription() { public String getWalletExportDescription() {
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature"; 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;
}
} }

View file

@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel; import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.List; import java.util.List;
@ -42,14 +43,14 @@ public class ColdcardSinglesig implements KeystoreFileImport, SinglesigWalletImp
} }
@Override @Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importWallet(scriptType, inputStream); Wallet wallet = importWallet(scriptType, inputStream, password);
return wallet.getKeystores().get(0); return wallet.getKeystores().get(0);
} }
@Override @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)) { if(!ALLOWED_SCRIPT_TYPES.contains(scriptType)) {
throw new ImportException("Script type of " + scriptType + " is not allowed"); throw new ImportException("Script type of " + scriptType + " is not allowed");
} }
@ -104,4 +105,9 @@ public class ColdcardSinglesig implements KeystoreFileImport, SinglesigWalletImp
public String getWalletImportDescription() { public String getWalletImportDescription() {
return "Import file created by using the Advanced > Dump Summary feature on your Coldcard"; return "Import file created by using the Advanced > Dump Summary feature on your Coldcard";
} }
@Override
public boolean isEncrypted(File file) {
return false;
}
} }

View file

@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.ExtendedPublicKey; import com.sparrowwallet.drongo.ExtendedPublicKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy; import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
@ -18,6 +19,7 @@ import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.zip.InflaterInputStream;
public class Electrum implements KeystoreFileImport, SinglesigWalletImport, MultisigWalletImport, WalletExport { public class Electrum implements KeystoreFileImport, SinglesigWalletImport, MultisigWalletImport, WalletExport {
@Override @Override
@ -41,8 +43,8 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult
} }
@Override @Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream) throws ImportException { public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importWallet(inputStream); Wallet wallet = importWallet(inputStream, password);
if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) { if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) {
throw new ImportException("Multisig wallet detected - import it using File > Import > Electrum"); throw new ImportException("Multisig wallet detected - import it using File > Import > Electrum");
@ -57,14 +59,25 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult
} }
@Override @Override
public Wallet importWallet(InputStream inputStream) throws ImportException { public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream); 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 { try {
Gson gson = new Gson(); Gson gson = new Gson();
Type stringStringMap = new TypeToken<Map<String, JsonElement>>(){}.getType(); Type stringStringMap = new TypeToken<Map<String, JsonElement>>(){}.getType();
Map<String,JsonElement> map = gson.fromJson(reader, stringStringMap); Map<String,JsonElement> map = gson.fromJson(reader, stringStringMap);
ElectrumJsonWallet ew = new ElectrumJsonWallet(); 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(); ew.wallet_type = map.get("wallet_type").getAsString();
for(String key : map.keySet()) { for(String key : map.keySet()) {
@ -137,9 +150,10 @@ public class Electrum implements KeystoreFileImport, SinglesigWalletImport, Mult
} }
@Override @Override
public Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException { public Wallet importWallet(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importWallet(inputStream); Wallet wallet = importWallet(inputStream, password);
wallet.setScriptType(scriptType); wallet.setScriptType(scriptType);
//TODO: Check this usage results in a valid wallet
return 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 @Override
public String getWalletExportDescription() { public String getWalletExportDescription() {
return "Export this wallet as an Electrum wallet file"; return "Export this wallet as an Electrum wallet file";

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.io;
public enum FileType {
TEXT, JSON, BINARY, UNKNOWN;
}

View file

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

View file

@ -3,8 +3,10 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport { 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;
} }

View file

@ -2,9 +2,11 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
public interface MultisigWalletImport extends Import { public interface MultisigWalletImport extends Import {
String getWalletImportDescription(); String getWalletImportDescription();
Wallet importWallet(InputStream inputStream) throws ImportException; Wallet importWallet(InputStream inputStream, String password) throws ImportException;
boolean isEncrypted(File file);
} }

View file

@ -3,9 +3,11 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
public interface SinglesigWalletImport extends Import { public interface SinglesigWalletImport extends Import {
String getWalletImportDescription(); String getWalletImportDescription();
Wallet importWallet(ScriptType scriptType, InputStream inputStream) throws ImportException; Wallet importWallet(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
boolean isEncrypted(File file);
} }

View file

@ -51,15 +51,31 @@
} }
.status-label .text, .description-label Text { .status-label .text, .description-label .text {
-fx-fill: #a0a1a7; -fx-fill: #a0a1a7;
} }
.status-error .text, .description-error Text { .status-error .text, .description-error .text {
-fx-fill: #ca1243; -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; -fx-fill: #1e88cf;
} }
.hyperlink:visited {
-fx-text-fill: #1e88cf;
-fx-underline: false;
}
.hyperlink:hover:visited {
-fx-underline: true;
}

View file

@ -14,7 +14,7 @@ public class ECIESInputStreamTest extends IoTest {
public void decrypt() throws ImportException { public void decrypt() throws ImportException {
Electrum electrum = new Electrum(); Electrum electrum = new Electrum();
ECKey decryptionKey = ECKey.createKeyPbkdf2HmacSha512("pass"); 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(PolicyType.SINGLE, wallet.getPolicyType());
Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType()); Assert.assertEquals(ScriptType.P2WPKH, wallet.getScriptType());