mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-24 12:46:45 +00:00
initialize and import tapsigner as keystore
This commit is contained in:
parent
7c64d689fd
commit
6b59ff60ad
37 changed files with 1050 additions and 24 deletions
|
@ -493,6 +493,8 @@ extraJavaModuleInfo {
|
|||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.34.jar', 'com.sparrowwallet.nightjar', '0.2.34') {
|
||||
requires('com.google.common')
|
||||
|
|
2
drongo
2
drongo
|
@ -1 +1 @@
|
|||
Subproject commit c642f7414a35c9ad69439687ba8724b76d9a6fb1
|
||||
Subproject commit 2168c56de924dd7bff5458b32f9a60a6c79066a1
|
|
@ -0,0 +1,148 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.CardAuthorizationException;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty errorText = new SimpleStringProperty("");
|
||||
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.importer = importer;
|
||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
importButton = new Button("Tap");
|
||||
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||
tapGlyph.setFontSize(12);
|
||||
importButton.setGraphic(tapGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importCard();
|
||||
});
|
||||
return importButton;
|
||||
}
|
||||
|
||||
private void importCard() {
|
||||
if(pin.get().isEmpty()) {
|
||||
setDescription("Enter PIN code");
|
||||
setContent(getPinEntry());
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation);
|
||||
cardImportService.messageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setDescription(newValue);
|
||||
});
|
||||
cardImportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||
});
|
||||
cardImportService.setOnFailed(event -> {
|
||||
log.error("Error importing keystore from card", event.getSource().getException());
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError("Import Error", "Incorrect PIN code, try again:");
|
||||
} else {
|
||||
setError("Import Error", rootCause.getMessage());
|
||||
}
|
||||
});
|
||||
cardImportService.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setError(String title, String detail) {
|
||||
super.setError(title, null);
|
||||
errorText.set(detail);
|
||||
setContent(getPinEntry());
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
private Node getPinEntry() {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
if(!errorText.get().isEmpty()) {
|
||||
Node contextBox = getContentBox(errorText.get());
|
||||
if(contextBox instanceof HBox hBox && hBox.getPrefHeight() == 60) {
|
||||
hBox.setPrefHeight(50);
|
||||
}
|
||||
vBox.getChildren().add(contextBox);
|
||||
}
|
||||
|
||||
CustomPasswordField pinField = new ViewPasswordField();
|
||||
pinField.setPromptText("PIN Code");
|
||||
pinField.setText(pin.get());
|
||||
importButton.setDefaultButton(true);
|
||||
pin.bind(pinField.textProperty());
|
||||
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(errorText.get().isEmpty() ? 10 : 0, 30, 10, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public static class CardImportService extends Service<Keystore> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
private final List<ChildNumber> derivation;
|
||||
|
||||
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation) {
|
||||
this.cardImport = cardImport;
|
||||
this.pin = pin;
|
||||
this.derivation = derivation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Keystore> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Keystore call() throws Exception {
|
||||
cardImport.messageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
updateMessage(newValue);
|
||||
});
|
||||
return cardImport.getKeystore(pin, derivation);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,17 +34,4 @@ public class FileKeystoreImportPane extends FileImportPane {
|
|||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
|
@ -159,4 +161,17 @@ public class TitledDescriptionPane extends TitledPane {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,18 @@ public class WalletIcon extends StackPane {
|
|||
|
||||
private final Storage storage;
|
||||
private final ObjectProperty<Wallet> walletProperty = new SimpleObjectProperty<>();
|
||||
private final Keystore keystore;
|
||||
private final Glyph fallbackIcon;
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet) {
|
||||
this(storage, wallet, null, null);
|
||||
}
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet, Keystore keystore, Glyph fallbackIcon) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.keystore = keystore;
|
||||
this.fallbackIcon = fallbackIcon;
|
||||
setPrefSize(WIDTH, HEIGHT);
|
||||
walletProperty.addListener((observable, oldValue, newValue) -> {
|
||||
refresh();
|
||||
|
@ -58,8 +66,8 @@ public class WalletIcon extends StackPane {
|
|||
} else {
|
||||
Platform.runLater(() -> addWalletIcon(walletId));
|
||||
}
|
||||
} else if(wallet.getKeystores().size() == 1) {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
} else if(this.keystore != null || wallet.getKeystores().size() == 1) {
|
||||
Keystore keystore = this.keystore == null ? wallet.getKeystores().get(0) : this.keystore;
|
||||
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getSource() == KeystoreSource.HW_AIRGAPPED) {
|
||||
WalletModel walletModel = keystore.getWalletModel();
|
||||
|
||||
|
@ -96,7 +104,10 @@ public class WalletIcon extends StackPane {
|
|||
}
|
||||
|
||||
if(getChildren().isEmpty()) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
Glyph glyph = fallbackIcon;
|
||||
if(glyph == null) {
|
||||
glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
}
|
||||
glyph.setFontSize(10.0);
|
||||
getChildren().add(glyph);
|
||||
}
|
||||
|
|
|
@ -82,7 +82,8 @@ public class FontAwesome5 extends GlyphFont {
|
|||
USER_PLUS('\uf234'),
|
||||
USER_SLASH('\uf506'),
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
WEIGHT('\uf496'),
|
||||
WIFI('\uf1eb');
|
||||
|
||||
private final char ch;
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
public interface CardImport extends ImportExport {
|
||||
StringProperty messageProperty();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreCardImport extends CardImport {
|
||||
Keystore getKeystore(String pin, List<ChildNumber> derivation) throws ImportException;
|
||||
String getKeystoreImportDescription(int account);
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Base58;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.util.List;
|
||||
|
||||
public class CardApi {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardApi.class);
|
||||
|
||||
private final CardProtocol cardProtocol;
|
||||
private String cvc;
|
||||
|
||||
public CardApi(String cvc) throws CardException {
|
||||
this.cardProtocol = new CardProtocol();
|
||||
this.cvc = cvc;
|
||||
}
|
||||
|
||||
public void initialize() throws CardException {
|
||||
cardProtocol.verify();
|
||||
cardProtocol.setup(cvc, null);
|
||||
}
|
||||
|
||||
CardStatus getStatus() throws CardException {
|
||||
return cardProtocol.getStatus();
|
||||
}
|
||||
|
||||
void checkWait(CardStatus cardStatus, StringProperty messageProperty) throws CardException {
|
||||
if(cardStatus.auth_delay != null) {
|
||||
int delay = cardStatus.auth_delay.intValue();
|
||||
while(delay > 0) {
|
||||
messageProperty.set("Auth delay, waiting " + delay + "s...");
|
||||
CardWait cardWait = cardProtocol.authWait();
|
||||
if(cardWait.success) {
|
||||
delay = cardWait.auth_delay == null ? 0 : cardWait.auth_delay.intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setDerivation(List<ChildNumber> derivation) throws CardException {
|
||||
cardProtocol.derive(cvc, derivation);
|
||||
}
|
||||
|
||||
public Keystore getKeystore() throws CardException {
|
||||
CardStatus cardStatus = cardProtocol.getStatus();
|
||||
|
||||
CardXpub masterXpub = cardProtocol.xpub(cvc, true);
|
||||
ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub));
|
||||
String masterFingerprint = Utils.bytesToHex(masterXpubkey.getKey().getFingerprint());
|
||||
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, cardStatus.getDerivation());
|
||||
|
||||
CardXpub derivedXpub = cardProtocol.xpub(cvc, false);
|
||||
ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub));
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.TAPSIGNER.toDisplayString());
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setExtendedPublicKey(derivedXpubkey);
|
||||
keystore.setWalletModel(WalletModel.TAPSIGNER);
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
try {
|
||||
cardProtocol.disconnect();
|
||||
} catch(CardException e) {
|
||||
log.warn("Error disconnecting from card reader", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardAuthorizationException extends CardException {
|
||||
public CardAuthorizationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardAuthorizationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardAuthorizationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardBackup extends CardResponse {
|
||||
byte[] data;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CardCerts {
|
||||
List<byte[]> cert_chain;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardChange extends CardResponse {
|
||||
boolean success;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardDerive extends CardResponse {
|
||||
byte[] sig;
|
||||
byte[] chain_code;
|
||||
byte[] master_pubkey;
|
||||
byte[] pubkey;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,266 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import org.bitcoin.NativeSecp256k1;
|
||||
import org.bitcoin.NativeSecp256k1Util;
|
||||
import org.bitcoin.Secp256k1Context;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CardProtocol {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardProtocol.class);
|
||||
|
||||
public static final byte[] OPENDIME_HEADER = "OPENDIME".getBytes(StandardCharsets.UTF_8);
|
||||
public static final List<byte[]> FACTORY_ROOT_KEYS = List.of(Utils.hexToBytes("03028a0e89e70d0ec0d932053a89ab1da7d9182bdc6d2f03e706ee99517d05d9e1"));
|
||||
public static final int MAX_PATH_DEPTH = 8;
|
||||
|
||||
private final CardTransport cardTransport;
|
||||
private final Gson gson;
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
private byte[] cardPubkey;
|
||||
private byte[] lastCardNonce;
|
||||
|
||||
public CardProtocol() throws CardException {
|
||||
this.cardTransport = new CardTransport();
|
||||
this.gson = new GsonBuilder().registerTypeAdapter(byte[].class, new ByteArrayToHexTypeAdapter()).create();
|
||||
}
|
||||
|
||||
public CardStatus getStatus() throws CardException {
|
||||
JsonObject status = send("status");
|
||||
CardStatus cardStatus = gson.fromJson(status, CardStatus.class);
|
||||
if(cardPubkey == null) {
|
||||
cardPubkey = cardStatus.pubkey;
|
||||
}
|
||||
|
||||
return cardStatus;
|
||||
}
|
||||
|
||||
public CardCerts getCerts() throws CardException {
|
||||
JsonObject certs = send("certs");
|
||||
return gson.fromJson(certs, CardCerts.class);
|
||||
}
|
||||
|
||||
public void verify() throws CardException {
|
||||
CardCerts cardCerts = getCerts();
|
||||
|
||||
byte[] userNonce = getNonce();
|
||||
byte[] cardNonce = lastCardNonce;
|
||||
JsonObject certs = send("check", Map.of("nonce", userNonce));
|
||||
CardSignature cardSignature = gson.fromJson(certs, CardSignature.class);
|
||||
Sha256Hash verificationData = getVerificationData(cardNonce, userNonce, new byte[0]);
|
||||
ECDSASignature ecdsaSignature = cardSignature.getSignature();
|
||||
if(!ecdsaSignature.verify(verificationData.getBytes(), cardPubkey)) {
|
||||
throw new CardException("Card authentication failure: Provided signature did not match public key");
|
||||
}
|
||||
|
||||
byte[] pubkey = cardPubkey;
|
||||
for(byte[] cert : cardCerts.cert_chain) {
|
||||
Sha256Hash pubkeyHash = Sha256Hash.of(pubkey);
|
||||
|
||||
try {
|
||||
ECKey recoveredKey = ECKey.signedHashToKey(pubkeyHash, cert, false);
|
||||
pubkey = recoveredKey.getPubKey();
|
||||
} catch(SignatureException e) {
|
||||
throw new CardException("Card signature error", e);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] rootPubKey = pubkey;
|
||||
if(FACTORY_ROOT_KEYS.stream().noneMatch(key -> Arrays.equals(key, rootPubKey))) {
|
||||
throw new CardException("Card authentication failure: Could not verify to root certificate");
|
||||
}
|
||||
}
|
||||
|
||||
public CardRead read(String cvc) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("nonce", getNonce());
|
||||
|
||||
JsonObject read = sendAuth("read", args, cvc);
|
||||
return gson.fromJson(read, CardRead.class);
|
||||
}
|
||||
|
||||
public CardSetup setup(String cvc, byte[] entropy) throws CardException {
|
||||
if(entropy == null) {
|
||||
entropy = Sha256Hash.hashTwice(secureRandom.generateSeed(128));
|
||||
}
|
||||
|
||||
if(entropy.length != 32) {
|
||||
throw new IllegalArgumentException("Invalid entropy length of " + entropy.length);
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("chain_code", entropy);
|
||||
JsonObject setup = sendAuth("new", args, cvc);
|
||||
return gson.fromJson(setup, CardSetup.class);
|
||||
}
|
||||
|
||||
public CardWait authWait() throws CardException {
|
||||
JsonObject wait = send("wait");
|
||||
return gson.fromJson(wait, CardWait.class);
|
||||
}
|
||||
|
||||
public CardXpub xpub(String cvc, boolean master) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("master", master);
|
||||
|
||||
JsonObject xpub = sendAuth("xpub", args, cvc);
|
||||
return gson.fromJson(xpub, CardXpub.class);
|
||||
}
|
||||
|
||||
public CardDerive derive(String cvc, List<ChildNumber> path) throws CardException {
|
||||
if(path.stream().anyMatch(childNumber -> !childNumber.isHardened())) {
|
||||
throw new IllegalArgumentException("Derivation path cannot contain unhardened components");
|
||||
}
|
||||
|
||||
if(path.size() > MAX_PATH_DEPTH) {
|
||||
throw new IllegalArgumentException("Derivation path cannot have more than " + MAX_PATH_DEPTH + " components");
|
||||
}
|
||||
|
||||
if(lastCardNonce == null || cardPubkey == null) {
|
||||
getStatus();
|
||||
}
|
||||
|
||||
byte[] userNonce = getNonce();
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("path", path.stream().map(cn -> Integer.toUnsignedLong(cn.i())).collect(Collectors.toList()));
|
||||
args.put("nonce", userNonce);
|
||||
|
||||
JsonObject derive = sendAuth("derive", args, cvc);
|
||||
return gson.fromJson(derive, CardDerive.class);
|
||||
}
|
||||
|
||||
public CardSign sign(String cvc, List<ChildNumber> subpath, Sha256Hash digest) throws CardException {
|
||||
if(subpath.stream().anyMatch(ChildNumber::isHardened)) {
|
||||
throw new IllegalArgumentException("Derivation path cannot contain hardened components");
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("subpath", subpath.stream().map(cn -> Integer.toUnsignedLong(cn.i())).collect(Collectors.toList()));
|
||||
args.put("digest", digest.getBytes());
|
||||
|
||||
for(int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
JsonObject sign = sendAuth("sign", args, cvc);
|
||||
CardSign cardSign = gson.fromJson(sign, CardSign.class);
|
||||
if(!cardSign.getSignature().verify(digest.getBytes(), cardSign.pubkey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return cardSign;
|
||||
} catch(CardUnluckyNumberException e) {
|
||||
log.debug("Got unlucky number signing, trying again...");
|
||||
}
|
||||
}
|
||||
|
||||
throw new CardSignFailedException("Failed to sign digest after 5 tries.");
|
||||
}
|
||||
|
||||
public CardChange change(String currentCvc, String newCvc) throws CardException {
|
||||
if(newCvc.length() < 6 || newCvc.length() > 32) {
|
||||
throw new IllegalArgumentException("CVC cannot be of length " + newCvc.length());
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("data", newCvc.getBytes(StandardCharsets.UTF_8));
|
||||
JsonObject change = sendAuth("change", args, currentCvc);
|
||||
return gson.fromJson(change, CardChange.class);
|
||||
}
|
||||
|
||||
public CardBackup backup(String cvc) throws CardException {
|
||||
JsonObject backup = sendAuth("backup", new HashMap<>(), cvc);
|
||||
return gson.fromJson(backup, CardBackup.class);
|
||||
}
|
||||
|
||||
public void disconnect() throws CardException {
|
||||
cardTransport.disconnect();
|
||||
}
|
||||
|
||||
private JsonObject send(String cmd) throws CardException {
|
||||
return send(cmd, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
JsonObject jsonObject = cardTransport.send(cmd, args);
|
||||
if(jsonObject.get("card_nonce") != null) {
|
||||
lastCardNonce = gson.fromJson(jsonObject, CardResponse.class).card_nonce;
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
private JsonObject sendAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
|
||||
addAuth(cmd, args, cvc);
|
||||
return send(cmd, args);
|
||||
}
|
||||
|
||||
public void 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());
|
||||
}
|
||||
|
||||
if(lastCardNonce == null || cardPubkey == null) {
|
||||
getStatus();
|
||||
}
|
||||
|
||||
try {
|
||||
ECKey ephemeralKey = new ECKey(secureRandom);
|
||||
if(Secp256k1Context.isEnabled()) {
|
||||
byte[] sessionKey = NativeSecp256k1.createECDHSecret(ephemeralKey.getPrivKeyBytes(), cardPubkey);
|
||||
byte[] md = Sha256Hash.hash(Utils.concat(lastCardNonce, cmd.getBytes(StandardCharsets.UTF_8)));
|
||||
byte[] mask = Arrays.copyOf(Utils.xor(sessionKey, md), cvc.length());
|
||||
byte[] xcvc = Utils.xor(cvc.getBytes(StandardCharsets.UTF_8), mask);
|
||||
|
||||
args.put("epubkey", ephemeralKey.getPubKey());
|
||||
args.put("xcvc", xcvc);
|
||||
|
||||
if(cmd.equals("sign") && args.get("digest") instanceof byte[] digestBytes) {
|
||||
args.put("digest", Utils.xor(digestBytes, sessionKey));
|
||||
} else if(cmd.equals("change") && args.get("data") instanceof byte[] dataBytes) {
|
||||
args.put("data", Utils.xor(dataBytes, Arrays.copyOf(sessionKey, dataBytes.length)));
|
||||
}
|
||||
}
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Sha256Hash getVerificationData(byte[] cardNonce, byte[] userNonce, byte[] commandData) {
|
||||
byte[] data = new byte[40 + commandData.length];
|
||||
System.arraycopy(OPENDIME_HEADER, 0, data, 0, 8);
|
||||
System.arraycopy(cardNonce, 0, data, 8, 16);
|
||||
System.arraycopy(userNonce, 0, data, 24, 16);
|
||||
System.arraycopy(commandData, 0, data, 40, commandData.length);
|
||||
return Sha256Hash.of(data);
|
||||
}
|
||||
|
||||
private byte[] getNonce() {
|
||||
byte[] nonce = new byte[16];
|
||||
secureRandom.nextBytes(nonce);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private static class ByteArrayToHexTypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Utils.hexToBytes(json.getAsString());
|
||||
}
|
||||
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(src));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardRead {
|
||||
byte[] sig;
|
||||
byte[] pubkey;
|
||||
byte[] card_nonce;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardResponse {
|
||||
byte[] card_nonce;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardSetup extends CardResponse {
|
||||
int slot;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardSign extends CardResponse {
|
||||
int slot;
|
||||
byte[] sig;
|
||||
byte[] pubkey;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
if(sig != null) {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardSignFailedException extends CardException {
|
||||
public CardSignFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardSignFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardSignFailedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardSignature extends CardResponse {
|
||||
byte[] auth_sig;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(auth_sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(auth_sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CardStatus extends CardResponse {
|
||||
int proto;
|
||||
String ver;
|
||||
BigInteger birth;
|
||||
Boolean tapsigner;
|
||||
List<BigInteger> path;
|
||||
BigInteger num_backups;
|
||||
byte[] pubkey;
|
||||
BigInteger auth_delay;
|
||||
boolean testnet;
|
||||
|
||||
public boolean isInitialized() {
|
||||
return path != null;
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
byte[] pubkeyHash = Sha256Hash.hash(pubkey);
|
||||
String base32 = BaseEncoding.base32().encode(Arrays.copyOfRange(pubkeyHash, 8, 32));
|
||||
return base32.replaceAll("(.{5})", "$1-");
|
||||
}
|
||||
|
||||
public List<ChildNumber> getDerivation() {
|
||||
if(isInitialized()) {
|
||||
return path.stream().map(i -> new ChildNumber(i.intValue())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CardStatus{" +
|
||||
"proto=" + proto +
|
||||
", ver='" + ver + '\'' +
|
||||
", birth=" + birth +
|
||||
", tapsigner=" + tapsigner +
|
||||
", path=" + path +
|
||||
", num_backups=" + num_backups +
|
||||
", pubkey=" + Arrays.toString(pubkey) +
|
||||
", auth_delay=" + auth_delay +
|
||||
", testnet=" + testnet +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import co.nstant.in.cbor.CborBuilder;
|
||||
import co.nstant.in.cbor.CborDecoder;
|
||||
import co.nstant.in.cbor.CborEncoder;
|
||||
import co.nstant.in.cbor.CborException;
|
||||
import co.nstant.in.cbor.builder.ArrayBuilder;
|
||||
import co.nstant.in.cbor.builder.MapBuilder;
|
||||
import co.nstant.in.cbor.model.*;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CardTransport {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardTransport.class);
|
||||
|
||||
public static final String APPID = "f0436f696e6b697465434152447631";
|
||||
private static final int CBOR_CLA = 0x00;
|
||||
private static final int CBOR_INS = 0xCB;
|
||||
private static final int SW_OKAY = 0x9000;
|
||||
|
||||
private final Card connection;
|
||||
|
||||
public CardTransport() throws CardException {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
List<CardTerminal> terminals = tf.terminals().list();
|
||||
if(terminals.isEmpty()) {
|
||||
throw new IllegalStateException("No reader connected");
|
||||
}
|
||||
|
||||
CardTerminal cardTerminal = (CardTerminal)terminals.get(0);
|
||||
connection = cardTerminal.connect("*");
|
||||
|
||||
CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
|
||||
if(resp.getSW() != SW_OKAY) {
|
||||
throw new CardException("Card returned response of " + resp.getSW());
|
||||
}
|
||||
}
|
||||
|
||||
public JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
Map<String, Object> sendMap = new LinkedHashMap<>();
|
||||
sendMap.put("cmd", cmd);
|
||||
sendMap.putAll(args);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
MapBuilder<CborBuilder> cborBuilder = new CborBuilder().addMap();
|
||||
for(Map.Entry<String, Object> entry : sendMap.entrySet()) {
|
||||
if(entry.getValue() instanceof String strValue) {
|
||||
cborBuilder.put(entry.getKey(), strValue);
|
||||
} else if(entry.getValue() instanceof byte[] byteValue) {
|
||||
cborBuilder.put(entry.getKey(), byteValue);
|
||||
} else if(entry.getValue() instanceof Long longValue) {
|
||||
cborBuilder.put(entry.getKey(), longValue);
|
||||
} else if(entry.getValue() instanceof Boolean booleanValue) {
|
||||
cborBuilder.put(entry.getKey(), booleanValue);
|
||||
} else if(entry.getValue() instanceof List<?> listValue) {
|
||||
ArrayBuilder<MapBuilder<CborBuilder>> arrayBuilder = cborBuilder.putArray(entry.getKey());
|
||||
for(Object value : listValue) {
|
||||
if(value instanceof String strValue) {
|
||||
arrayBuilder.add(strValue);
|
||||
} else if(value instanceof byte[] byteValue) {
|
||||
arrayBuilder.add(byteValue);
|
||||
} else if(value instanceof Long longValue) {
|
||||
arrayBuilder.add(longValue);
|
||||
} else if(value instanceof Boolean booleanValue) {
|
||||
arrayBuilder.add(booleanValue);
|
||||
}
|
||||
}
|
||||
arrayBuilder.end();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
new CborEncoder(baos).encode(cborBuilder.end().build());
|
||||
byte[] sendBytes = baos.toByteArray();
|
||||
|
||||
CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(CBOR_CLA, CBOR_INS, 0, 0, sendBytes));
|
||||
|
||||
if(resp.getSW() != SW_OKAY) {
|
||||
throw new CardException("Received error SW value " + resp.getSW());
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(resp.getData());
|
||||
List<DataItem> dataItems = new CborDecoder(bais).decode();
|
||||
for(DataItem dataItem : dataItems) {
|
||||
if(dataItem instanceof co.nstant.in.cbor.model.Map map) {
|
||||
JsonObject result = new JsonObject();
|
||||
for(DataItem key : map.getKeys()) {
|
||||
String strKey = key.toString();
|
||||
result.add(strKey, getJsonElement(map.get(key)));
|
||||
}
|
||||
|
||||
if(result.get("error") != null) {
|
||||
String msg = result.get("error").getAsString();
|
||||
int code = result.get("code") == null ? 500 : result.get("code").getAsInt();
|
||||
String message = code + " on " + cmd + ": " + msg;
|
||||
if(code == 205) {
|
||||
throw new CardUnluckyNumberException(message);
|
||||
} else if(code == 401) {
|
||||
throw new CardAuthorizationException(message);
|
||||
}
|
||||
|
||||
throw new CardException(code + " on " + cmd + ": " + msg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch(CborException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return new JsonObject();
|
||||
}
|
||||
|
||||
private JsonElement getJsonElement(DataItem dataItem) {
|
||||
if(dataItem instanceof UnicodeString strValue) {
|
||||
return new JsonPrimitive(strValue.toString());
|
||||
} else if(dataItem instanceof ByteString byteString) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(byteString.getBytes()));
|
||||
} else if(dataItem instanceof UnsignedInteger unsignedInteger) {
|
||||
return new JsonPrimitive(unsignedInteger.getValue());
|
||||
} else if(dataItem instanceof SimpleValue simpleValue) {
|
||||
return new JsonPrimitive(simpleValue.getValue() == SimpleValueType.TRUE.getValue());
|
||||
} else if(dataItem instanceof Array array) {
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for(DataItem item : array.getDataItems()) {
|
||||
jsonArray.add(getJsonElement(item));
|
||||
}
|
||||
return jsonArray;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot convert dataItem of type " + dataItem.getClass() + "to JsonElement");
|
||||
}
|
||||
|
||||
public void disconnect() throws CardException {
|
||||
connection.disconnect(true);
|
||||
}
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
} catch(Exception e) {
|
||||
log.error("Error detecting card terminals", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardUnluckyNumberException extends CardException {
|
||||
public CardUnluckyNumberException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardUnluckyNumberException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardUnluckyNumberException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CardWait {
|
||||
boolean success;
|
||||
BigInteger auth_delay;
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardXpub {
|
||||
byte[] xpub;
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.util.List;
|
||||
|
||||
public class CkCard implements KeystoreCardImport {
|
||||
private final StringProperty messageProperty = new SimpleStringProperty("");
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(String pin, List<ChildNumber> derivation) throws ImportException {
|
||||
if(pin.length() < 6) {
|
||||
throw new ImportException("PIN too short");
|
||||
}
|
||||
|
||||
if(pin.length() > 32) {
|
||||
throw new ImportException("PIN too long");
|
||||
}
|
||||
|
||||
CardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CardApi(pin);
|
||||
CardStatus cardStatus = cardApi.getStatus();
|
||||
if(!cardStatus.isInitialized()) {
|
||||
cardApi.initialize();
|
||||
cardStatus = cardApi.getStatus();
|
||||
}
|
||||
cardApi.checkWait(cardStatus, messageProperty);
|
||||
|
||||
if(!derivation.equals(cardStatus.getDerivation())) {
|
||||
cardApi.setDerivation(derivation);
|
||||
}
|
||||
return cardApi.getKeystore();
|
||||
} catch(CardException e) {
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the keystore from your Tapsigner by placing it on the card reader.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Tapsigner";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.TAPSIGNER;
|
||||
}
|
||||
|
||||
public StringProperty messageProperty() {
|
||||
return messageProperty;
|
||||
}
|
||||
}
|
|
@ -1,29 +1,36 @@
|
|||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.sparrow.control.CardImportPane;
|
||||
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.CkCard;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Accordion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.TerminalFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(HwAirgappedController.class);
|
||||
|
||||
@FXML
|
||||
private Accordion importAccordion;
|
||||
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||
List<KeystoreFileImport> fileImporters = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
importers = 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)) {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
fileImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
for(KeystoreFileImport importer : fileImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
|
@ -32,6 +39,30 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
|||
}
|
||||
}
|
||||
|
||||
List<KeystoreCardImport> cardImporters = Collections.emptyList();
|
||||
if(isReaderAvailable()) {
|
||||
cardImporters = List.of(new CkCard());
|
||||
}
|
||||
for(KeystoreCardImport importer : cardImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
}
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
} catch(Exception e) {
|
||||
log.error("Error detecting card terminals", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import javafx.application.Platform;
|
|||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
@ -306,7 +307,11 @@ public class KeystoreController extends WalletFormController implements Initiali
|
|||
}
|
||||
}
|
||||
|
||||
private Glyph getTypeIcon(Keystore keystore) {
|
||||
private Node getTypeIcon(Keystore keystore) {
|
||||
return new WalletIcon(getWalletForm().getStorage(), getWalletForm().getWallet(), keystore, getDefaultTypeIcon(keystore));
|
||||
}
|
||||
|
||||
private Glyph getDefaultTypeIcon(Keystore keystore) {
|
||||
switch (keystore.getSource()) {
|
||||
case HW_USB:
|
||||
return new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||
|
|
|
@ -49,4 +49,5 @@ open module com.sparrowwallet.sparrow {
|
|||
requires net.coobird.thumbnailator;
|
||||
requires com.github.hervegirod;
|
||||
requires com.sparrowwallet.toucan;
|
||||
requires java.smartcardio;
|
||||
}
|
|
@ -23,7 +23,7 @@
|
|||
<Form fx:id="keystoreForm" GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="">
|
||||
<Field text="Type:">
|
||||
<Label fx:id="type" graphicTextGap="8"/>
|
||||
<Label fx:id="type" graphicTextGap="5"/>
|
||||
<Button fx:id="viewSeedButton" text="View Seed..." graphicTextGap="5" onAction="#showPrivate">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="KEY" />
|
||||
|
|
8
src/main/resources/image/tapsigner-icon-invert.svg
Normal file
8
src/main/resources/image/tapsigner-icon-invert.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15px" height="15px" viewBox="0 0 15 15" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(81.568627%,81.176471%,81.176471%);fill-opacity:1;" d="M 3.128906 2.40625 L 11.730469 2.40625 C 12.167969 2.40625 12.523438 2.78125 12.523438 3.246094 L 12.523438 11.753906 C 12.523438 12.21875 12.167969 12.59375 11.730469 12.59375 L 3.128906 12.59375 C 2.6875 12.59375 2.332031 12.21875 2.332031 11.753906 L 2.332031 3.246094 C 2.332031 2.78125 2.6875 2.40625 3.128906 2.40625 Z M 3.128906 2.40625 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.137255%,23.137255%,23.137255%);fill-opacity:1;" d="M 4.773438 9.8125 L 4.773438 6.101562 L 3.042969 6.101562 L 3.042969 5.175781 L 7.699219 5.175781 L 7.699219 6.101562 L 5.96875 6.101562 L 5.96875 9.8125 C 5.96875 9.8125 4.773438 9.8125 4.773438 9.8125 Z M 4.773438 9.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.137255%,23.137255%,23.137255%);fill-opacity:1;" d="M 8.203125 9.308594 L 10.804688 9.308594 C 11.15625 9.308594 11.296875 9.0625 11.296875 8.660156 C 11.296875 8.414062 11.246094 8.167969 10.777344 8.019531 L 8.9375 7.410156 C 8.378906 7.222656 8.136719 6.90625 8.136719 6.261719 C 8.136719 5.601562 8.457031 5.175781 9.195312 5.175781 L 11.71875 5.175781 L 11.71875 5.679688 L 9.308594 5.679688 C 8.957031 5.679688 8.785156 5.84375 8.785156 6.277344 C 8.785156 6.527344 8.808594 6.773438 9.222656 6.910156 L 10.980469 7.496094 C 11.597656 7.699219 11.945312 7.933594 11.945312 8.660156 C 11.945312 9.289062 11.695312 9.8125 10.980469 9.8125 L 8.203125 9.8125 Z M 8.203125 9.308594 "/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
8
src/main/resources/image/tapsigner-icon.svg
Normal file
8
src/main/resources/image/tapsigner-icon.svg
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="15px" height="15px" viewBox="0 0 15 15" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(23.137255%,23.137255%,23.137255%);fill-opacity:1;" d="M 3.128906 2.40625 L 11.730469 2.40625 C 12.167969 2.40625 12.523438 2.78125 12.523438 3.246094 L 12.523438 11.753906 C 12.523438 12.21875 12.167969 12.59375 11.730469 12.59375 L 3.128906 12.59375 C 2.6875 12.59375 2.332031 12.21875 2.332031 11.753906 L 2.332031 3.246094 C 2.332031 2.78125 2.6875 2.40625 3.128906 2.40625 Z M 3.128906 2.40625 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.411765%,89.411765%,89.411765%);fill-opacity:1;" d="M 4.773438 9.8125 L 4.773438 6.101562 L 3.042969 6.101562 L 3.042969 5.175781 L 7.699219 5.175781 L 7.699219 6.101562 L 5.96875 6.101562 L 5.96875 9.8125 C 5.96875 9.8125 4.773438 9.8125 4.773438 9.8125 Z M 4.773438 9.8125 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(89.411765%,89.411765%,89.411765%);fill-opacity:1;" d="M 8.203125 9.308594 L 10.804688 9.308594 C 11.15625 9.308594 11.296875 9.0625 11.296875 8.660156 C 11.296875 8.414062 11.246094 8.167969 10.777344 8.019531 L 8.9375 7.410156 C 8.378906 7.222656 8.136719 6.90625 8.136719 6.261719 C 8.136719 5.601562 8.457031 5.175781 9.195312 5.175781 L 11.71875 5.175781 L 11.71875 5.679688 L 9.308594 5.679688 C 8.957031 5.679688 8.785156 5.84375 8.785156 6.277344 C 8.785156 6.527344 8.808594 6.773438 9.222656 6.910156 L 10.980469 7.496094 C 11.597656 7.699219 11.945312 7.933594 11.945312 8.660156 C 11.945312 9.289062 11.695312 9.8125 10.980469 9.8125 L 8.203125 9.8125 Z M 8.203125 9.308594 "/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/main/resources/image/tapsigner.png
Normal file
BIN
src/main/resources/image/tapsigner.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
BIN
src/main/resources/image/tapsigner@2x.png
Normal file
BIN
src/main/resources/image/tapsigner@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
src/main/resources/image/tapsigner@3x.png
Normal file
BIN
src/main/resources/image/tapsigner@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
Loading…
Reference in a new issue