unseal satscard functionality added to sweep private key dialog

This commit is contained in:
Craig Raw 2023-01-31 09:30:53 +02:00
parent 300545b289
commit 176e440195
13 changed files with 291 additions and 8 deletions

View file

@ -161,7 +161,8 @@ run {
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson"] "--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"]
if(os.macOsX) { if(os.macOsX) {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png", applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
@ -210,6 +211,7 @@ jlink {
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow", "--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.io=com.google.gson", "--add-opens=java.base/java.io=com.google.gson",
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop", "--add-reads=com.sparrowwallet.merged.module=java.desktop",
"--add-reads=com.sparrowwallet.merged.module=java.sql", "--add-reads=com.sparrowwallet.merged.module=java.sql",
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow", "--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor; import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
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;
@ -65,6 +66,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button displayAddressButton; private Button displayAddressButton;
private Button signMessageButton; private Button signMessageButton;
private Button discoverKeystoresButton; private Button discoverKeystoresButton;
private Button unsealButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty(""); private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty(""); private final SimpleStringProperty pin = new SimpleStringProperty("");
@ -199,6 +201,32 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton); buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
} }
public DevicePane(Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.UNSEAL;
this.wallet = null;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
this.availableAccounts = null;
setDefaultStatus();
showHideLink.setVisible(false);
createUnsealButton();
initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().add(unsealButton);
}
private void initialise(Device device) { private void initialise(Device device) {
if(device.isNeedsPinSent()) { if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice); unlockButton.setDefaultButton(defaultDevice);
@ -342,6 +370,17 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setVisible(false); discoverKeystoresButton.setVisible(false);
} }
private void createUnsealButton() {
unsealButton = new Button("Unseal");
unsealButton.setAlignment(Pos.CENTER_RIGHT);
unsealButton.setOnAction(event -> {
unsealButton.setDisable(true);
unseal();
});
unsealButton.managedProperty().bind(unsealButton.visibleProperty());
unsealButton.setVisible(false);
}
private void unlock(Device device) { private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) { if(device.getModel().requiresPinPrompt()) {
promptPin(); promptPin();
@ -816,6 +855,22 @@ public class DevicePane extends TitledDescriptionPane {
getXpubsService.start(); getXpubsService.start();
} }
private void unseal() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<ECKey> unsealService = cardApi.getUnsealService(messageProperty);
handleCardOperation(unsealService, unsealButton, "Unseal", event -> {
EventManager.get().post(new DeviceUnsealedEvent(unsealService.getValue(), cardApi.getDefaultScriptType()));
});
} catch(Exception e) {
log.error("Unseal Error: " + e.getMessage(), e);
setError("Unseal Error", e.getMessage());
unsealButton.setDisable(false);
}
}
}
private void showOperationButton() { private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) { if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) { if(defaultDevice) {
@ -842,6 +897,10 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setDefaultButton(defaultDevice); discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true); discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false); showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.UNSEAL)) {
unsealButton.setDefaultButton(defaultDevice);
unsealButton.setVisible(true);
showHideLink.setVisible(false);
} }
} }
@ -959,6 +1018,6 @@ public class DevicePane extends TitledDescriptionPane {
} }
public enum DeviceOperation { public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES; IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, UNSEAL;
} }
} }

View file

@ -0,0 +1,32 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceUnsealedEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.UnsealedKey> {
public DeviceUnsealDialog(List<String> operationFingerprints) {
super(operationFingerprints);
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(device, defaultDevice);
}
@Subscribe
public void deviceUnsealed(DeviceUnsealedEvent event) {
setResult(new UnsealedKey(event.getPrivateKey(), event.getScriptType()));
}
public record UnsealedKey(ECKey privateKey, ScriptType scriptType) {}
}

View file

