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.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<ECKey> 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);

View file

@ -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();

View file

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

View file

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

View file

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

View file

@ -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<Map<String, JsonElement>>(){}.getType();
Map<String,JsonElement> 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";

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

View file

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

View file

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

View file

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

View file

@ -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());