mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
bip129 round 1 support with optional signing of bsms keystore exports
This commit is contained in:
parent
1cb6778502
commit
2a7f14a4ed
14 changed files with 565 additions and 49 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 50fdb71bf3e67db55b2cd66dc3fbdc8faf73be63
|
Subproject commit caed93ca6d3e29433c13ed9db705e34e057caf43
|
|
@ -215,7 +215,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||||
|
|
||||||
private Node getPasswordEntry(File file) {
|
private Node getPasswordEntry(File file) {
|
||||||
CustomPasswordField passwordField = new ViewPasswordField();
|
CustomPasswordField passwordField = new ViewPasswordField();
|
||||||
passwordField.setPromptText("Wallet password");
|
passwordField.setPromptText("Password");
|
||||||
password.bind(passwordField.textProperty());
|
password.bind(passwordField.textProperty());
|
||||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,168 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ButtonType;
|
||||||
|
import javafx.scene.control.Control;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.controlsfx.control.SegmentedButton;
|
||||||
|
import org.controlsfx.glyphfont.Glyph;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||||
|
private final Keystore keystore;
|
||||||
|
private final KeystoreFileExport exporter;
|
||||||
|
private final boolean scannable;
|
||||||
|
private final boolean file;
|
||||||
|
|
||||||
|
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||||
|
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||||
|
this.keystore = keystore;
|
||||||
|
this.exporter = exporter;
|
||||||
|
this.scannable = exporter.isKeystoreExportScannable();
|
||||||
|
this.file = exporter.isKeystoreExportFile();
|
||||||
|
|
||||||
|
buttonBox.getChildren().clear();
|
||||||
|
buttonBox.getChildren().add(createButton());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Control createButton() {
|
||||||
|
if(scannable && file) {
|
||||||
|
ToggleButton showButton = new ToggleButton("Show...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
showButton.setGraphic(cameraGlyph);
|
||||||
|
showButton.setOnAction(event -> {
|
||||||
|
showButton.setSelected(false);
|
||||||
|
exportQR();
|
||||||
|
});
|
||||||
|
|
||||||
|
ToggleButton fileButton = new ToggleButton("Export File...");
|
||||||
|
fileButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
fileButton.setOnAction(event -> {
|
||||||
|
fileButton.setSelected(false);
|
||||||
|
exportFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
SegmentedButton segmentedButton = new SegmentedButton();
|
||||||
|
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||||
|
return segmentedButton;
|
||||||
|
} else if(scannable) {
|
||||||
|
Button showButton = new Button("Show...");
|
||||||
|
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||||
|
cameraGlyph.setFontSize(12);
|
||||||
|
showButton.setGraphic(cameraGlyph);
|
||||||
|
showButton.setOnAction(event -> {
|
||||||
|
exportQR();
|
||||||
|
});
|
||||||
|
return showButton;
|
||||||
|
} else {
|
||||||
|
Button exportButton = new Button("Export File...");
|
||||||
|
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
exportButton.setOnAction(event -> {
|
||||||
|
exportFile();
|
||||||
|
});
|
||||||
|
return exportButton;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportQR() {
|
||||||
|
exportKeystore(null, keystore);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportFile() {
|
||||||
|
Stage window = new Stage();
|
||||||
|
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||||
|
String extension = exporter.getExportFileExtension(keystore);
|
||||||
|
String fileName = keystore.getLabel();
|
||||||
|
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||||
|
|
||||||
|
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||||
|
File file = fileChooser.showSaveDialog(window);
|
||||||
|
if(file != null) {
|
||||||
|
exportKeystore(file, keystore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exportKeystore(File file, Keystore exportKeystore) {
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||||
|
exporter.exportKeystore(exportKeystore, baos);
|
||||||
|
|
||||||
|
if(exporter.requiresSignature()) {
|
||||||
|
String message = baos.toString(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
|
||||||
|
TextAreaDialog dialog = new TextAreaDialog(message, false);
|
||||||
|
dialog.setTitle("Sign " + exporter.getName() + " Export");
|
||||||
|
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
|
||||||
|
dialog.showAndWait();
|
||||||
|
|
||||||
|
Wallet wallet = new Wallet();
|
||||||
|
wallet.setScriptType(ScriptType.P2PKH);
|
||||||
|
wallet.getKeystores().add(keystore);
|
||||||
|
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
|
|
||||||
|
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
|
||||||
|
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||||
|
if(optSignature.isPresent()) {
|
||||||
|
exporter.addSignature(keystore, optSignature.get(), baos);
|
||||||
|
}
|
||||||
|
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||||
|
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
|
||||||
|
exporter.addSignature(keystore, signature, baos);
|
||||||
|
} else {
|
||||||
|
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
|
||||||
|
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
|
||||||
|
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
|
||||||
|
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
|
||||||
|
throw new RuntimeException("Export aborted due to lack of device message signing support.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(file != null) {
|
||||||
|
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||||
|
outputStream.write(baos.toByteArray());
|
||||||
|
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QRDisplayDialog qrDisplayDialog;
|
||||||
|
if(exporter instanceof Bip129) {
|
||||||
|
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
|
||||||
|
} else {
|
||||||
|
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
qrDisplayDialog.showAndWait();
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
String errorMessage = e.getMessage();
|
||||||
|
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||||
|
errorMessage = e.getCause().getMessage();
|
||||||
|
}
|
||||||
|
setError("Export Error", errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.google.common.eventbus.Subscribe;
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||||
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.layout.AnchorPane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class KeystoreExportDialog extends Dialog<Keystore> {
|
||||||
|
public KeystoreExportDialog(Keystore keystore) {
|
||||||
|
EventManager.get().register(this);
|
||||||
|
setOnCloseRequest(event -> {
|
||||||
|
EventManager.get().unregister(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
final DialogPane dialogPane = getDialogPane();
|
||||||
|
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||||
|
|
||||||
|
StackPane stackPane = new StackPane();
|
||||||
|
dialogPane.setContent(stackPane);
|
||||||
|
|
||||||
|
AnchorPane anchorPane = new AnchorPane();
|
||||||
|
stackPane.getChildren().add(anchorPane);
|
||||||
|
|
||||||
|
ScrollPane scrollPane = new ScrollPane();
|
||||||
|
scrollPane.setPrefHeight(200);
|
||||||
|
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||||
|
anchorPane.getChildren().add(scrollPane);
|
||||||
|
scrollPane.setFitToWidth(true);
|
||||||
|
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||||
|
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||||
|
|
||||||
|
List<KeystoreFileExport> exporters = List.of(new Bip129());
|
||||||
|
|
||||||
|
Accordion exportAccordion = new Accordion();
|
||||||
|
for(KeystoreFileExport exporter : exporters) {
|
||||||
|
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||||
|
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
|
||||||
|
exportAccordion.getPanes().add(exportPane);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||||
|
scrollPane.setContent(exportAccordion);
|
||||||
|
|
||||||
|
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||||
|
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||||
|
dialogPane.setPrefWidth(500);
|
||||||
|
dialogPane.setPrefHeight(280);
|
||||||
|
AppServices.moveToActiveWindowScreen(this);
|
||||||
|
|
||||||
|
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void keystoreExported(KeystoreExportEvent event) {
|
||||||
|
setResult(event.getKeystore());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.sparrowwallet.sparrow.event;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
|
||||||
|
public class KeystoreExportEvent {
|
||||||
|
private final Keystore keystore;
|
||||||
|
|
||||||
|
public KeystoreExportEvent(Keystore keystore) {
|
||||||
|
this.keystore = keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Keystore getKeystore() {
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
}
|
189
src/main/java/com/sparrowwallet/sparrow/io/Bip129.java
Normal file
189
src/main/java/com/sparrowwallet/sparrow/io/Bip129.java
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.google.common.io.CharStreams;
|
||||||
|
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
|
||||||
|
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.*;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Bip129 implements KeystoreFileExport, KeystoreFileImport {
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "BSMS";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WalletModel getWalletModel() {
|
||||||
|
return WalletModel.SEED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreExportDescription() {
|
||||||
|
return "Exports the keystore in the Bitcoin Secure Multisig Setup (BSMS) format.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException {
|
||||||
|
if(!keystore.isValid()) {
|
||||||
|
throw new ExportException("Invalid keystore");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
String record = "BSMS 1.0\n00\n[" +
|
||||||
|
keystore.getKeyDerivation().toString() +
|
||||||
|
"]" +
|
||||||
|
keystore.getExtendedPublicKey().toString() +
|
||||||
|
"\n" +
|
||||||
|
keystore.getLabel();
|
||||||
|
outputStream.write(record.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new ExportException("Error writing BSMS file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresSignature() {
|
||||||
|
//Due to poor vendor support of multiline message signing at the xpub derivation path, signing BSMS keystore exports is configurable (default false)
|
||||||
|
return Config.get().isSignBsmsExports();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException {
|
||||||
|
try {
|
||||||
|
String append = "\n" + signature;
|
||||||
|
outputStream.write(append.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new ExportException("Error writing BSMS file", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getExportFileExtension(Keystore keystore) {
|
||||||
|
return "bsms";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeystoreExportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEncrypted(File file) {
|
||||||
|
try {
|
||||||
|
try(BufferedReader reader = new BufferedReader(new FileReader(file, StandardCharsets.UTF_8))) {
|
||||||
|
String text = CharStreams.toString(reader);
|
||||||
|
return Utils.isHex(text.trim());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||||
|
try {
|
||||||
|
try(BufferedReader streamReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
||||||
|
BufferedReader reader = streamReader;
|
||||||
|
if(password != null) {
|
||||||
|
byte[] token;
|
||||||
|
if((password.length() == 16 || password.length() == 32) && Utils.isHex(password)) {
|
||||||
|
token = Utils.hexToBytes(password);
|
||||||
|
} else if(Utils.isNumber(password)) {
|
||||||
|
BigInteger bi = new BigInteger(password);
|
||||||
|
token = Utils.bigIntegerToBytes(bi, bi.toByteArray().length >= 16 ? 16 : 8);
|
||||||
|
} else if(password.split(" ").length == 6 || password.split(" ").length == 12) {
|
||||||
|
List<String> mnemonicWords = Arrays.asList(password.split(" "));
|
||||||
|
token = Bip39MnemonicCode.INSTANCE.toEntropy(mnemonicWords);
|
||||||
|
} else {
|
||||||
|
throw new ImportException("Provided password needs to be in hexadecimal, decimal or mnemonic format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
String hex = CharStreams.toString(streamReader).trim();
|
||||||
|
byte[] data = Utils.hexToBytes(hex);
|
||||||
|
byte[] mac = Arrays.copyOfRange(data, 0, 32);
|
||||||
|
byte[] iv = Arrays.copyOfRange(mac, 0, 16);
|
||||||
|
byte[] ciphertext = Arrays.copyOfRange(data, 32, data.length);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||||
|
|
||||||
|
Pbkdf2KeyDeriver pbkdf2KeyDeriver = new Pbkdf2KeyDeriver(token, 2048, 256);
|
||||||
|
byte[] key = pbkdf2KeyDeriver.deriveKey("No SPOF").getKeyBytes();
|
||||||
|
|
||||||
|
Key keySpec = new SecretKeySpec(key, "AES");
|
||||||
|
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||||
|
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||||
|
byte[] plaintext = cipher.doFinal(ciphertext);
|
||||||
|
String plaintextString = new String(plaintext, StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
SecretKeySpec secretKeySpec = new SecretKeySpec(Sha256Hash.hash(key), "HmacSHA256");
|
||||||
|
Mac hmac = Mac.getInstance("HmacSHA256");
|
||||||
|
hmac.init(secretKeySpec);
|
||||||
|
String macData = Utils.bytesToHex(token) + plaintextString;
|
||||||
|
byte[] calculatedMac = hmac.doFinal(macData.getBytes(StandardCharsets.UTF_8));
|
||||||
|
if(!Arrays.equals(mac, calculatedMac)) {
|
||||||
|
throw new ImportException("Message digest authentication failed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
reader = new BufferedReader(new StringReader(plaintextString));
|
||||||
|
}
|
||||||
|
|
||||||
|
String header = reader.readLine();
|
||||||
|
String token = reader.readLine();
|
||||||
|
String descriptor = reader.readLine();
|
||||||
|
String label = reader.readLine();
|
||||||
|
String signature = reader.readLine();
|
||||||
|
|
||||||
|
return getKeystore(header, token, descriptor, label, signature);
|
||||||
|
}
|
||||||
|
} catch(MnemonicException.MnemonicWordException e) {
|
||||||
|
throw new ImportException("Error importing BSMS: Invalid mnemonic word " + e.badWord, e);
|
||||||
|
} catch(MnemonicException.MnemonicChecksumException e) {
|
||||||
|
throw new ImportException("Error importing BSMS: Invalid mnemonic checksum", e);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new ImportException("Error importing BSMS", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Keystore getKeystore(String header, String token, String descriptor, String label, String signature) throws ImportException {
|
||||||
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor("sh(" + descriptor + ")");
|
||||||
|
Wallet wallet = outputDescriptor.toWallet();
|
||||||
|
Keystore keystore = wallet.getKeystores().get(0);
|
||||||
|
keystore.setLabel(label);
|
||||||
|
|
||||||
|
if(signature != null) {
|
||||||
|
try {
|
||||||
|
String message = header + "\n" + token + "\n" + descriptor + "\n" + label;
|
||||||
|
keystore.getExtendedPublicKey().getKey().verifyMessage(message, signature);
|
||||||
|
} catch(SignatureException e) {
|
||||||
|
throw new ImportException("Signature did not match provided public key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreImportDescription(int account) {
|
||||||
|
return "Imports a keystore that was exported using the Bitcoin Secure Multisig Setup (BSMS) format.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isKeystoreImportScannable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,6 +48,7 @@ public class Config {
|
||||||
private boolean showLoadingLog = true;
|
private boolean showLoadingLog = true;
|
||||||
private boolean showAddressTransactionCount = false;
|
private boolean showAddressTransactionCount = false;
|
||||||
private boolean showDeprecatedImportExport = false;
|
private boolean showDeprecatedImportExport = false;
|
||||||
|
private boolean signBsmsExports = false;
|
||||||
private boolean preventSleep = false;
|
private boolean preventSleep = false;
|
||||||
private List<File> recentWalletFiles;
|
private List<File> recentWalletFiles;
|
||||||
private Integer keyDerivationPeriod;
|
private Integer keyDerivationPeriod;
|
||||||
|
@ -315,6 +316,15 @@ public class Config {
|
||||||
flush();
|
flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isSignBsmsExports() {
|
||||||
|
return signBsmsExports;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSignBsmsExports(boolean signBsmsExports) {
|
||||||
|
this.signBsmsExports = signBsmsExports;
|
||||||
|
flush();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isPreventSleep() {
|
public boolean isPreventSleep() {
|
||||||
return preventSleep;
|
return preventSleep;
|
||||||
}
|
}
|
||||||
|
|
|
@ -301,11 +301,19 @@ public class Hwi {
|
||||||
Process process = null;
|
Process process = null;
|
||||||
try {
|
try {
|
||||||
List<String> processArguments = new ArrayList<>(arguments);
|
List<String> processArguments = new ArrayList<>(arguments);
|
||||||
|
|
||||||
|
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
|
||||||
|
if(useStdin) {
|
||||||
processArguments.add("--stdin");
|
processArguments.add("--stdin");
|
||||||
|
} else {
|
||||||
|
processArguments.add(command.toString());
|
||||||
|
processArguments.addAll(Arrays.asList(commandArguments));
|
||||||
|
}
|
||||||
|
|
||||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||||
process = processBuilder.start();
|
process = processBuilder.start();
|
||||||
|
|
||||||
|
if(useStdin) {
|
||||||
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||||
writer.write(command.toString());
|
writer.write(command.toString());
|
||||||
for(String commandArgument : commandArguments) {
|
for(String commandArgument : commandArguments) {
|
||||||
|
@ -315,6 +323,7 @@ public class Hwi {
|
||||||
}
|
}
|
||||||
writer.flush();
|
writer.flush();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return getProcessOutput(process);
|
return getProcessOutput(process);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
public interface KeystoreExport extends ImportExport {
|
||||||
|
String getKeystoreExportDescription();
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.sparrowwallet.sparrow.io;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
|
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public interface KeystoreFileExport extends KeystoreExport {
|
||||||
|
void exportKeystore(Keystore keystore, OutputStream outputStream) throws ExportException;
|
||||||
|
boolean requiresSignature();
|
||||||
|
void addSignature(Keystore keystore, String signature, OutputStream outputStream) throws ExportException;
|
||||||
|
String getExportFileExtension(Keystore keystore);
|
||||||
|
boolean isKeystoreExportScannable();
|
||||||
|
default boolean isKeystoreExportFile() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,10 +23,7 @@ import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import javax.smartcardio.CardException;
|
import javax.smartcardio.CardException;
|
||||||
import java.util.Base64;
|
import java.util.*;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class CkCardApi extends CardApi {
|
public class CkCardApi extends CardApi {
|
||||||
private static final Logger log = LoggerFactory.getLogger(CkCardApi.class);
|
private static final Logger log = LoggerFactory.getLogger(CkCardApi.class);
|
||||||
|
@ -227,8 +224,18 @@ public class CkCardApi extends CardApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
|
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
|
||||||
List<ChildNumber> keystoreDerivation = derivation.subList(0, derivation.size() - 2);
|
List<ChildNumber> keystoreDerivation;
|
||||||
List<ChildNumber> subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size());
|
List<ChildNumber> subPathDerivation;
|
||||||
|
|
||||||
|
Optional<ChildNumber> firstUnhardened = derivation.stream().filter(cn -> !cn.isHardened()).findFirst();
|
||||||
|
if(firstUnhardened.isPresent()) {
|
||||||
|
int index = derivation.indexOf(firstUnhardened.get());
|
||||||
|
keystoreDerivation = derivation.subList(0, index);
|
||||||
|
subPathDerivation = derivation.subList(index, derivation.size());
|
||||||
|
} else {
|
||||||
|
keystoreDerivation = derivation;
|
||||||
|
subPathDerivation = Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
Keystore cardKeystore = getKeystore();
|
Keystore cardKeystore = getKeystore();
|
||||||
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
|
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
|
||||||
|
@ -239,9 +246,15 @@ public class CkCardApi extends CardApi {
|
||||||
signingKeystore = getKeystore();
|
signingKeystore = getKeystore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ECKey signingPubKey;
|
||||||
|
if(subPathDerivation.isEmpty()) {
|
||||||
|
signingPubKey = signingKeystore.getExtendedPublicKey().getKey();
|
||||||
|
} else {
|
||||||
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
|
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
|
||||||
ECKey addressPubKey = signingKeystore.getPubKey(addressNode);
|
signingPubKey = signingKeystore.getPubKey(addressNode);
|
||||||
return addressPubKey.signMessage(message, scriptType, hash -> {
|
}
|
||||||
|
|
||||||
|
return signingPubKey.signMessage(message, scriptType, hash -> {
|
||||||
try {
|
try {
|
||||||
CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash);
|
CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash);
|
||||||
return cardSign.getSignature();
|
return cardSign.getSignature();
|
||||||
|
|
|
@ -28,7 +28,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||||
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||||
fileImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
fileImporters = List.of(new Bip129(), new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||||
}
|
}
|
||||||
|
|
||||||
for(KeystoreFileImport importer : fileImporters) {
|
for(KeystoreFileImport importer : fileImporters) {
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||||
|
|
||||||
import com.google.common.eventbus.Subscribe;
|
import com.google.common.eventbus.Subscribe;
|
||||||
import com.sparrowwallet.drongo.*;
|
import com.sparrowwallet.drongo.*;
|
||||||
|
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||||
import com.sparrowwallet.drongo.wallet.*;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
|
@ -56,6 +57,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
@FXML
|
@FXML
|
||||||
private Label type;
|
private Label type;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button exportButton;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button viewSeedButton;
|
private Button viewSeedButton;
|
||||||
|
|
||||||
|
@ -129,6 +133,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportButton.managedProperty().bind(exportButton.visibleProperty());
|
||||||
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
|
viewSeedButton.managedProperty().bind(viewSeedButton.visibleProperty());
|
||||||
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
|
viewKeyButton.managedProperty().bind(viewKeyButton.visibleProperty());
|
||||||
changePinButton.managedProperty().bind(changePinButton.visibleProperty());
|
changePinButton.managedProperty().bind(changePinButton.visibleProperty());
|
||||||
|
@ -136,7 +141,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
|
displayXpubQR.managedProperty().bind(displayXpubQR.visibleProperty());
|
||||||
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
|
displayXpubQR.visibleProperty().bind(scanXpubQR.visibleProperty().not());
|
||||||
|
|
||||||
updateType();
|
updateType(false);
|
||||||
|
|
||||||
label.setText(keystore.getLabel());
|
label.setText(keystore.getLabel());
|
||||||
|
|
||||||
|
@ -280,9 +285,10 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateType() {
|
private void updateType(boolean showExport) {
|
||||||
type.setText(getTypeLabel(keystore));
|
type.setText(getTypeLabel(keystore));
|
||||||
type.setGraphic(getTypeIcon(keystore));
|
type.setGraphic(getTypeIcon(keystore));
|
||||||
|
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
|
||||||
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
||||||
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
||||||
changePinButton.setVisible(keystore.getWalletModel().isCard());
|
changePinButton.setVisible(keystore.getWalletModel().isCard());
|
||||||
|
@ -309,9 +315,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
private String getTypeLabel(Keystore keystore) {
|
private String getTypeLabel(Keystore keystore) {
|
||||||
switch (keystore.getSource()) {
|
switch (keystore.getSource()) {
|
||||||
case HW_USB:
|
case HW_USB:
|
||||||
return "Connected Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
|
return "Connected Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
|
||||||
case HW_AIRGAPPED:
|
case HW_AIRGAPPED:
|
||||||
return "Airgapped Hardware Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
|
return "Airgapped Wallet (" + keystore.getWalletModel().toDisplayString() + ")";
|
||||||
case SW_SEED:
|
case SW_SEED:
|
||||||
return "Software Wallet";
|
return "Software Wallet";
|
||||||
case SW_WATCH:
|
case SW_WATCH:
|
||||||
|
@ -367,7 +373,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
keystore.setSeed(importedKeystore.getSeed());
|
keystore.setSeed(importedKeystore.getSeed());
|
||||||
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
|
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
|
||||||
|
|
||||||
updateType();
|
updateType(true);
|
||||||
label.setText(keystore.getLabel());
|
label.setText(keystore.getLabel());
|
||||||
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||||
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||||
|
@ -381,6 +387,11 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void export(ActionEvent event) {
|
||||||
|
KeystoreExportDialog keystoreExportDialog = new KeystoreExportDialog(keystore);
|
||||||
|
keystoreExportDialog.showAndWait();
|
||||||
|
}
|
||||||
|
|
||||||
public void showPrivate(ActionEvent event) {
|
public void showPrivate(ActionEvent event) {
|
||||||
int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore);
|
int keystoreIndex = getWalletForm().getWallet().getKeystores().indexOf(keystore);
|
||||||
Wallet copy = getWalletForm().getWallet().copy();
|
Wallet copy = getWalletForm().getWallet().copy();
|
||||||
|
@ -614,4 +625,11 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void walletSettingsChanged(WalletSettingsChangedEvent event) {
|
||||||
|
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||||
|
exportButton.setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
<Fieldset inputGrow="SOMETIMES" text="">
|
<Fieldset inputGrow="SOMETIMES" text="">
|
||||||
<Field text="Type:">
|
<Field text="Type:">
|
||||||
<Label fx:id="type" graphicTextGap="5"/>
|
<Label fx:id="type" graphicTextGap="5"/>
|
||||||
|
<HBox spacing="10" alignment="CENTER_LEFT">
|
||||||
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
|
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
|
||||||
<graphic>
|
<graphic>
|
||||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
|
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
|
||||||
|
@ -48,6 +49,12 @@
|
||||||
<Tooltip text="Change the PIN of current card"/>
|
<Tooltip text="Change the PIN of current card"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button fx:id="exportButton" text="Export..." onAction="#export">
|
||||||
|
<tooltip>
|
||||||
|
<Tooltip text="Export this keystore as a signer for a multisig wallet"/>
|
||||||
|
</tooltip>
|
||||||
|
</Button>
|
||||||
|
</HBox>
|
||||||
<Pane HBox.hgrow="ALWAYS" />
|
<Pane HBox.hgrow="ALWAYS" />
|
||||||
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
|
<Button fx:id="importButton" text="Import..." graphicTextGap="5" onAction="#importKeystore">
|
||||||
<graphic>
|
<graphic>
|
||||||
|
|
Loading…
Reference in a new issue