mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +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.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",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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))));
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
|
|
@ -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 +
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
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