@ -14,6 +14,7 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.Wallet; import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5; import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.net.ElectrumServer; import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -42,7 +43,7 @@ import tornadofx.control.Form;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -96,6 +97,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
HBox.setHgrow(key, Priority.ALWAYS); HBox.setHgrow(key, Priority.ALWAYS);
keyField.getInputs().add(keyBox); keyField.getInputs().add(keyBox);
if(CardApi.isReaderAvailable()) {
VBox cardButtonBox = new VBox(5);
Button cardKey = new Button("", getGlyph(FontAwesome5.Glyph.WIFI));
cardKey.setOnAction(event -> unsealPrivateKey());
cardButtonBox.getChildren().add(cardKey);
keyBox.getChildren().add(cardButtonBox);
}
Field keyScriptTypeField = new Field(); Field keyScriptTypeField = new Field();
keyScriptTypeField.setText("Script Type:"); keyScriptTypeField.setText("Script Type:");
keyScriptType = new ComboBox<>(); keyScriptType = new ComboBox<>();
@ -279,6 +288,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
} }
} }
private void unsealPrivateKey() {
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
Optional<DeviceUnsealDialog.UnsealedKey> optPrivateKey = deviceUnsealDialog.showAndWait();
if(optPrivateKey.isPresent()) {
DeviceUnsealDialog.UnsealedKey unsealedKey = optPrivateKey.get();
key.setText(unsealedKey.privateKey().getPrivateKeyEncoded().toBase58());
keyScriptType.setValue(unsealedKey.scriptType());
}
}
private void createTransaction() { private void createTransaction() {
try { try {
DumpedPrivateKey privateKey = getPrivateKey(); DumpedPrivateKey privateKey = getPrivateKey();

View file

@ -0,0 +1,22 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
public class DeviceUnsealedEvent {
private final ECKey privateKey;
private final ScriptType scriptType;
public DeviceUnsealedEvent(ECKey privateKey, ScriptType scriptType) {
this.privateKey = privateKey;
this.scriptType = scriptType;
}
public ECKey getPrivateKey() {
return privateKey;
}
public ScriptType getScriptType() {
return scriptType;
}
}

View file

@ -1,6 +1,8 @@
package com.sparrowwallet.sparrow.io; package com.sparrowwallet.sparrow.io;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.Keystore;
@ -13,7 +15,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException; import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.TerminalFactory; import javax.smartcardio.TerminalFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -45,6 +50,8 @@ public abstract class CardApi {
public abstract WalletModel getCardType() throws CardException; public abstract WalletModel getCardType() throws CardException;
public abstract ScriptType getDefaultScriptType();
public abstract Service<Void> getAuthDelayService() throws CardException; public abstract Service<Void> getAuthDelayService() throws CardException;
public abstract boolean requiresBackup() throws CardException; public abstract boolean requiresBackup() throws CardException;
@ -63,6 +70,8 @@ public abstract class CardApi {
public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty); public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<ECKey> getUnsealService(StringProperty messageProperty);
public abstract void disconnect(); public abstract void disconnect();
public static boolean isReaderAvailable() { public static boolean isReaderAvailable() {
@ -70,9 +79,53 @@ public abstract class CardApi {
TerminalFactory tf = TerminalFactory.getDefault(); TerminalFactory tf = TerminalFactory.getDefault();
return !tf.terminals().list().isEmpty(); return !tf.terminals().list().isEmpty();
} catch(Exception e) { } catch(Exception e) {
log.error("Error detecting card terminals", e); Throwable cause = Throwables.getRootCause(e);
if(cause.getMessage().equals("SCARD_E_NO_SERVICE")) {
recoverNoService();
} else {
log.error("Error detecting card terminals", e);
}
} }
return false; return false;
} }
private static void recoverNoService() {
try {
Class<?> pcscterminal = Class.forName("sun.security.smartcardio.PCSCTerminals");
Field contextId = pcscterminal.getDeclaredField("contextId");
contextId.setAccessible(true);
if(contextId.getLong(pcscterminal) != 0L)
{
// First get a new context value
Class<?> pcsc = Class.forName("sun.security.smartcardio.PCSC");
Method SCardEstablishContext = pcsc.getDeclaredMethod(
"SCardEstablishContext",
Integer.TYPE);
SCardEstablishContext.setAccessible(true);
Field SCARD_SCOPE_USER = pcsc.getDeclaredField("SCARD_SCOPE_USER");
SCARD_SCOPE_USER.setAccessible(true);
long newId = ((Long)SCardEstablishContext.invoke(pcsc,
new Object[] { SCARD_SCOPE_USER.getInt(pcsc) }
));
contextId.setLong(pcscterminal, newId);
// Then clear the terminals in cache
TerminalFactory factory = TerminalFactory.getDefault();
CardTerminals terminals = factory.terminals();
Field fieldTerminals = pcscterminal.getDeclaredField("terminals");
fieldTerminals.setAccessible(true);
Class<?> classMap = Class.forName("java.util.Map");
Method clearMap = classMap.getDeclaredMethod("clear");
clearMap.invoke(fieldTerminals.get(terminals));
}
} catch(Exception e) {
log.error("Failed to recover card service", e);
}
}
} }

View file

@ -188,6 +188,16 @@ public class CardProtocol {
return gson.fromJson(backup, CardBackup.class); return gson.fromJson(backup, CardBackup.class);
} }
public CardUnseal unseal(String cvc) throws CardException {
CardStatus status = getStatus();
Map<String, Object> args = new HashMap<>();
args.put("slot", status.getSlot());
JsonObject unseal = sendAuth("unseal", args, cvc);
return gson.fromJson(unseal, CardUnseal.class);
}
public void disconnect() throws CardException { public void disconnect() throws CardException {
cardTransport.disconnect(); cardTransport.disconnect();
} }
@ -206,11 +216,18 @@ public class CardProtocol {
} }
private JsonObject sendAuth(String cmd, Map<String, Object> args, String cvc) throws CardException { private JsonObject sendAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
addAuth(cmd, args, cvc); byte[] sessionKey = addAuth(cmd, args, cvc);
return send(cmd, args); JsonObject jsonObject = send(cmd, args);
if(jsonObject.get("privkey") != null) {
byte[] privKeyBytes = Utils.hexToBytes(jsonObject.get("privkey").getAsString());
jsonObject.add("privkey", new JsonPrimitive(Utils.bytesToHex(Utils.xor(sessionKey, privKeyBytes))));
}
return jsonObject;
} }
public void addAuth(String cmd, Map<String, Object> args, String cvc) throws CardException { public byte[] addAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
if(cvc.length() < 6 || cvc.length() > 32) { if(cvc.length() < 6 || cvc.length() > 32) {
throw new IllegalArgumentException("CVC cannot be of length " + cvc.length()); throw new IllegalArgumentException("CVC cannot be of length " + cvc.length());
} }
@ -235,6 +252,10 @@ public class CardProtocol {
} else if(cmd.equals("change") && args.get("data") instanceof byte[] dataBytes) { } else if(cmd.equals("change") && args.get("data") instanceof byte[] dataBytes) {
args.put("data", Utils.xor(dataBytes, Arrays.copyOf(sessionKey, dataBytes.length))); args.put("data", Utils.xor(dataBytes, Arrays.copyOf(sessionKey, dataBytes.length)));
} }
return sessionKey;
} else {
throw new IllegalStateException("Native library libsecp256k1 required but not enabled");
} }
} catch(NativeSecp256k1Util.AssertFailException e) { } catch(NativeSecp256k1Util.AssertFailException e) {
throw new RuntimeException(e); throw new RuntimeException(e);

View file

@ -17,12 +17,14 @@ public class CardStatus extends CardResponse {
boolean tapsigner; boolean tapsigner;
List<BigInteger> path; List<BigInteger> path;
BigInteger num_backups; BigInteger num_backups;
List<BigInteger> slots;
String addr;
byte[] pubkey; byte[] pubkey;
BigInteger auth_delay; BigInteger auth_delay;
boolean testnet; boolean testnet;
public boolean isInitialized() { public boolean isInitialized() {
return path != null; return getCardType() != WalletModel.TAPSIGNER || path != null;
} }
public String getIdentifier() { public String getIdentifier() {
@ -47,6 +49,14 @@ public class CardStatus extends CardResponse {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD; return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
} }
public int getSlot() {
if(slots == null || slots.isEmpty()) {
return 0;
}
return slots.get(0).intValue();
}
@Override @Override
public String toString() { public String toString() {
return "CardStatus{" + return "CardStatus{" +
@ -56,6 +66,8 @@ public class CardStatus extends CardResponse {
", tapsigner=" + tapsigner + ", tapsigner=" + tapsigner +
", path=" + path + ", path=" + path +
", num_backups=" + num_backups + ", num_backups=" + num_backups +
", slots=" + slots +
", addr='" + addr + '\'' +
", pubkey=" + Arrays.toString(pubkey) + ", pubkey=" + Arrays.toString(pubkey) +
", auth_delay=" + auth_delay + ", auth_delay=" + auth_delay +
", testnet=" + testnet + ", testnet=" + testnet +

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.crypto.ECKey;
public class CardUnseal extends CardResponse {
int slot;
byte[] privkey;
byte[] pubkey;
byte[] master_pk;
byte[] chain_code;
public ECKey getPrivateKey() {
return ECKey.fromPrivate(privkey);
}
}

View file

@ -61,6 +61,11 @@ public class CkCardApi extends CardApi {
return cardStatus.getCardType(); return cardStatus.getCardType();
} }
@Override
public ScriptType getDefaultScriptType() {
return ScriptType.P2WPKH;
}
CardStatus getStatus() throws CardException { CardStatus getStatus() throws CardException {
CardStatus cardStatus = cardProtocol.getStatus(); CardStatus cardStatus = cardProtocol.getStatus();
if(cardType != null && cardStatus.getCardType() != cardType) { if(cardType != null && cardStatus.getCardType() != cardType) {
@ -239,6 +244,16 @@ public class CkCardApi extends CardApi {
} }
} }
@Override
public Service<ECKey> getUnsealService(StringProperty messageProperty) {
return new UnsealService(messageProperty);
}
ECKey unseal() throws CardException {
CardUnseal cardUnseal = cardProtocol.unseal(cvc);
return cardUnseal.getPrivateKey();
}
@Override @Override
public void disconnect() { public void disconnect() {
try { try {
@ -302,6 +317,10 @@ public class CkCardApi extends CardApi {
@Override @Override
protected PSBT call() throws Exception { protected PSBT call() throws Exception {
CardStatus cardStatus = getStatus(); CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign transactions.");
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
sign(wallet, psbt); sign(wallet, psbt);
@ -359,6 +378,10 @@ public class CkCardApi extends CardApi {
@Override @Override
protected String call() throws Exception { protected String call() throws Exception {
CardStatus cardStatus = getStatus(); CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign messages.");
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return signMessage(message, scriptType, derivation); return signMessage(message, scriptType, derivation);
@ -366,4 +389,29 @@ public class CkCardApi extends CardApi {
}; };
} }
} }
public class UnsealService extends Service<ECKey> {
private final StringProperty messageProperty;
public UnsealService(StringProperty messageProperty) {
this.messageProperty = messageProperty;
}
@Override
protected Task<ECKey> createTask() {
return new Task<>() {
@Override
protected ECKey call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to unseal private keys.");
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return unseal();
}
};
}
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB