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.javafx.application=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) {
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=java.base/java.net=com.sparrowwallet.sparrow",
"--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.sql",
"--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.OutputDescriptor;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -65,6 +66,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private Button unsealButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty("");
@ -199,6 +201,32 @@ public class DevicePane extends TitledDescriptionPane {
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) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
@ -342,6 +370,17 @@ public class DevicePane extends TitledDescriptionPane {
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) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@ -816,6 +855,22 @@ public class DevicePane extends TitledDescriptionPane {
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() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
@ -842,6 +897,10 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
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 {
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.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.collections.FXCollections;
@ -42,7 +43,7 @@ import tornadofx.control.Form;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -96,6 +97,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
HBox.setHgrow(key, Priority.ALWAYS);
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();
keyScriptTypeField.setText("Script Type:");
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() {
try {
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;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
@ -13,7 +15,10 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import javax.smartcardio.CardTerminals;
import javax.smartcardio.TerminalFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
@ -45,6 +50,8 @@ public abstract class CardApi {
public abstract WalletModel getCardType() throws CardException;
public abstract ScriptType getDefaultScriptType();
public abstract Service<Void> getAuthDelayService() 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<ECKey> getUnsealService(StringProperty messageProperty);
public abstract void disconnect();
public static boolean isReaderAvailable() {
@ -70,9 +79,53 @@ public abstract class CardApi {
TerminalFactory tf = TerminalFactory.getDefault();
return !tf.terminals().list().isEmpty();
} catch(Exception 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;
}
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);
}
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 {
cardTransport.disconnect();
}
@ -206,11 +216,18 @@ public class CardProtocol {
}
private JsonObject sendAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
addAuth(cmd, args, cvc);
return send(cmd, args);
byte[] sessionKey = addAuth(cmd, args, cvc);
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))));
}
public void addAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
return jsonObject;
}
public byte[] addAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
if(cvc.length() < 6 || cvc.length() > 32) {
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) {
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) {
throw new RuntimeException(e);

View file

@ -17,12 +17,14 @@ public class CardStatus extends CardResponse {
boolean tapsigner;
List<BigInteger> path;
BigInteger num_backups;
List<BigInteger> slots;
String addr;
byte[] pubkey;
BigInteger auth_delay;
boolean testnet;
public boolean isInitialized() {
return path != null;
return getCardType() != WalletModel.TAPSIGNER || path != null;
}
public String getIdentifier() {
@ -47,6 +49,14 @@ public class CardStatus extends CardResponse {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
}
public int getSlot() {
if(slots == null || slots.isEmpty()) {
return 0;
}
return slots.get(0).intValue();
}
@Override
public String toString() {
return "CardStatus{" +
@ -56,6 +66,8 @@ public class CardStatus extends CardResponse {
", tapsigner=" + tapsigner +
", path=" + path +
", num_backups=" + num_backups +
", slots=" + slots +
", addr='" + addr + '\'' +
", pubkey=" + Arrays.toString(pubkey) +
", auth_delay=" + auth_delay +
", 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();
}
@Override
public ScriptType getDefaultScriptType() {
return ScriptType.P2WPKH;
}
CardStatus getStatus() throws CardException {
CardStatus cardStatus = cardProtocol.getStatus();
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
public void disconnect() {
try {
@ -302,6 +317,10 @@ public class CkCardApi extends CardApi {
@Override
protected PSBT call() throws Exception {
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);
sign(wallet, psbt);
@ -359,6 +378,10 @@ public class CkCardApi extends CardApi {
@Override
protected String call() throws Exception {
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);
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