mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16:44 +00:00
unseal satscard functionality added to sweep private key dialog
This commit is contained in:
parent
300545b289
commit
176e440195
13 changed files with 291 additions and 8 deletions
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 +
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
src/main/resources/image/satscard.png
Normal file
BIN
src/main/resources/image/satscard.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/main/resources/image/satscard@2x.png
Normal file
BIN
src/main/resources/image/satscard@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
BIN
src/main/resources/image/satscard@3x.png
Normal file
BIN
src/main/resources/image/satscard@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
Loading…
Reference in a new issue