mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-04 21:36:45 +00:00
add initial satochip card support
This commit is contained in:
parent
24578dcf88
commit
ee6589991d
27 changed files with 2268 additions and 33 deletions
2
drongo
2
drongo
|
@ -1 +1 @@
|
||||||
Subproject commit 30aff119081a4a13f931ea6625f69d7974addb04
|
Subproject commit 12db57c8d75c6f9eb96a8da89e80139850cc450b
|
|
@ -4,8 +4,7 @@ import com.google.common.base.Throwables;
|
||||||
import com.sparrowwallet.drongo.KeyDerivation;
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||||
|
@ -36,6 +35,7 @@ import org.slf4j.LoggerFactory;
|
||||||
import javax.smartcardio.CardException;
|
import javax.smartcardio.CardException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
||||||
|
|
||||||
|
@ -74,8 +74,14 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(pin.get().length() < 6) {
|
StringProperty messageProperty = new SimpleStringProperty();
|
||||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> setDescription(newValue));
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(pin.get().length() < importer.getWalletModel().getMinPinLength()) {
|
||||||
|
setDescription(pin.get().isEmpty() ? (!importer.getWalletModel().hasDefaultPin() && !importer.isInitialized() ? "Choose a PIN code" : "Enter PIN code") : "PIN code too short");
|
||||||
setContent(getPinAndDerivationEntry());
|
setContent(getPinAndDerivationEntry());
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
setExpanded(true);
|
setExpanded(true);
|
||||||
|
@ -83,12 +89,6 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringProperty messageProperty = new SimpleStringProperty();
|
|
||||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
|
||||||
Platform.runLater(() -> setDescription(newValue));
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
if(!importer.isInitialized()) {
|
if(!importer.isInitialized()) {
|
||||||
setDescription("Card not initialized");
|
setDescription("Card not initialized");
|
||||||
setContent(getInitializationPanel(messageProperty));
|
setContent(getInitializationPanel(messageProperty));
|
||||||
|
@ -121,6 +121,75 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node getInitializationPanel(StringProperty messageProperty) {
|
private Node getInitializationPanel(StringProperty messageProperty) {
|
||||||
|
if(importer.getWalletModel().requiresSeedInitialization()) {
|
||||||
|
return getSeedInitializationPanel(messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getEntropyInitializationPanel(messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getSeedInitializationPanel(StringProperty messageProperty) {
|
||||||
|
VBox confirmationBox = new VBox(5);
|
||||||
|
CustomPasswordField confirmationPin = new ViewPasswordField();
|
||||||
|
confirmationPin.setPromptText("Re-enter chosen PIN");
|
||||||
|
confirmationBox.getChildren().add(confirmationPin);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
if(!pin.get().equals(confirmationPin.getText())) {
|
||||||
|
setError("PIN Error", "The confirmation PIN did not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pinSize = pin.get().length();
|
||||||
|
if(pinSize < importer.getWalletModel().getMinPinLength() || pinSize > importer.getWalletModel().getMaxPinLength()) {
|
||||||
|
setError("PIN Error", "PIN length must be between " + importer.getWalletModel().getMinPinLength() + " and " + importer.getWalletModel().getMaxPinLength() + " characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(importer.getWalletModel().toDisplayString() + " Seed Words", 12);
|
||||||
|
seedEntryDialog.initOwner(this.getScene().getWindow());
|
||||||
|
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||||
|
if(optWords.isPresent()) {
|
||||||
|
try {
|
||||||
|
List<String> mnemonicWords = optWords.get();
|
||||||
|
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
|
||||||
|
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, "", System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||||
|
byte[] seedBytes = seed.getSeedBytes();
|
||||||
|
|
||||||
|
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), seedBytes, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
setDescription("Leave card on reader");
|
||||||
|
setExpanded(false);
|
||||||
|
importButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
log.error("Invalid seed entered", e);
|
||||||
|
AppServices.showErrorDialog("Invalid seed entered", "The seed was invalid.\n\n" + e.getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(confirmationBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(confirmationBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getEntropyInitializationPanel(StringProperty messageProperty) {
|
||||||
VBox initTypeBox = new VBox(5);
|
VBox initTypeBox = new VBox(5);
|
||||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||||
RadioButton advanced = new RadioButton("Advanced");
|
RadioButton advanced = new RadioButton("Advanced");
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
@ -22,7 +23,7 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||||
private final CheckBox backupFirst;
|
private final CheckBox backupFirst;
|
||||||
private final ButtonType okButtonType;
|
private final ButtonType okButtonType;
|
||||||
|
|
||||||
public CardPinDialog(boolean backupOnly) {
|
public CardPinDialog(WalletModel walletModel, boolean backupOnly) {
|
||||||
this.existingPin = new ViewPasswordField();
|
this.existingPin = new ViewPasswordField();
|
||||||
this.newPin = new ViewPasswordField();
|
this.newPin = new ViewPasswordField();
|
||||||
this.newPinConfirm = new ViewPasswordField();
|
this.newPinConfirm = new ViewPasswordField();
|
||||||
|
@ -71,7 +72,11 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||||
if(backupOnly) {
|
if(backupOnly) {
|
||||||
fieldset.getChildren().addAll(currentField);
|
fieldset.getChildren().addAll(currentField);
|
||||||
} else {
|
} else {
|
||||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
fieldset.getChildren().addAll(currentField, newField, confirmField);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(walletModel.supportsBackup()) {
|
||||||
|
fieldset.getChildren().add(backupField);
|
||||||
}
|
}
|
||||||
|
|
||||||
form.getChildren().add(fieldset);
|
form.getChildren().add(fieldset);
|
||||||
|
@ -80,8 +85,8 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||||
ValidationSupport validationSupport = new ValidationSupport();
|
ValidationSupport validationSupport = new ValidationSupport();
|
||||||
Platform.runLater( () -> {
|
Platform.runLater( () -> {
|
||||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||||
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < 6 || existingPin.getText().length() > 32));
|
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < walletModel.getMinPinLength() || existingPin.getText().length() > walletModel.getMaxPinLength()));
|
||||||
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < 6 || newPin.getText().length() > 32));
|
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < walletModel.getMinPinLength() || newPin.getText().length() > walletModel.getMaxPinLength()));
|
||||||
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -89,8 +94,8 @@ public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||||
okButton.setPrefWidth(130);
|
okButton.setPrefWidth(130);
|
||||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < 6 || existingPin.getText().length() > 32
|
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < walletModel.getMinPinLength() || existingPin.getText().length() > walletModel.getMaxPinLength()
|
||||||
|| newPin.getText().length() < 6 || newPin.getText().length() > 32
|
|| newPin.getText().length() < walletModel.getMinPinLength() || newPin.getText().length() > walletModel.getMaxPinLength()
|
||||||
|| !newPin.getText().equals(newPinConfirm.getText()),
|
|| !newPin.getText().equals(newPinConfirm.getText()),
|
||||||
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
||||||
okButton.disableProperty().bind(isInvalid);
|
okButton.disableProperty().bind(isInvalid);
|
||||||
|
|
|
@ -673,8 +673,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
try {
|
try {
|
||||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
if(!cardApi.isInitialized()) {
|
if(!cardApi.isInitialized()) {
|
||||||
if(pin.get().length() < 6) {
|
if(pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
setDescription(pin.get().isEmpty() ? (device.getModel().hasDefaultPin() ? "Enter PIN code" : "Choose a PIN code") : "PIN code too short");
|
||||||
setContent(getCardPinEntry(importButton));
|
setContent(getCardPinEntry(importButton));
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
setExpanded(true);
|
setExpanded(true);
|
||||||
|
@ -795,7 +795,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
|
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
|
||||||
if(pinRequired && pin.get().length() < 6) {
|
if(pinRequired && pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||||
setContent(getCardPinEntry(operationButton));
|
setContent(getCardPinEntry(operationButton));
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
|
@ -940,7 +940,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
try {
|
try {
|
||||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||||
if(!cardApi.isInitialized()) {
|
if(!cardApi.isInitialized()) {
|
||||||
if(pin.get().length() < 6) {
|
if(pin.get().length() < device.getModel().getMinPinLength()) {
|
||||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||||
setContent(getCardPinEntry(getAddressButton));
|
setContent(getCardPinEntry(getAddressButton));
|
||||||
showHideLink.setVisible(false);
|
showHideLink.setVisible(false);
|
||||||
|
@ -1047,6 +1047,75 @@ public class DevicePane extends TitledDescriptionPane {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
|
if(device.getModel().requiresSeedInitialization()) {
|
||||||
|
return getCardSeedInitializationPanel(cardApi, operationButton, deviceOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getCardEntropyInitializationPanel(cardApi, operationButton, deviceOperation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getCardSeedInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
|
VBox confirmationBox = new VBox(5);
|
||||||
|
CustomPasswordField confirmationPin = new ViewPasswordField();
|
||||||
|
confirmationPin.setPromptText("Re-enter chosen PIN");
|
||||||
|
confirmationBox.getChildren().add(confirmationPin);
|
||||||
|
|
||||||
|
Button initializeButton = new Button("Initialize");
|
||||||
|
initializeButton.setDefaultButton(true);
|
||||||
|
initializeButton.setOnAction(event -> {
|
||||||
|
initializeButton.setDisable(true);
|
||||||
|
if(!pin.get().equals(confirmationPin.getText())) {
|
||||||
|
setError("PIN Error", "The confirmation PIN did not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pinSize = pin.get().length();
|
||||||
|
if(pinSize < device.getModel().getMinPinLength() || pinSize > device.getModel().getMaxPinLength()) {
|
||||||
|
setError("PIN Error", "PIN length must be between " + device.getModel().getMinPinLength() + " and " + device.getModel().getMaxPinLength() + " characters");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(device.getModel().toDisplayString() + " Seed Words", 12);
|
||||||
|
seedEntryDialog.initOwner(this.getScene().getWindow());
|
||||||
|
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||||
|
if(optWords.isPresent()) {
|
||||||
|
try {
|
||||||
|
List<String> mnemonicWords = optWords.get();
|
||||||
|
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
|
||||||
|
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, "", System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||||
|
byte[] seedBytes = seed.getSeedBytes();
|
||||||
|
|
||||||
|
Service<Void> cardInitializationService = cardApi.getInitializationService(seedBytes, messageProperty);
|
||||||
|
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||||
|
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||||
|
operationButton.setDisable(false);
|
||||||
|
setDefaultStatus();
|
||||||
|
setExpanded(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.setOnFailed(failEvent -> {
|
||||||
|
log.error("Error initializing card", failEvent.getSource().getException());
|
||||||
|
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
});
|
||||||
|
cardInitializationService.start();
|
||||||
|
} catch(MnemonicException e) {
|
||||||
|
log.error("Invalid seed entered", e);
|
||||||
|
AppServices.showErrorDialog("Invalid seed entered", "The seed was invalid.\n\n" + e.getMessage());
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initializeButton.setDisable(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
HBox contentBox = new HBox(20);
|
||||||
|
contentBox.getChildren().addAll(confirmationBox, initializeButton);
|
||||||
|
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||||
|
HBox.setHgrow(confirmationBox, Priority.ALWAYS);
|
||||||
|
|
||||||
|
return contentBox;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node getCardEntropyInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||||
VBox initTypeBox = new VBox(5);
|
VBox initTypeBox = new VBox(5);
|
||||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||||
RadioButton advanced = new RadioButton("Advanced");
|
RadioButton advanced = new RadioButton("Advanced");
|
||||||
|
|
|
@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||||
import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
|
import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
|
||||||
|
import com.sparrowwallet.sparrow.io.satochip.SatoCardApi;
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.concurrent.Service;
|
import javafx.concurrent.Service;
|
||||||
import org.controlsfx.tools.Platform;
|
import org.controlsfx.tools.Platform;
|
||||||
|
@ -23,6 +24,7 @@ import javax.smartcardio.TerminalFactory;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -34,19 +36,29 @@ public abstract class CardApi {
|
||||||
new File("/usr/local/lib/libpcsclite.so.1"),
|
new File("/usr/local/lib/libpcsclite.so.1"),
|
||||||
new File("/lib/x86_64-linux-gnu/libpcsclite.so.1"),
|
new File("/lib/x86_64-linux-gnu/libpcsclite.so.1"),
|
||||||
new File("/lib/aarch64-linux-gnu/libpcsclite.so.1"),
|
new File("/lib/aarch64-linux-gnu/libpcsclite.so.1"),
|
||||||
new File("/usr/lib64/libpcsclite.so.1")};
|
new File("/usr/lib64/libpcsclite.so.1"),
|
||||||
|
new File("/usr/lib/x86_64-linux-gnu/libpcsclite.so.1")};
|
||||||
|
|
||||||
private static boolean initialized;
|
private static boolean initialized;
|
||||||
|
|
||||||
public static List<WalletModel> getConnectedCards() throws CardException {
|
public static List<WalletModel> getConnectedCards() throws CardException {
|
||||||
|
List<WalletModel> cards = new ArrayList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CkCardApi ckCardApi = new CkCardApi(null, null);
|
CkCardApi ckCardApi = new CkCardApi(null, null);
|
||||||
return List.of(ckCardApi.getCardType());
|
cards.add(ckCardApi.getCardType());
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
//ignore
|
//ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
return Collections.emptyList();
|
try {
|
||||||
|
SatoCardApi satoCardApi = new SatoCardApi(null, null);
|
||||||
|
cards.add(satoCardApi.getCardType());
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CardApi getCardApi(WalletModel walletModel, String pin) throws CardException {
|
public static CardApi getCardApi(WalletModel walletModel, String pin) throws CardException {
|
||||||
|
@ -54,6 +66,10 @@ public abstract class CardApi {
|
||||||
return new CkCardApi(walletModel, pin);
|
return new CkCardApi(walletModel, pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(walletModel == WalletModel.SATOCHIP) {
|
||||||
|
return new SatoCardApi(walletModel, pin);
|
||||||
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
|
throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,5 @@ import javax.smartcardio.CardException;
|
||||||
|
|
||||||
public interface CardImport extends ImportExport {
|
public interface CardImport extends ImportExport {
|
||||||
boolean isInitialized() throws CardException;
|
boolean isInitialized() throws CardException;
|
||||||
void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException;
|
void initialize(String pin, byte[] entropy, StringProperty messageProperty) throws CardException;
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ public class CardTransport {
|
||||||
break;
|
break;
|
||||||
} catch(CardException e) {
|
} catch(CardException e) {
|
||||||
if(!iter.hasNext()) {
|
if(!iter.hasNext()) {
|
||||||
log.error(e.getMessage());
|
log.debug(e.getMessage());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@ public class CardTransport {
|
||||||
CardChannel cardChannel = connection.getBasicChannel();
|
CardChannel cardChannel = connection.getBasicChannel();
|
||||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
|
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
|
||||||
if(resp.getSW() != SW_OKAY) {
|
if(resp.getSW() != SW_OKAY) {
|
||||||
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()) + ". Note that only the Tapsigner is currently supported.");
|
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class Tapsigner implements KeystoreCardImport {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException {
|
public void initialize(String pin, byte[] entropy, StringProperty messageProperty) throws CardException {
|
||||||
if(pin.length() < 6) {
|
if(pin.length() < 6) {
|
||||||
throw new CardException("PIN too short.");
|
throw new CardException("PIN too short.");
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ public class Tapsigner implements KeystoreCardImport {
|
||||||
throw new IllegalStateException("Card is already initialized.");
|
throw new IllegalStateException("Card is already initialized.");
|
||||||
}
|
}
|
||||||
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||||
cardApi.initialize(0, chainCode);
|
cardApi.initialize(0, entropy);
|
||||||
} finally {
|
} finally {
|
||||||
if(cardApi != null) {
|
if(cardApi != null) {
|
||||||
cardApi.disconnect();
|
cardApi.disconnect();
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO7816-4 APDU.
|
||||||
|
*/
|
||||||
|
public class APDUCommand {
|
||||||
|
protected int cla;
|
||||||
|
protected int ins;
|
||||||
|
protected int p1;
|
||||||
|
protected int p2;
|
||||||
|
protected byte[] data;
|
||||||
|
protected boolean needsLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an APDU with no response data length field. The data field cannot be null, but can be a zero-length array.
|
||||||
|
*
|
||||||
|
* @param cla class byte
|
||||||
|
* @param ins instruction code
|
||||||
|
* @param p1 P1 parameter
|
||||||
|
* @param p2 P2 parameter
|
||||||
|
* @param data the APDU data
|
||||||
|
*/
|
||||||
|
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||||
|
this(cla, ins, p1, p2, data, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an APDU with an optional data length field. The data field cannot be null, but can be a zero-length array.
|
||||||
|
* The LE byte, if sent, is set to 0.
|
||||||
|
*
|
||||||
|
* @param cla class byte
|
||||||
|
* @param ins instruction code
|
||||||
|
* @param p1 P1 parameter
|
||||||
|
* @param p2 P2 parameter
|
||||||
|
* @param data the APDU data
|
||||||
|
* @param needsLE whether the LE byte should be sent or not
|
||||||
|
*/
|
||||||
|
public APDUCommand(int cla, int ins, int p1, int p2, byte[] data, boolean needsLE) {
|
||||||
|
this.cla = cla & 0xff;
|
||||||
|
this.ins = ins & 0xff;
|
||||||
|
this.p1 = p1 & 0xff;
|
||||||
|
this.p2 = p2 & 0xff;
|
||||||
|
this.data = data;
|
||||||
|
this.needsLE = needsLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the APDU in order to send it to the card.
|
||||||
|
*
|
||||||
|
* @return the byte array representation of the APDU
|
||||||
|
*/
|
||||||
|
public byte[] serialize() {
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(this.cla);
|
||||||
|
out.write(this.ins);
|
||||||
|
out.write(this.p1);
|
||||||
|
out.write(this.p2);
|
||||||
|
out.write(this.data.length);
|
||||||
|
out.write(this.data, 0, this.data.length);
|
||||||
|
|
||||||
|
if(this.needsLE) {
|
||||||
|
out.write(0); // Response length
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the APDU to human readable hex string format
|
||||||
|
*
|
||||||
|
* @return the hex string representation of the APDU
|
||||||
|
*/
|
||||||
|
public String toHexString() {
|
||||||
|
byte[] raw = this.serialize();
|
||||||
|
if(raw == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.bytesToHex(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the CLA of the APDU
|
||||||
|
*
|
||||||
|
* @return the CLA of the APDU
|
||||||
|
*/
|
||||||
|
public int getCla() {
|
||||||
|
return cla;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the INS of the APDU
|
||||||
|
*
|
||||||
|
* @return the INS of the APDU
|
||||||
|
*/
|
||||||
|
public int getIns() {
|
||||||
|
return ins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the P1 of the APDU
|
||||||
|
*
|
||||||
|
* @return the P1 of the APDU
|
||||||
|
*/
|
||||||
|
public int getP1() {
|
||||||
|
return p1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the P2 of the APDU
|
||||||
|
*
|
||||||
|
* @return the P2 of the APDU
|
||||||
|
*/
|
||||||
|
public int getP2() {
|
||||||
|
return p2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data field of the APDU
|
||||||
|
*
|
||||||
|
* @return the data field of the APDU
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether LE is sent or not.
|
||||||
|
*
|
||||||
|
* @return whether LE is sent or not
|
||||||
|
*/
|
||||||
|
public boolean getNeedsLE() {
|
||||||
|
return this.needsLE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ISO7816-4 APDU response.
|
||||||
|
*/
|
||||||
|
public class APDUResponse {
|
||||||
|
public static final int SW_OK = 0x9000;
|
||||||
|
public static final int SW_SECURITY_CONDITION_NOT_SATISFIED = 0x6982;
|
||||||
|
public static final int SW_AUTHENTICATION_METHOD_BLOCKED = 0x6983;
|
||||||
|
public static final int SW_CARD_LOCKED = 0x6283;
|
||||||
|
public static final int SW_REFERENCED_DATA_NOT_FOUND = 0x6A88;
|
||||||
|
public static final int SW_CONDITIONS_OF_USE_NOT_SATISFIED = 0x6985; // applet may be already installed
|
||||||
|
public static final int SW_WRONG_PIN_MASK = 0x63C0;
|
||||||
|
public static final String HEXES = "0123456789ABCDEF";
|
||||||
|
|
||||||
|
private final byte[] apdu;
|
||||||
|
private byte[] data;
|
||||||
|
private int sw;
|
||||||
|
private int sw1;
|
||||||
|
private int sw2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an APDU object by parsing the raw response from the card.
|
||||||
|
*
|
||||||
|
* @param apdu the raw response from the card.
|
||||||
|
*/
|
||||||
|
public APDUResponse(byte[] apdu) {
|
||||||
|
if(apdu.length < 2) {
|
||||||
|
throw new IllegalArgumentException("APDU response must be at least 2 bytes");
|
||||||
|
}
|
||||||
|
this.apdu = apdu;
|
||||||
|
this.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse(byte[] data, byte sw1, byte sw2) {
|
||||||
|
byte[] apdu = new byte[data.length + 2];
|
||||||
|
System.arraycopy(data, 0, apdu, 0, data.length);
|
||||||
|
apdu[data.length] = sw1;
|
||||||
|
apdu[data.length + 1] = sw2;
|
||||||
|
this.apdu = apdu;
|
||||||
|
this.parse();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the APDU response, separating the response data from SW.
|
||||||
|
*/
|
||||||
|
private void parse() {
|
||||||
|
int length = this.apdu.length;
|
||||||
|
|
||||||
|
this.sw1 = this.apdu[length - 2] & 0xff;
|
||||||
|
this.sw2 = this.apdu[length - 1] & 0xff;
|
||||||
|
this.sw = (this.sw1 << 8) | this.sw2;
|
||||||
|
|
||||||
|
this.data = new byte[length - 2];
|
||||||
|
System.arraycopy(this.apdu, 0, this.data, 0, length - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the data field of this APDU.
|
||||||
|
*
|
||||||
|
* @return the data field of this APDU
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Status Word.
|
||||||
|
*
|
||||||
|
* @return the status word
|
||||||
|
*/
|
||||||
|
public int getSw() {
|
||||||
|
return this.sw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SW1 byte
|
||||||
|
*
|
||||||
|
* @return SW1
|
||||||
|
*/
|
||||||
|
public int getSw1() {
|
||||||
|
return this.sw1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the SW2 byte
|
||||||
|
*
|
||||||
|
* @return SW2
|
||||||
|
*/
|
||||||
|
public int getSw2() {
|
||||||
|
return this.sw2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the raw unparsed response.
|
||||||
|
*
|
||||||
|
* @return raw APDU data
|
||||||
|
*/
|
||||||
|
public byte[] getBytes() {
|
||||||
|
return this.apdu;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serializes the APDU to human readable hex string format
|
||||||
|
*
|
||||||
|
* @return the hex string representation of the APDU
|
||||||
|
*/
|
||||||
|
public String toHexString() {
|
||||||
|
byte[] raw = this.apdu;
|
||||||
|
if(raw == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.bytesToHex(raw);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,214 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
public final class Constants {
|
||||||
|
|
||||||
|
// Prevents instantiation of class
|
||||||
|
private Constants() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* Instruction codes *
|
||||||
|
****************************************/
|
||||||
|
public static final byte CLA = (byte) 0xB0;
|
||||||
|
// Applet initialization
|
||||||
|
public static final byte INS_SETUP = (byte) 0x2A;
|
||||||
|
// Keys' use and management
|
||||||
|
public static final byte INS_IMPORT_KEY = (byte) 0x32;
|
||||||
|
public static final byte INS_RESET_KEY = (byte) 0x33;
|
||||||
|
public static final byte INS_GET_PUBLIC_FROM_PRIVATE = (byte) 0x35;
|
||||||
|
// External authentication
|
||||||
|
public static final byte INS_CREATE_PIN = (byte) 0x40; //TODO: remove?
|
||||||
|
public static final byte INS_VERIFY_PIN = (byte) 0x42;
|
||||||
|
public static final byte INS_CHANGE_PIN = (byte) 0x44;
|
||||||
|
public static final byte INS_UNBLOCK_PIN = (byte) 0x46;
|
||||||
|
public static final byte INS_LOGOUT_ALL = (byte) 0x60;
|
||||||
|
// Status information
|
||||||
|
public static final byte INS_LIST_PINS = (byte) 0x48;
|
||||||
|
public static final byte INS_GET_STATUS = (byte) 0x3C;
|
||||||
|
public static final byte INS_CARD_LABEL = (byte) 0x3D;
|
||||||
|
// HD wallet
|
||||||
|
public static final byte INS_BIP32_IMPORT_SEED = (byte) 0x6C;
|
||||||
|
public static final byte INS_BIP32_RESET_SEED = (byte) 0x77;
|
||||||
|
public static final byte INS_BIP32_GET_AUTHENTIKEY = (byte) 0x73;
|
||||||
|
public static final byte INS_BIP32_SET_AUTHENTIKEY_PUBKEY = (byte) 0x75;
|
||||||
|
public static final byte INS_BIP32_GET_EXTENDED_KEY = (byte) 0x6D;
|
||||||
|
public static final byte INS_BIP32_SET_EXTENDED_PUBKEY = (byte) 0x74;
|
||||||
|
public static final byte INS_SIGN_MESSAGE = (byte) 0x6E;
|
||||||
|
public static final byte INS_SIGN_SHORT_MESSAGE = (byte) 0x72;
|
||||||
|
public static final byte INS_SIGN_TRANSACTION = (byte) 0x6F;
|
||||||
|
public static final byte INS_PARSE_TRANSACTION = (byte) 0x71;
|
||||||
|
public static final byte INS_CRYPT_TRANSACTION_2FA = (byte) 0x76;
|
||||||
|
public static final byte INS_SET_2FA_KEY = (byte) 0x79;
|
||||||
|
public static final byte INS_RESET_2FA_KEY = (byte) 0x78;
|
||||||
|
public static final byte INS_SIGN_TRANSACTION_HASH = (byte) 0x7A;
|
||||||
|
// secure channel
|
||||||
|
public static final byte INS_INIT_SECURE_CHANNEL = (byte) 0x81;
|
||||||
|
public static final byte INS_PROCESS_SECURE_CHANNEL = (byte) 0x82;
|
||||||
|
// secure import from SeedKeeper
|
||||||
|
public static final byte INS_IMPORT_ENCRYPTED_SECRET = (byte) 0xAC;
|
||||||
|
public static final byte INS_IMPORT_TRUSTED_PUBKEY = (byte) 0xAA;
|
||||||
|
public static final byte INS_EXPORT_TRUSTED_PUBKEY = (byte) 0xAB;
|
||||||
|
public static final byte INS_EXPORT_AUTHENTIKEY = (byte) 0xAD;
|
||||||
|
// Personalization PKI support
|
||||||
|
public static final byte INS_IMPORT_PKI_CERTIFICATE = (byte) 0x92;
|
||||||
|
public static final byte INS_EXPORT_PKI_CERTIFICATE = (byte) 0x93;
|
||||||
|
public static final byte INS_SIGN_PKI_CSR = (byte) 0x94;
|
||||||
|
public static final byte INS_EXPORT_PKI_PUBKEY = (byte) 0x98;
|
||||||
|
public static final byte INS_LOCK_PKI = (byte) 0x99;
|
||||||
|
public static final byte INS_CHALLENGE_RESPONSE_PKI = (byte) 0x9A;
|
||||||
|
// reset to factory settings
|
||||||
|
public static final byte INS_RESET_TO_FACTORY = (byte) 0xFF;
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* Error codes *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entered PIN is not correct
|
||||||
|
*/
|
||||||
|
public static final short SW_PIN_FAILED = (short) 0x63C0;// includes number of tries remaining
|
||||||
|
///** DEPRECATED - Entered PIN is not correct */
|
||||||
|
//public static final short SW_AUTH_FAILED = (short) 0x9C02;
|
||||||
|
/**
|
||||||
|
* Required operation is not allowed in actual circumstances
|
||||||
|
*/
|
||||||
|
public static final short SW_OPERATION_NOT_ALLOWED = (short) 0x9C03;
|
||||||
|
/**
|
||||||
|
* Required setup is not not done
|
||||||
|
*/
|
||||||
|
public static final short SW_SETUP_NOT_DONE = (short) 0x9C04;
|
||||||
|
/**
|
||||||
|
* Required setup is already done
|
||||||
|
*/
|
||||||
|
public static final short SW_SETUP_ALREADY_DONE = (short) 0x9C07;
|
||||||
|
/**
|
||||||
|
* Required feature is not (yet) supported
|
||||||
|
*/
|
||||||
|
public static final short SW_UNSUPPORTED_FEATURE = (short) 0x9C05;
|
||||||
|
/**
|
||||||
|
* Required operation was not authorized because of a lack of privileges
|
||||||
|
*/
|
||||||
|
public static final short SW_UNAUTHORIZED = (short) 0x9C06;
|
||||||
|
/**
|
||||||
|
* Algorithm specified is not correct
|
||||||
|
*/
|
||||||
|
public static final short SW_INCORRECT_ALG = (short) 0x9C09;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There have been memory problems on the card
|
||||||
|
*/
|
||||||
|
public static final short SW_NO_MEMORY_LEFT = (short) 0x9C01;
|
||||||
|
///** DEPRECATED - Required object is missing */
|
||||||
|
//public static final short SW_OBJECT_NOT_FOUND= (short) 0x9C07;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Incorrect P1 parameter
|
||||||
|
*/
|
||||||
|
public static final short SW_INCORRECT_P1 = (short) 0x9C10;
|
||||||
|
/**
|
||||||
|
* Incorrect P2 parameter
|
||||||
|
*/
|
||||||
|
public static final short SW_INCORRECT_P2 = (short) 0x9C11;
|
||||||
|
/**
|
||||||
|
* Invalid input parameter to command
|
||||||
|
*/
|
||||||
|
public static final short SW_INVALID_PARAMETER = (short) 0x9C0F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eckeys initialized
|
||||||
|
*/
|
||||||
|
public static final short SW_ECKEYS_INITIALIZED_KEY = (short) 0x9C1A;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify operation detected an invalid signature
|
||||||
|
*/
|
||||||
|
public static final short SW_SIGNATURE_INVALID = (short) 0x9C0B;
|
||||||
|
/**
|
||||||
|
* Operation has been blocked for security reason
|
||||||
|
*/
|
||||||
|
public static final short SW_IDENTITY_BLOCKED = (short) 0x9C0C;
|
||||||
|
/**
|
||||||
|
* For debugging purposes
|
||||||
|
*/
|
||||||
|
public static final short SW_INTERNAL_ERROR = (short) 0x9CFF;
|
||||||
|
/**
|
||||||
|
* Very low probability error
|
||||||
|
*/
|
||||||
|
public static final short SW_BIP32_DERIVATION_ERROR = (short) 0x9C0E;
|
||||||
|
/**
|
||||||
|
* Incorrect initialization of method
|
||||||
|
*/
|
||||||
|
public static final short SW_INCORRECT_INITIALIZATION = (short) 0x9C13;
|
||||||
|
/**
|
||||||
|
* Bip32 seed is not initialized
|
||||||
|
*/
|
||||||
|
public static final short SW_BIP32_UNINITIALIZED_SEED = (short) 0x9C14;
|
||||||
|
/**
|
||||||
|
* Bip32 seed is already initialized (must be reset before change)
|
||||||
|
*/
|
||||||
|
public static final short SW_BIP32_INITIALIZED_SEED = (short) 0x9C17;
|
||||||
|
//** DEPRECATED - Bip32 authentikey pubkey is not initialized*/
|
||||||
|
//public static final short SW_BIP32_UNINITIALIZED_AUTHENTIKEY_PUBKEY= (short) 0x9C16;
|
||||||
|
/**
|
||||||
|
* Incorrect transaction hash
|
||||||
|
*/
|
||||||
|
public static final short SW_INCORRECT_TXHASH = (short) 0x9C15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 2FA already initialized
|
||||||
|
*/
|
||||||
|
public static final short SW_2FA_INITIALIZED_KEY = (short) 0x9C18;
|
||||||
|
/**
|
||||||
|
* 2FA uninitialized
|
||||||
|
*/
|
||||||
|
public static final short SW_2FA_UNINITIALIZED_KEY = (short) 0x9C19;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HMAC errors
|
||||||
|
*/
|
||||||
|
public static final short SW_HMAC_UNSUPPORTED_KEYSIZE = (short) 0x9c1E;
|
||||||
|
public static final short SW_HMAC_UNSUPPORTED_MSGSIZE = (short) 0x9c1F;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secure channel
|
||||||
|
*/
|
||||||
|
public static final short SW_SECURE_CHANNEL_REQUIRED = (short) 0x9C20;
|
||||||
|
public static final short SW_SECURE_CHANNEL_UNINITIALIZED = (short) 0x9C21;
|
||||||
|
public static final short SW_SECURE_CHANNEL_WRONG_IV = (short) 0x9C22;
|
||||||
|
public static final short SW_SECURE_CHANNEL_WRONG_MAC = (short) 0x9C23;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Secret data is too long for import
|
||||||
|
**/
|
||||||
|
public static final short SW_IMPORTED_DATA_TOO_LONG = (short) 0x9C32;
|
||||||
|
/**
|
||||||
|
* Wrong HMAC when importing Secret through Secure import
|
||||||
|
**/
|
||||||
|
public static final short SW_SECURE_IMPORT_WRONG_MAC = (short) 0x9C33;
|
||||||
|
/**
|
||||||
|
* Wrong Fingerprint when importing Secret through Secure import
|
||||||
|
**/
|
||||||
|
public static final short SW_SECURE_IMPORT_WRONG_FINGERPRINT = (short) 0x9C34;
|
||||||
|
/**
|
||||||
|
* No Trusted Pubkey when importing Secret through Secure import
|
||||||
|
**/
|
||||||
|
public static final short SW_SECURE_IMPORT_NO_TRUSTEDPUBKEY = (short) 0x9C35;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PKI perso error
|
||||||
|
*/
|
||||||
|
public static final short SW_PKI_ALREADY_LOCKED = (short) 0x9C40;
|
||||||
|
/**
|
||||||
|
* CARD HAS BEEN RESET TO FACTORY
|
||||||
|
*/
|
||||||
|
public static final short SW_RESET_TO_FACTORY = (short) 0xFF00;
|
||||||
|
/**
|
||||||
|
* For instructions that have been deprecated
|
||||||
|
*/
|
||||||
|
public static final short SW_INS_DEPRECATED = (short) 0x9C26;
|
||||||
|
/**
|
||||||
|
* For debugging purposes 2
|
||||||
|
*/
|
||||||
|
public static final short SW_DEBUG_FLAG = (short) 0x9FFF;
|
||||||
|
|
||||||
|
}
|
101
src/main/java/com/sparrowwallet/sparrow/io/satochip/KeyPath.java
Normal file
101
src/main/java/com/sparrowwallet/sparrow/io/satochip/KeyPath.java
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import java.util.StringTokenizer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keypath object to be used with the SatochipCommandSet
|
||||||
|
*/
|
||||||
|
public class KeyPath {
|
||||||
|
private final byte[] data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a keypath into a byte array to be used with the SatochipCommandSet object.
|
||||||
|
* <p>
|
||||||
|
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
|
||||||
|
* <p>
|
||||||
|
* The first component should be "m", indicating the master key.
|
||||||
|
* <p>
|
||||||
|
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
|
||||||
|
* which indicates an hardened key.
|
||||||
|
* <p>
|
||||||
|
* An example of a valid path is "m/44'/0'/0'/0/0"
|
||||||
|
*
|
||||||
|
* @param keypath the keypath as a string
|
||||||
|
*/
|
||||||
|
public KeyPath(String keypath) {
|
||||||
|
StringTokenizer tokenizer = new StringTokenizer(keypath, "/");
|
||||||
|
|
||||||
|
String sourceOrFirstElement = tokenizer.nextToken(); // m
|
||||||
|
|
||||||
|
int componentCount = tokenizer.countTokens();
|
||||||
|
if(componentCount > 10) {
|
||||||
|
throw new IllegalArgumentException("Too many components");
|
||||||
|
}
|
||||||
|
|
||||||
|
data = new byte[4 * componentCount];
|
||||||
|
|
||||||
|
for(int i = 0; i < componentCount; i++) {
|
||||||
|
long component = parseComponent(tokenizer.nextToken());
|
||||||
|
writeComponent(component, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPath(byte[] data) {
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseComponent(String num) {
|
||||||
|
long sign;
|
||||||
|
|
||||||
|
if(num.endsWith("'")) {
|
||||||
|
sign = 0x80000000L;
|
||||||
|
num = num.substring(0, (num.length() - 1));
|
||||||
|
} else {
|
||||||
|
sign = 0L;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(num.startsWith("+") || num.startsWith("-")) {
|
||||||
|
throw new NumberFormatException("No sign allowed");
|
||||||
|
}
|
||||||
|
return (sign | Long.parseLong(num));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeComponent(long component, int i) {
|
||||||
|
int off = (i * 4);
|
||||||
|
data[off] = (byte) ((component >> 24) & 0xff);
|
||||||
|
data[off + 1] = (byte) ((component >> 16) & 0xff);
|
||||||
|
data[off + 2] = (byte) ((component >> 8) & 0xff);
|
||||||
|
data[off + 3] = (byte) (component & 0xff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The byte encoded key path.
|
||||||
|
*
|
||||||
|
* @return byte encoded key path
|
||||||
|
*/
|
||||||
|
public byte[] getData() {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
StringBuffer sb = new StringBuffer();
|
||||||
|
sb.append('m');
|
||||||
|
|
||||||
|
for(int i = 0; i < this.data.length; i += 4) {
|
||||||
|
sb.append('/');
|
||||||
|
appendComponent(sb, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendComponent(StringBuffer sb, int i) {
|
||||||
|
int num = ((this.data[i] & 0x7f) << 24) | ((this.data[i + 1] & 0xff) << 16) | ((this.data[i + 2] & 0xff) << 8) | (this.data[i + 3] & 0xff);
|
||||||
|
sb.append(num);
|
||||||
|
|
||||||
|
if((this.data[i] & 0x80) == 0x80) {
|
||||||
|
sb.append('\'');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,388 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.KeyDerivation;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.Network;
|
||||||
|
import com.sparrowwallet.drongo.address.Address;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||||
|
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
|
||||||
|
import com.sparrowwallet.drongo.protocol.*;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||||
|
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
|
||||||
|
import com.sparrowwallet.drongo.wallet.*;
|
||||||
|
import com.sparrowwallet.sparrow.control.CardImportPane;
|
||||||
|
import com.sparrowwallet.sparrow.io.CardApi;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.smartcardio.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
public class SatoCardApi extends CardApi {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SatoCardApi.class);
|
||||||
|
|
||||||
|
private final WalletModel cardType;
|
||||||
|
private final SatochipCommandSet cardProtocol;
|
||||||
|
private final String pin;
|
||||||
|
private String basePath = null;
|
||||||
|
|
||||||
|
public SatoCardApi(WalletModel cardType, String pin) throws CardException {
|
||||||
|
this.cardType = cardType;
|
||||||
|
this.cardProtocol = new SatochipCommandSet();
|
||||||
|
this.pin = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isInitialized() throws CardException {
|
||||||
|
SatoCardStatus cardStatus = this.getStatus();
|
||||||
|
return cardStatus.isInitialized(); // setupDone && isSeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
@Override
|
||||||
|
public void initialize(int slot, byte[] seedBytes) throws CardException {
|
||||||
|
// TODO check device certificate
|
||||||
|
SatoCardStatus cardStatus = this.getStatus();
|
||||||
|
|
||||||
|
APDUResponse rapdu;
|
||||||
|
if(!cardStatus.isSetupDone()) {
|
||||||
|
byte maxPinTries = 5;
|
||||||
|
rapdu = this.cardProtocol.cardSetup(maxPinTries, pin.getBytes(StandardCharsets.UTF_8));
|
||||||
|
// check ok
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!cardStatus.isSeeded()) {
|
||||||
|
// check pin
|
||||||
|
rapdu = this.cardProtocol.cardVerifyPIN(0, pin);
|
||||||
|
// todo: check PIN response
|
||||||
|
|
||||||
|
rapdu = this.cardProtocol.cardBip32ImportSeed(seedBytes);
|
||||||
|
// check ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WalletModel getCardType() throws CardException {
|
||||||
|
return WalletModel.SATOCHIP;
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
@Override
|
||||||
|
public int getCurrentSlot() throws CardException {
|
||||||
|
throw new CardException("Satochip does not support slots");
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
@Override
|
||||||
|
public ScriptType getDefaultScriptType() {
|
||||||
|
return ScriptType.P2WPKH;
|
||||||
|
}
|
||||||
|
|
||||||
|
SatoCardStatus getStatus() throws CardException {
|
||||||
|
return this.cardProtocol.getApplicationStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<Void> getAuthDelayService() throws CardException {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean requiresBackup() throws CardException {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<String> getBackupService() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean changePin(String newPin) throws CardException {
|
||||||
|
this.cardProtocol.cardChangePIN((byte) 0, this.pin, newPin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDerivation(List<ChildNumber> derivation) throws CardException {
|
||||||
|
this.basePath = KeyDerivation.writePath(derivation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<Void> getInitializationService(byte[] seedBytes, StringProperty messageProperty) {
|
||||||
|
return new CardInitializationService(seedBytes, messageProperty);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||||
|
return new CardImportPane.CardImportService(new Satochip(), pin, derivation, messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Satochip derives BIP32 keys based on the fullPath (from masterseed to leaf), not the partial path from a given xpub.
|
||||||
|
* the basePath (from masterseed to xpub) is only provided in Satochip.java:getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty)
|
||||||
|
* In SatoCardApi:getKeystore(), no derivation path (i.e. basePath from masterSeed to xpub or relative path) is given and no derivation is reliably available as a object field.
|
||||||
|
* currently, we try to get the path from this.basePath if available (or use a default value) but it's not reliable enough
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore() throws CardException {
|
||||||
|
this.cardProtocol.cardVerifyPIN(0, pin);
|
||||||
|
String keyDerivationString = (this.basePath != null ? this.basePath : getDefaultScriptType().getDefaultDerivationPath());
|
||||||
|
ExtendedKey.Header xtype = Network.get().getXpubHeader();
|
||||||
|
String xpub = this.cardProtocol.cardBip32GetXpub(keyDerivationString, xtype);
|
||||||
|
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(xpub);
|
||||||
|
String masterFingerprint = Utils.bytesToHex(extendedKey.getKey().getFingerprint());
|
||||||
|
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString);
|
||||||
|
|
||||||
|
Keystore keystore = new Keystore();
|
||||||
|
keystore.setLabel(WalletModel.SATOCHIP.toDisplayString());
|
||||||
|
keystore.setKeyDerivation(keyDerivation);
|
||||||
|
keystore.setSource(KeystoreSource.HW_USB);
|
||||||
|
keystore.setExtendedPublicKey(extendedKey);
|
||||||
|
keystore.setWalletModel(WalletModel.SATOCHIP);
|
||||||
|
|
||||||
|
return keystore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||||
|
return new SignService(wallet, psbt, messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sign(Wallet wallet, PSBT psbt) throws CardException {
|
||||||
|
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||||
|
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||||
|
if(!psbtInput.isSigned()) {
|
||||||
|
WalletNode signingNode = signingNodes.get(psbtInput);
|
||||||
|
String fullPath = null;
|
||||||
|
List<Keystore> keystores = wallet.getKeystores();
|
||||||
|
for(int i = 0; i < keystores.size(); i++) {
|
||||||
|
Keystore keystore = keystores.get(i);
|
||||||
|
WalletModel walletModel = keystore.getWalletModel();
|
||||||
|
if(walletModel == WalletModel.SATOCHIP) {
|
||||||
|
String basePath = keystore.getKeyDerivation().getDerivationPath();
|
||||||
|
String extendedPath = signingNode.getDerivationPath().substring(1);
|
||||||
|
fullPath = basePath + extendedPath;
|
||||||
|
keystore.getPubKey(signingNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
psbtInput.sign(new CardPSBTInputSigner(signingNode, fullPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||||
|
return new SignMessageService(message, scriptType, derivation, messageProperty);
|
||||||
|
}
|
||||||
|
|
||||||
|
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
|
||||||
|
String fullpath = KeyDerivation.writePath(derivation);
|
||||||
|
cardProtocol.cardVerifyPIN(0, pin);
|
||||||
|
|
||||||
|
// 2FA is optionnal, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
|
||||||
|
SatoCardStatus cardStatus = this.getStatus();
|
||||||
|
if(cardStatus.needs2FA()) {
|
||||||
|
throw new CardException("Satochip 2FA is not (yet) supported within Sparrow");
|
||||||
|
}
|
||||||
|
|
||||||
|
// derive the correct key in satochip
|
||||||
|
APDUResponse rapdu = cardProtocol.cardBip32GetExtendedKey(fullpath);
|
||||||
|
// recover pubkey
|
||||||
|
SatochipParser parser = new SatochipParser();
|
||||||
|
byte[][] extendeKeyBytes = parser.parseBip32GetExtendedKey(rapdu);
|
||||||
|
ECKey pubkey = ECKey.fromPublicOnly(extendeKeyBytes[0]);
|
||||||
|
|
||||||
|
// sign msg
|
||||||
|
return pubkey.signMessage(message, scriptType, hash -> {
|
||||||
|
try {
|
||||||
|
// do the signature with satochip
|
||||||
|
byte keynbr = (byte) 0xFF;
|
||||||
|
byte[] chalresponse = null;
|
||||||
|
APDUResponse rapdu2 = cardProtocol.cardSignTransactionHash(keynbr, hash.getBytes(), chalresponse);
|
||||||
|
byte[] sigBytes = rapdu2.getData();
|
||||||
|
return ECDSASignature.decodeFromDER(sigBytes);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<ECKey> getPrivateKeyService(Integer slot, StringProperty messageProperty) {
|
||||||
|
throw new UnsupportedOperationException("Satochip does not support private key export");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Service<Address> getAddressService(StringProperty messageProperty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void disconnect() {
|
||||||
|
cardProtocol.cardDisconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CardInitializationService extends Service<Void> {
|
||||||
|
private final byte[] seedBytes;
|
||||||
|
private final StringProperty messageProperty;
|
||||||
|
|
||||||
|
public CardInitializationService(byte[] seedBytes, StringProperty messageProperty) {
|
||||||
|
this.seedBytes = seedBytes;
|
||||||
|
this.messageProperty = messageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected Void call() throws Exception {
|
||||||
|
if(seedBytes == null) {
|
||||||
|
throw new CardException("Failed to initialize Satochip - no seed provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(0, seedBytes);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignService extends Service<PSBT> {
|
||||||
|
private final Wallet wallet;
|
||||||
|
private final PSBT psbt;
|
||||||
|
private final StringProperty messageProperty;
|
||||||
|
|
||||||
|
public SignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||||
|
this.wallet = wallet;
|
||||||
|
this.psbt = psbt;
|
||||||
|
this.messageProperty = messageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<PSBT> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected PSBT call() throws Exception {
|
||||||
|
sign(wallet, psbt);
|
||||||
|
return psbt;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CardPSBTInputSigner implements PSBTInputSigner {
|
||||||
|
private final WalletNode signingNode;
|
||||||
|
private final String fullPath;
|
||||||
|
private ECKey pubkey;
|
||||||
|
|
||||||
|
// todo: provide derivationpath instead of WalletNode??
|
||||||
|
public CardPSBTInputSigner(WalletNode signingNode, String fullPath) {
|
||||||
|
this.signingNode = signingNode;
|
||||||
|
this.fullPath = fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
|
||||||
|
try {
|
||||||
|
// 2FA is optional, currently not supported in sparrow as it requires to send 2FA to a mobile app through a server.
|
||||||
|
SatoCardStatus cardStatus = getStatus();
|
||||||
|
if(cardStatus.needs2FA()) {
|
||||||
|
throw new CardException("Satochip 2FA is not (yet) supported within Sparrow");
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify PIN
|
||||||
|
APDUResponse rapdu0 = cardProtocol.cardVerifyPIN(0, pin);
|
||||||
|
|
||||||
|
// derive the correct key in satochip and recover pubkey
|
||||||
|
APDUResponse rapdu = cardProtocol.cardBip32GetExtendedKey(fullPath);
|
||||||
|
SatochipParser parser = new SatochipParser();
|
||||||
|
byte[][] extendeKeyBytes = parser.parseBip32GetExtendedKey(rapdu);
|
||||||
|
ECKey internalPubkey = ECKey.fromPublicOnly(extendeKeyBytes[0]);
|
||||||
|
|
||||||
|
if(signatureType == TransactionSignature.Type.ECDSA) {
|
||||||
|
// for ECDSA, pubkey is the same as internalPubkey
|
||||||
|
pubkey = internalPubkey;
|
||||||
|
// do the signature with satochip
|
||||||
|
byte keynbr = (byte) 0xFF;
|
||||||
|
byte[] chalresponse = null;
|
||||||
|
APDUResponse rapdu2 = cardProtocol.cardSignTransactionHash(keynbr, hash.getBytes(), chalresponse);
|
||||||
|
byte[] sigBytes = rapdu2.getData();
|
||||||
|
ECDSASignature ecdsaSig = ECDSASignature.decodeFromDER(sigBytes).toCanonicalised();
|
||||||
|
TransactionSignature txSig = new TransactionSignature(ecdsaSig, sigHash);
|
||||||
|
|
||||||
|
// verify
|
||||||
|
boolean isCorrect = pubkey.verify(hash, txSig);
|
||||||
|
return txSig;
|
||||||
|
} else {
|
||||||
|
// Satochip supports schnorr signature only for version >= 0.14 !
|
||||||
|
byte[] versionBytes = cardStatus.getCardVersion();
|
||||||
|
int protocolVersion = versionBytes[0] * 256 + versionBytes[1];
|
||||||
|
if(protocolVersion < (256 * 0 + 14)) {
|
||||||
|
throw new CardException(WalletModel.SATOCHIP.toDisplayString() + " (with version below v0.14) cannot sign Taproot transactions");
|
||||||
|
}
|
||||||
|
|
||||||
|
// tweak the bip32 key according to bip341
|
||||||
|
byte keynbr = (byte) 0xFF;
|
||||||
|
byte[] tweak = null;
|
||||||
|
APDUResponse rapduTweak = cardProtocol.cardTaprootTweakPrivkey(keynbr, tweak);
|
||||||
|
byte[] tweakedPubkeyBytes = new byte[65];
|
||||||
|
System.arraycopy(rapduTweak.getData(), 2, tweakedPubkeyBytes, 0, 65);
|
||||||
|
pubkey = ECKey.fromPublicOnly(tweakedPubkeyBytes);
|
||||||
|
|
||||||
|
byte[] chalresponse = null;
|
||||||
|
APDUResponse rapdu2 = cardProtocol.cardSignSchnorrHash(keynbr, hash.getBytes(), chalresponse);
|
||||||
|
byte[] sigBytes = rapdu2.getData();
|
||||||
|
SchnorrSignature schnorrSig = SchnorrSignature.decode(sigBytes);
|
||||||
|
TransactionSignature txSig = new TransactionSignature(schnorrSig, sigHash);
|
||||||
|
|
||||||
|
// verify sig with outputPubkey...
|
||||||
|
boolean isCorrect2 = pubkey.verify(hash, txSig);
|
||||||
|
|
||||||
|
return txSig; //new TransactionSignature(schnorrSig, sigHash);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ECKey getPubKey() {
|
||||||
|
return pubkey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SignMessageService extends Service<String> {
|
||||||
|
private final String message;
|
||||||
|
private final ScriptType scriptType;
|
||||||
|
private final List<ChildNumber> derivation;
|
||||||
|
private final StringProperty messageProperty;
|
||||||
|
|
||||||
|
public SignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||||
|
this.message = message;
|
||||||
|
this.scriptType = scriptType;
|
||||||
|
this.derivation = derivation;
|
||||||
|
this.messageProperty = messageProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<String> createTask() {
|
||||||
|
return new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected String call() throws Exception {
|
||||||
|
return signMessage(message, scriptType, derivation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the result of a GET STATUS command retrieving application status.
|
||||||
|
*/
|
||||||
|
public class SatoCardStatus {
|
||||||
|
private boolean setup_done = false;
|
||||||
|
private boolean is_seeded = false;
|
||||||
|
private boolean needs_secure_channel = false;
|
||||||
|
private boolean needs_2FA = false;
|
||||||
|
|
||||||
|
private byte protocol_major_version = (byte) 0;
|
||||||
|
private byte protocol_minor_version = (byte) 0;
|
||||||
|
private byte applet_major_version = (byte) 0;
|
||||||
|
private byte applet_minor_version = (byte) 0;
|
||||||
|
|
||||||
|
private byte PIN0_remaining_tries = (byte) 0;
|
||||||
|
private byte PUK0_remaining_tries = (byte) 0;
|
||||||
|
private byte PIN1_remaining_tries = (byte) 0;
|
||||||
|
private byte PUK1_remaining_tries = (byte) 0;
|
||||||
|
|
||||||
|
private int protocol_version = 0; //(d["protocol_major_version"]<<8)+d["protocol_minor_version"]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor from TLV data
|
||||||
|
*
|
||||||
|
* @param rapdu the TLV data
|
||||||
|
* @throws IllegalArgumentException if the TLV does not follow the expected format
|
||||||
|
*/
|
||||||
|
public SatoCardStatus(APDUResponse rapdu) {
|
||||||
|
int sw = rapdu.getSw();
|
||||||
|
|
||||||
|
if(sw == 0x9000) {
|
||||||
|
byte[] data = rapdu.getData();
|
||||||
|
protocol_major_version = data[0];
|
||||||
|
protocol_minor_version = data[1];
|
||||||
|
applet_major_version = data[2];
|
||||||
|
applet_minor_version = data[3];
|
||||||
|
protocol_version = (protocol_major_version << 8) + protocol_minor_version;
|
||||||
|
|
||||||
|
if(data.length >= 8) {
|
||||||
|
PIN0_remaining_tries = data[4];
|
||||||
|
PUK0_remaining_tries = data[5];
|
||||||
|
PIN1_remaining_tries = data[6];
|
||||||
|
PUK1_remaining_tries = data[7];
|
||||||
|
needs_2FA = false; //default value
|
||||||
|
}
|
||||||
|
if(data.length >= 9) {
|
||||||
|
needs_2FA = data[8] != 0X00;
|
||||||
|
}
|
||||||
|
if(data.length >= 10) {
|
||||||
|
is_seeded = data[9] != 0X00;
|
||||||
|
}
|
||||||
|
if(data.length >= 11) {
|
||||||
|
setup_done = data[10] != 0X00;
|
||||||
|
} else {
|
||||||
|
setup_done = true;
|
||||||
|
}
|
||||||
|
if(data.length >= 12) {
|
||||||
|
needs_secure_channel = data[11] != 0X00;
|
||||||
|
} else {
|
||||||
|
needs_secure_channel = false;
|
||||||
|
needs_2FA = false; //default value
|
||||||
|
}
|
||||||
|
} else if(sw == 0x9c04) {
|
||||||
|
setup_done = false;
|
||||||
|
is_seeded = false;
|
||||||
|
needs_secure_channel = false;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Invalid getStatus data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getters
|
||||||
|
public boolean isSeeded() {
|
||||||
|
return is_seeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSetupDone() {
|
||||||
|
return setup_done;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInitialized() {
|
||||||
|
return (setup_done && is_seeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsSecureChannel() {
|
||||||
|
return needs_secure_channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needs2FA() {
|
||||||
|
return needs_2FA;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getPin0RemainingCounter() {
|
||||||
|
return PIN0_remaining_tries;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getCardVersion() {
|
||||||
|
byte[] versionBytes = new byte[4];
|
||||||
|
versionBytes[0] = protocol_major_version;
|
||||||
|
versionBytes[1] = protocol_minor_version;
|
||||||
|
versionBytes[2] = applet_major_version;
|
||||||
|
versionBytes[3] = applet_minor_version;
|
||||||
|
return versionBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "setup_done: " + setup_done + "\n" +
|
||||||
|
"is_seeded: " + is_seeded + "\n" +
|
||||||
|
"needs_2FA: " + needs_2FA + "\n" +
|
||||||
|
"needs_secure_channel: " + needs_secure_channel + "\n" +
|
||||||
|
"protocol_major_version: " + protocol_major_version + "\n" +
|
||||||
|
"protocol_minor_version: " + protocol_minor_version + "\n" +
|
||||||
|
"applet_major_version: " + applet_major_version + "\n" +
|
||||||
|
"applet_minor_version: " + applet_minor_version;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.smartcardio.*;
|
||||||
|
import javax.smartcardio.CardChannel;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class SatoCardTransport {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SatoCardTransport.class);
|
||||||
|
|
||||||
|
private final Card connection;
|
||||||
|
|
||||||
|
SatoCardTransport(byte[] appletAid) throws CardException {
|
||||||
|
TerminalFactory tf = TerminalFactory.getDefault();
|
||||||
|
List<CardTerminal> terminals = tf.terminals().list();
|
||||||
|
if(terminals.isEmpty()) {
|
||||||
|
throw new IllegalStateException("No reader connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
Card connection = null;
|
||||||
|
for(Iterator<CardTerminal> iter = terminals.iterator(); iter.hasNext(); ) {
|
||||||
|
try {
|
||||||
|
connection = getConnection(iter.next(), appletAid);
|
||||||
|
break;
|
||||||
|
} catch(CardException e) {
|
||||||
|
if(!iter.hasNext()) {
|
||||||
|
log.info(e.getMessage());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Card getConnection(CardTerminal cardTerminal, byte[] appletAid) throws CardException {
|
||||||
|
Card connection = cardTerminal.connect("*");
|
||||||
|
|
||||||
|
CardChannel cardChannel = connection.getBasicChannel();
|
||||||
|
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, appletAid));
|
||||||
|
if(resp.getSW() != APDUResponse.SW_OK) {
|
||||||
|
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUResponse send(APDUCommand capdu) throws CardException {
|
||||||
|
javax.smartcardio.CardChannel cardChannel = this.connection.getBasicChannel();
|
||||||
|
|
||||||
|
CommandAPDU cmd = new CommandAPDU(capdu.getCla(), capdu.getIns(), capdu.getP1(), capdu.getP2(), capdu.getData());
|
||||||
|
ResponseAPDU resp = cardChannel.transmit(cmd);
|
||||||
|
|
||||||
|
return new APDUResponse(resp.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect() throws CardException {
|
||||||
|
connection.disconnect(true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
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.StringProperty;
|
||||||
|
|
||||||
|
import javax.smartcardio.CardException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class Satochip implements KeystoreCardImport {
|
||||||
|
@Override
|
||||||
|
public boolean isInitialized() throws CardException {
|
||||||
|
SatoCardApi cardApi = null;
|
||||||
|
try {
|
||||||
|
cardApi = new SatoCardApi(WalletModel.SATOCHIP, null);
|
||||||
|
return cardApi.isInitialized();
|
||||||
|
} finally {
|
||||||
|
if(cardApi != null) {
|
||||||
|
cardApi.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initialize(String pin, byte[] entropy, StringProperty messageProperty) throws CardException {
|
||||||
|
if(pin.length() < 4) {
|
||||||
|
throw new CardException("PIN too short.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pin.length() > 16) {
|
||||||
|
throw new CardException("PIN too long.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SatoCardApi cardApi = null;
|
||||||
|
try {
|
||||||
|
cardApi = new SatoCardApi(WalletModel.SATOCHIP, pin);
|
||||||
|
SatoCardStatus cardStatus = cardApi.getStatus();
|
||||||
|
if(cardStatus.isInitialized()) {
|
||||||
|
throw new IllegalStateException("Card is already initialized.");
|
||||||
|
}
|
||||||
|
|
||||||
|
cardApi.initialize(0, entropy);
|
||||||
|
} finally {
|
||||||
|
if(cardApi != null) {
|
||||||
|
cardApi.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
|
||||||
|
if(pin.length() < 4) {
|
||||||
|
throw new ImportException("PIN too short.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(pin.length() > 16) {
|
||||||
|
throw new ImportException("PIN too long.");
|
||||||
|
}
|
||||||
|
|
||||||
|
SatoCardApi cardApi = null;
|
||||||
|
try {
|
||||||
|
cardApi = new SatoCardApi(WalletModel.SATOCHIP, pin);
|
||||||
|
SatoCardStatus cardStatus = cardApi.getStatus();
|
||||||
|
if(!cardStatus.isInitialized()) {
|
||||||
|
throw new IllegalStateException("Card is not initialized.");
|
||||||
|
}
|
||||||
|
cardApi.setDerivation(derivation);
|
||||||
|
return cardApi.getKeystore();
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new ImportException(e);
|
||||||
|
} finally {
|
||||||
|
if(cardApi != null) {
|
||||||
|
cardApi.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getKeystoreImportDescription(int account) {
|
||||||
|
return "Import the keystore from your Satochip by inserting or placing it on the card reader.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "Satochip";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WalletModel getWalletModel() {
|
||||||
|
return WalletModel.SATOCHIP;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,488 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.ExtendedKey;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.protocol.Base58;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
|
import static com.sparrowwallet.sparrow.io.satochip.Constants.*;
|
||||||
|
|
||||||
|
import javax.smartcardio.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to send APDU to the applet. Each method corresponds to an APDU as defined in the APPLICATION.md
|
||||||
|
* file. Some APDUs map to multiple methods for the sake of convenience since their payload or response require some
|
||||||
|
* pre/post processing.
|
||||||
|
*/
|
||||||
|
public class SatochipCommandSet {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SatochipCommandSet.class);
|
||||||
|
|
||||||
|
private final SatoCardTransport cardTransport;
|
||||||
|
private final SecureChannelSession secureChannel;
|
||||||
|
private SatoCardStatus status;
|
||||||
|
private SatochipParser parser = null;
|
||||||
|
|
||||||
|
private String pinCached = null;
|
||||||
|
|
||||||
|
public static final byte[] SATOCHIP_AID = Utils.hexToBytes("5361746f43686970"); //SatoChip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SatochipCommandSet using the given APDU Channel
|
||||||
|
*/
|
||||||
|
public SatochipCommandSet() throws CardException {
|
||||||
|
this.cardTransport = new SatoCardTransport(SATOCHIP_AID);
|
||||||
|
this.secureChannel = new SecureChannelSession();
|
||||||
|
this.parser = new SatochipParser();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the application info as stored from the last sent SELECT command. Returns null if no succesful SELECT
|
||||||
|
* command has been sent using this command set.
|
||||||
|
*
|
||||||
|
* @return the application info object
|
||||||
|
*/
|
||||||
|
public SatoCardStatus getApplicationStatus() {
|
||||||
|
if(this.status == null) {
|
||||||
|
this.cardGetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* AUTHENTIKEY *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public APDUResponse cardTransmit(APDUCommand plainApdu) {
|
||||||
|
// we try to transmit the APDU until we receive the answer or we receive an unrecoverable error
|
||||||
|
boolean isApduTransmitted = false;
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
byte[] apduBytes = plainApdu.serialize();
|
||||||
|
byte ins = apduBytes[1];
|
||||||
|
boolean isEncrypted = false;
|
||||||
|
|
||||||
|
// check if status available
|
||||||
|
if(status == null) {
|
||||||
|
APDUCommand statusCapdu = new APDUCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
|
||||||
|
APDUResponse statusRapdu = this.cardTransport.send(statusCapdu);
|
||||||
|
status = new SatoCardStatus(statusRapdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUCommand capdu = plainApdu;
|
||||||
|
if(status.needsSecureChannel() && (ins != 0xA4) && (ins != 0x81) && (ins != 0x82) && (ins != INS_GET_STATUS)) {
|
||||||
|
if(!secureChannel.initializedSecureChannel()) {
|
||||||
|
// get card's public key
|
||||||
|
APDUResponse secChannelRapdu = this.cardInitiateSecureChannel();
|
||||||
|
byte[] pubkey = this.parser.parseInitiateSecureChannel(secChannelRapdu);
|
||||||
|
// setup secure channel
|
||||||
|
this.secureChannel.initiateSecureChannel(pubkey);
|
||||||
|
}
|
||||||
|
// encrypt apdu
|
||||||
|
capdu = secureChannel.encryptSecureChannel(plainApdu);
|
||||||
|
isEncrypted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUResponse rapdu = this.cardTransport.send(capdu);
|
||||||
|
int sw12 = rapdu.getSw();
|
||||||
|
|
||||||
|
// check answer
|
||||||
|
if(sw12 == 0x9000) { // ok!
|
||||||
|
if(isEncrypted) {
|
||||||
|
// decrypt
|
||||||
|
rapdu = secureChannel.decryptSecureChannel(rapdu);
|
||||||
|
}
|
||||||
|
isApduTransmitted = true; // leave loop
|
||||||
|
return rapdu;
|
||||||
|
}
|
||||||
|
// PIN authentication is required
|
||||||
|
else if(sw12 == 0x9C06) {
|
||||||
|
//cardVerifyPIN();
|
||||||
|
log.error("Error, Satochip PIN required");
|
||||||
|
throw new CardAuthorizationException("PIN is required");
|
||||||
|
}
|
||||||
|
// SecureChannel is not initialized
|
||||||
|
else if(sw12 == 0x9C21) {
|
||||||
|
log.error("Error, Satochip secure channel required");
|
||||||
|
secureChannel.resetSecureChannel();
|
||||||
|
} else {
|
||||||
|
// cannot resolve issue at this point
|
||||||
|
isApduTransmitted = true; // leave loop
|
||||||
|
return rapdu;
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("Error transmitting Satochip command set" + e);
|
||||||
|
return new APDUResponse(new byte[0], (byte) 0x00, (byte) 0x00); // return empty APDUResponse
|
||||||
|
}
|
||||||
|
} while(!isApduTransmitted);
|
||||||
|
|
||||||
|
return new APDUResponse(new byte[0], (byte) 0x00, (byte) 0x00); // should not happen
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cardDisconnect() {
|
||||||
|
secureChannel.resetSecureChannel();
|
||||||
|
status = null;
|
||||||
|
pinCached = null;
|
||||||
|
try {
|
||||||
|
cardTransport.disconnect();
|
||||||
|
} catch(CardException e) {
|
||||||
|
log.error("Error disconnecting Satochip" + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cardGetStatus() {
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
|
||||||
|
APDUResponse respApdu = this.cardTransmit(plainApdu);
|
||||||
|
this.status = new SatoCardStatus(respApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse cardInitiateSecureChannel() throws CardException {
|
||||||
|
byte[] pubkey = secureChannel.getPublicKey();
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, INS_INIT_SECURE_CHANNEL, 0x00, 0x00, pubkey);
|
||||||
|
|
||||||
|
return this.cardTransport.send(plainApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* CARD MGMT *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public APDUResponse cardSetup(byte pin_tries0, byte[] pin0) {
|
||||||
|
// use random values for pin1, ublk0, ublk1
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
byte[] ublk0 = new byte[8];
|
||||||
|
byte[] ublk1 = new byte[8];
|
||||||
|
byte[] pin1 = new byte[8];
|
||||||
|
random.nextBytes(ublk0);
|
||||||
|
random.nextBytes(ublk1);
|
||||||
|
random.nextBytes(pin1);
|
||||||
|
|
||||||
|
byte ublk_tries0 = (byte) 0x01;
|
||||||
|
byte ublk_tries1 = (byte) 0x01;
|
||||||
|
byte pin_tries1 = (byte) 0x01;
|
||||||
|
|
||||||
|
return cardSetup(pin_tries0, ublk_tries0, pin0, ublk0, pin_tries1, ublk_tries1, pin1, ublk1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse cardSetup(byte pin_tries0, byte ublk_tries0, byte[] pin0, byte[] ublk0,
|
||||||
|
byte pin_tries1, byte ublk_tries1, byte[] pin1, byte[] ublk1) {
|
||||||
|
|
||||||
|
byte[] pin = {0x4D, 0x75, 0x73, 0x63, 0x6C, 0x65, 0x30, 0x30}; //default pin
|
||||||
|
byte cla = (byte) 0xB0;
|
||||||
|
byte ins = INS_SETUP;
|
||||||
|
byte p1 = 0;
|
||||||
|
byte p2 = 0;
|
||||||
|
|
||||||
|
// data=[pin_length(1) | pin |
|
||||||
|
// pin_tries0(1) | ublk_tries0(1) | pin0_length(1) | pin0 | ublk0_length(1) | ublk0 |
|
||||||
|
// pin_tries1(1) | ublk_tries1(1) | pin1_length(1) | pin1 | ublk1_length(1) | ublk1 |
|
||||||
|
// memsize(2) | memsize2(2) | ACL(3) |
|
||||||
|
// option_flags(2) | hmacsha160_key(20) | amount_limit(8)]
|
||||||
|
int optionsize = 0;
|
||||||
|
int option_flags = 0; // do not use option (mostly deprecated)
|
||||||
|
int offset = 0;
|
||||||
|
int datasize = 16 + pin.length + pin0.length + pin1.length + ublk0.length + ublk1.length + optionsize;
|
||||||
|
byte[] data = new byte[datasize];
|
||||||
|
|
||||||
|
data[offset++] = (byte) pin.length;
|
||||||
|
System.arraycopy(pin, 0, data, offset, pin.length);
|
||||||
|
offset += pin.length;
|
||||||
|
// pin0 & ublk0
|
||||||
|
data[offset++] = pin_tries0;
|
||||||
|
data[offset++] = ublk_tries0;
|
||||||
|
data[offset++] = (byte) pin0.length;
|
||||||
|
System.arraycopy(pin0, 0, data, offset, pin0.length);
|
||||||
|
offset += pin0.length;
|
||||||
|
data[offset++] = (byte) ublk0.length;
|
||||||
|
System.arraycopy(ublk0, 0, data, offset, ublk0.length);
|
||||||
|
offset += ublk0.length;
|
||||||
|
// pin1 & ublk1
|
||||||
|
data[offset++] = pin_tries1;
|
||||||
|
data[offset++] = ublk_tries1;
|
||||||
|
data[offset++] = (byte) pin1.length;
|
||||||
|
System.arraycopy(pin1, 0, data, offset, pin1.length);
|
||||||
|
offset += pin1.length;
|
||||||
|
data[offset++] = (byte) ublk1.length;
|
||||||
|
System.arraycopy(ublk1, 0, data, offset, ublk1.length);
|
||||||
|
offset += ublk1.length;
|
||||||
|
|
||||||
|
// memsize default (deprecated)
|
||||||
|
data[offset++] = (byte) 00;
|
||||||
|
data[offset++] = (byte) 32;
|
||||||
|
data[offset++] = (byte) 00;
|
||||||
|
data[offset++] = (byte) 32;
|
||||||
|
|
||||||
|
// ACL (deprecated)
|
||||||
|
data[offset++] = (byte) 0x01;
|
||||||
|
data[offset++] = (byte) 0x01;
|
||||||
|
data[offset++] = (byte) 0x01;
|
||||||
|
|
||||||
|
APDUCommand plainApdu = new APDUCommand(cla, ins, p1, p2, data);
|
||||||
|
APDUResponse respApdu = this.cardTransmit(plainApdu);
|
||||||
|
|
||||||
|
if(respApdu.getSw() == 0x9000) {
|
||||||
|
//setPin0(pin0); // todo: cache value...
|
||||||
|
} else {
|
||||||
|
log.error("Error " + respApdu.toHexString());
|
||||||
|
}
|
||||||
|
|
||||||
|
return respApdu;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* PIN MGMT *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public APDUResponse cardVerifyPIN() throws CardException {
|
||||||
|
return this.cardVerifyPIN((byte) 0, pinCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse cardVerifyPIN(int pinNbr, String pin) throws CardException {
|
||||||
|
if(pin == null) {
|
||||||
|
if(pinCached == null) {
|
||||||
|
throw new CardException("PIN required!");
|
||||||
|
}
|
||||||
|
pin = this.pinCached;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if(pinBytes.length > 16) {
|
||||||
|
throw new CardException("PIN should be maximum 16 characters!");
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUCommand capdu = new APDUCommand(0xB0, INS_VERIFY_PIN, (byte) pinNbr, 0x00, pinBytes);
|
||||||
|
APDUResponse rapdu = this.cardTransmit(capdu);
|
||||||
|
|
||||||
|
// correct PIN: cache PIN value
|
||||||
|
int sw = rapdu.getSw();
|
||||||
|
if(sw == 0x9000) {
|
||||||
|
this.pinCached = pin; //set cached PIN value
|
||||||
|
}
|
||||||
|
// wrong PIN, get remaining tries available (since v0.11)
|
||||||
|
else if((sw & 0xffc0) == 0x63c0) {
|
||||||
|
this.pinCached = null; //reset cached PIN value
|
||||||
|
int pinLeft = (sw & ~0xffc0);
|
||||||
|
throw new CardAuthorizationException("Wrong PIN, remaining tries: " + pinLeft);
|
||||||
|
}
|
||||||
|
// wrong PIN (legacy before v0.11)
|
||||||
|
else if(sw == 0x9c02) {
|
||||||
|
this.pinCached = null; //reset cached PIN value
|
||||||
|
SatoCardStatus cardStatus = this.getApplicationStatus();
|
||||||
|
int pinLeft = cardStatus.getPin0RemainingCounter();
|
||||||
|
throw new CardAuthorizationException("Wrong PIN, remaining tries: " + pinLeft);
|
||||||
|
}
|
||||||
|
// blocked PIN
|
||||||
|
else if(sw == 0x9c0c) {
|
||||||
|
throw new CardException("Card is blocked!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return rapdu;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cardChangePIN(int pinNbr, String oldPin, String newPin) throws CardException {
|
||||||
|
byte[] oldPinBytes = oldPin.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] newPinBytes = newPin.getBytes(StandardCharsets.UTF_8);
|
||||||
|
int lc = 1 + oldPinBytes.length + 1 + newPinBytes.length;
|
||||||
|
byte[] data = new byte[lc];
|
||||||
|
|
||||||
|
data[0] = (byte) oldPinBytes.length;
|
||||||
|
int offset = 1;
|
||||||
|
System.arraycopy(oldPinBytes, 0, data, offset, oldPinBytes.length);
|
||||||
|
offset += oldPinBytes.length;
|
||||||
|
data[offset] = (byte) newPinBytes.length;
|
||||||
|
offset += 1;
|
||||||
|
System.arraycopy(newPinBytes, 0, data, offset, newPinBytes.length);
|
||||||
|
|
||||||
|
APDUCommand capdu = new APDUCommand(0xB0, INS_CHANGE_PIN, (byte) pinNbr, 0x00, data);
|
||||||
|
APDUResponse rapdu = this.cardTransmit(capdu);
|
||||||
|
|
||||||
|
// correct PIN: cache PIN value
|
||||||
|
int sw = rapdu.getSw();
|
||||||
|
if(sw == 0x9000) {
|
||||||
|
this.pinCached = newPin;
|
||||||
|
}
|
||||||
|
// wrong PIN, get remaining tries available (since v0.11)
|
||||||
|
else if((sw & 0xffc0) == 0x63c0) {
|
||||||
|
int pinLeft = (sw & ~0xffc0);
|
||||||
|
throw new CardAuthorizationException("Wrong PIN, remaining tries: " + pinLeft);
|
||||||
|
}
|
||||||
|
// wrong PIN (legacy before v0.11)
|
||||||
|
else if(sw == 0x9c02) {
|
||||||
|
SatoCardStatus cardStatus = this.getApplicationStatus();
|
||||||
|
int pinLeft = cardStatus.getPin0RemainingCounter();
|
||||||
|
throw new CardAuthorizationException("Wrong PIN, remaining tries: " + pinLeft);
|
||||||
|
}
|
||||||
|
// blocked PIN
|
||||||
|
else if(sw == 0x9c0c) {
|
||||||
|
throw new CardException("Card is blocked!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* BIP32 *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public APDUResponse cardBip32ImportSeed(byte[] masterseed) {
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_IMPORT_SEED, masterseed.length, 0x00, masterseed);
|
||||||
|
return this.cardTransmit(plainApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse cardBip32GetExtendedKey(String stringPath) {
|
||||||
|
KeyPath keyPath = new KeyPath(stringPath);
|
||||||
|
byte[] bytePath = keyPath.getData();
|
||||||
|
return cardBip32GetExtendedKey(bytePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse cardBip32GetExtendedKey(byte[] bytePath) {
|
||||||
|
byte p1 = (byte) (bytePath.length / 4);
|
||||||
|
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
|
||||||
|
return this.cardTransmit(plainApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the BIP32 xpub for given path.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* path (str): the path; if given as a string, it will be converted to bytes (4 bytes for each path index)
|
||||||
|
* xtype (str): the type of transaction such as 'standard', 'p2wpkh-p2sh', 'p2wpkh', 'p2wsh-p2sh', 'p2wsh'
|
||||||
|
* is_mainnet (bool): is mainnet or testnet
|
||||||
|
*
|
||||||
|
* Return:
|
||||||
|
* xpub (str): the corresponding xpub value
|
||||||
|
*/
|
||||||
|
public String cardBip32GetXpub(String stringPath, ExtendedKey.Header xtype) throws CardException {
|
||||||
|
// path is of the form 44'/0'/1'
|
||||||
|
KeyPath keyPath = new KeyPath(stringPath);
|
||||||
|
byte[] bytePath = keyPath.getData();
|
||||||
|
int depth = bytePath.length / 4;
|
||||||
|
|
||||||
|
APDUResponse rapdu = this.cardBip32GetExtendedKey(bytePath);
|
||||||
|
byte[][] extendedkey = this.parser.parseBip32GetExtendedKey(rapdu);
|
||||||
|
|
||||||
|
byte[] fingerprint = new byte[4];
|
||||||
|
byte[] childNumber = new byte[4];
|
||||||
|
if(depth == 0) { //masterkey
|
||||||
|
// fingerprint and childnumber set to all-zero bytes by default
|
||||||
|
//fingerprint= bytes([0,0,0,0])
|
||||||
|
//childNumber= bytes([0,0,0,0])
|
||||||
|
} else { //get parent info
|
||||||
|
byte[] bytePathParent = Arrays.copyOfRange(bytePath, 0, bytePath.length - 4);
|
||||||
|
APDUResponse rapdu2 = this.cardBip32GetExtendedKey(bytePathParent);
|
||||||
|
byte[][] extendedkeyParent = this.parser.parseBip32GetExtendedKey(rapdu2);
|
||||||
|
byte[] identifier = Utils.sha256hash160(extendedkeyParent[0]);
|
||||||
|
fingerprint = Arrays.copyOfRange(identifier, 0, 4);
|
||||||
|
childNumber = Arrays.copyOfRange(bytePath, bytePath.length - 4, bytePath.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(78);
|
||||||
|
buffer.putInt(xtype.getHeader());
|
||||||
|
buffer.put((byte) depth);
|
||||||
|
buffer.put(fingerprint);
|
||||||
|
buffer.put(childNumber);
|
||||||
|
buffer.put(extendedkey[1]); // chaincode
|
||||||
|
buffer.put(extendedkey[0]); // pubkey (compressed)
|
||||||
|
byte[] xpubByte = buffer.array();
|
||||||
|
|
||||||
|
return Base58.encodeChecked(xpubByte);
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* SIGNATURES *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public APDUResponse cardSignTransactionHash(byte keynbr, byte[] txhash, byte[] chalresponse) throws CardException {
|
||||||
|
byte[] data;
|
||||||
|
if(txhash.length != 32) {
|
||||||
|
throw new CardException("Wrong txhash length (should be 32)");
|
||||||
|
}
|
||||||
|
if(chalresponse == null) {
|
||||||
|
data = new byte[32];
|
||||||
|
System.arraycopy(txhash, 0, data, 0, txhash.length);
|
||||||
|
} else if(chalresponse.length == 20) {
|
||||||
|
data = new byte[32 + 2 + 20];
|
||||||
|
int offset = 0;
|
||||||
|
System.arraycopy(txhash, 0, data, offset, txhash.length);
|
||||||
|
offset += 32;
|
||||||
|
data[offset++] = (byte) 0x80; // 2 middle bytes for 2FA flag
|
||||||
|
data[offset++] = (byte) 0x00;
|
||||||
|
System.arraycopy(chalresponse, 0, data, offset, chalresponse.length);
|
||||||
|
} else {
|
||||||
|
throw new CardException("Wrong challenge-response length (should be 20)");
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, INS_SIGN_TRANSACTION_HASH, keynbr, 0x00, data);
|
||||||
|
return this.cardTransmit(plainApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function signs a given hash with a std or the last extended key
|
||||||
|
* If 2FA is enabled, a HMAC must be provided as an additional security layer. *
|
||||||
|
* ins: 0x7B
|
||||||
|
* p1: key number or 0xFF for the last derived Bip32 extended key
|
||||||
|
* p2: 0x00
|
||||||
|
* data: [hash(32b) | option: 2FA-flag(2b)|hmac(20b)]
|
||||||
|
* return: [sig]
|
||||||
|
*/
|
||||||
|
public APDUResponse cardSignSchnorrHash(byte keynbr, byte[] txhash, byte[] chalresponse) throws CardException {
|
||||||
|
byte[] data;
|
||||||
|
if(txhash.length != 32) {
|
||||||
|
throw new CardException("Wrong txhash length (should be 32)");
|
||||||
|
}
|
||||||
|
if(chalresponse == null) {
|
||||||
|
data = new byte[32];
|
||||||
|
System.arraycopy(txhash, 0, data, 0, txhash.length);
|
||||||
|
} else if(chalresponse.length == 20) {
|
||||||
|
data = new byte[32 + 2 + 20];
|
||||||
|
int offset = 0;
|
||||||
|
System.arraycopy(txhash, 0, data, offset, txhash.length);
|
||||||
|
offset += 32;
|
||||||
|
data[offset++] = (byte) 0x80; // 2 middle bytes for 2FA flag
|
||||||
|
data[offset++] = (byte) 0x00;
|
||||||
|
System.arraycopy(chalresponse, 0, data, offset, chalresponse.length);
|
||||||
|
} else {
|
||||||
|
throw new CardException("Wrong challenge-response length (should be 20)");
|
||||||
|
}
|
||||||
|
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, 0x7B, keynbr, 0x00, data);
|
||||||
|
return this.cardTransmit(plainApdu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function tweak the currently available private stored in the Satochip.
|
||||||
|
* Tweaking is based on the 'taproot_tweak_seckey(seckey0, h)' algorithm specification defined here:
|
||||||
|
* https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
|
||||||
|
* <p>
|
||||||
|
* ins: 0x7C
|
||||||
|
* p1: key number or 0xFF for the last derived Bip32 extended key
|
||||||
|
* p2: 0x00
|
||||||
|
* data: [hash(32b) | option: 2FA-flag(2b)|hmac(20b)]
|
||||||
|
* return: [sig]
|
||||||
|
*/
|
||||||
|
public APDUResponse cardTaprootTweakPrivkey(byte keynbr, byte[] tweak) throws CardException {
|
||||||
|
byte[] data;
|
||||||
|
if(tweak == null) {
|
||||||
|
tweak = new byte[32]; // by default use a 32-byte vector filled with '0x00'
|
||||||
|
}
|
||||||
|
if(tweak.length != 32) {
|
||||||
|
throw new CardException("Wrong tweak length (should be 32)");
|
||||||
|
}
|
||||||
|
data = new byte[33];
|
||||||
|
data[0] = (byte) 32;
|
||||||
|
System.arraycopy(tweak, 0, data, 1, tweak.length);
|
||||||
|
|
||||||
|
APDUCommand plainApdu = new APDUCommand(0xB0, 0x7C, keynbr, 0x00, data);
|
||||||
|
return this.cardTransmit(plainApdu);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.smartcardio.CardException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class SatochipParser {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SatochipParser.class);
|
||||||
|
|
||||||
|
public SatochipParser() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* PARSER *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public byte[] parseInitiateSecureChannel(APDUResponse rapdu) throws CardException {
|
||||||
|
try {
|
||||||
|
byte[] data = rapdu.getData();
|
||||||
|
|
||||||
|
// data= [coordxSize | coordx | sig1Size | sig1 | sig2Size | sig2]
|
||||||
|
int offset = 0;
|
||||||
|
int coordxSize = 256 * data[offset++] + data[offset++];
|
||||||
|
|
||||||
|
byte[] coordx = new byte[coordxSize];
|
||||||
|
System.arraycopy(data, offset, coordx, 0, coordxSize);
|
||||||
|
offset += coordxSize;
|
||||||
|
|
||||||
|
// msg1 is [coordx_size | coordx]
|
||||||
|
byte[] msg1 = new byte[2 + coordxSize];
|
||||||
|
System.arraycopy(data, 0, msg1, 0, msg1.length);
|
||||||
|
|
||||||
|
int sig1Size = 256 * data[offset++] + data[offset++];
|
||||||
|
byte[] sig1 = new byte[sig1Size];
|
||||||
|
System.arraycopy(data, offset, sig1, 0, sig1Size);
|
||||||
|
offset += sig1Size;
|
||||||
|
|
||||||
|
// msg2 is [coordxSize | coordx | sig1Size | sig1]
|
||||||
|
byte[] msg2 = new byte[2 + coordxSize + 2 + sig1Size];
|
||||||
|
System.arraycopy(data, 0, msg2, 0, msg2.length);
|
||||||
|
|
||||||
|
int sig2Size = 256 * data[offset++] + data[offset++];
|
||||||
|
byte[] sig2 = new byte[sig2Size];
|
||||||
|
System.arraycopy(data, offset, sig2, 0, sig2Size);
|
||||||
|
offset += sig2Size;
|
||||||
|
|
||||||
|
return recoverPubkey(msg1, sig1, coordx, false);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new CardException("Error parsing Satochip response", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[][] parseBip32GetExtendedKey(APDUResponse rapdu) throws CardException {
|
||||||
|
try {
|
||||||
|
byte[][] extendedkey = new byte[2][];
|
||||||
|
extendedkey[0] = new byte[33]; // pubkey
|
||||||
|
extendedkey[1] = new byte[32]; // chaincode
|
||||||
|
|
||||||
|
byte[] data = rapdu.getData();
|
||||||
|
//data: [chaincode(32b) | coordx_size(2b) | coordx | sig_size(2b) | sig | sig_size(2b) | sig2]
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
byte[] chaincode = new byte[32];
|
||||||
|
System.arraycopy(data, offset, chaincode, 0, chaincode.length);
|
||||||
|
offset += 32;
|
||||||
|
|
||||||
|
int coordxSize = 256 * (data[offset++] & 0x7f) + data[offset++]; // (data[32] & 0x80) is ignored (optimization flag)
|
||||||
|
byte[] coordx = new byte[coordxSize];
|
||||||
|
System.arraycopy(data, offset, coordx, 0, coordxSize);
|
||||||
|
offset += coordxSize;
|
||||||
|
|
||||||
|
// msg1 is [chaincode | coordx_size | coordx]
|
||||||
|
byte[] msg1 = new byte[32 + 2 + coordxSize];
|
||||||
|
System.arraycopy(data, 0, msg1, 0, msg1.length);
|
||||||
|
|
||||||
|
int sig1Size = 256 * data[offset++] + data[offset++];
|
||||||
|
byte[] sig1 = new byte[sig1Size];
|
||||||
|
System.arraycopy(data, offset, sig1, 0, sig1Size);
|
||||||
|
offset += sig1Size;
|
||||||
|
|
||||||
|
// msg2 is [chaincode | coordxSize | coordx | sig1Size | sig1]
|
||||||
|
byte[] msg2 = new byte[32 + 2 + coordxSize + 2 + sig1Size];
|
||||||
|
System.arraycopy(data, 0, msg2, 0, msg2.length);
|
||||||
|
|
||||||
|
int sig2Size = 256 * data[offset++] + data[offset++];
|
||||||
|
byte[] sig2 = new byte[sig2Size];
|
||||||
|
System.arraycopy(data, offset, sig2, 0, sig2Size);
|
||||||
|
offset += sig2Size;
|
||||||
|
|
||||||
|
byte[] pubkey = recoverPubkey(msg1, sig1, coordx, true); // true: compressed (33 bytes)
|
||||||
|
|
||||||
|
// todo: recover from si2
|
||||||
|
System.arraycopy(pubkey, 0, extendedkey[0], 0, pubkey.length);
|
||||||
|
System.arraycopy(chaincode, 0, extendedkey[1], 0, chaincode.length);
|
||||||
|
return extendedkey;
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new CardException("Error parsing Satochip extended key", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/****************************************
|
||||||
|
* recovery methods *
|
||||||
|
****************************************/
|
||||||
|
|
||||||
|
public byte[] recoverPubkey(byte[] msg, byte[] dersig, byte[] coordx, Boolean compressed) throws CardException {
|
||||||
|
// convert msg to hash
|
||||||
|
//byte[] hash = Sha256Hash.hash(msg);
|
||||||
|
ECDSASignature ecdsaSig = ECDSASignature.decodeFromDER(dersig);
|
||||||
|
|
||||||
|
byte recId = -1;
|
||||||
|
ECKey k = null;
|
||||||
|
for(byte i = 0; i < 4; i++) {
|
||||||
|
k = ECKey.recoverFromSignature(i, ecdsaSig, Sha256Hash.of(msg), compressed);
|
||||||
|
if(k != null && Arrays.equals(k.getPubKeyXCoord(), coordx)) {
|
||||||
|
recId = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(recId == -1) {
|
||||||
|
throw new CardException("Could not construct a recoverable key. This should never happen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return k.getPubKey();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package com.sparrowwallet.sparrow.io.satochip;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.crypto.AESKeyCrypter;
|
||||||
|
|
||||||
|
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||||
|
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||||
|
import com.sparrowwallet.drongo.crypto.Key;
|
||||||
|
import com.sparrowwallet.drongo.crypto.EncryptedData;
|
||||||
|
|
||||||
|
import javax.crypto.Mac;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import javax.smartcardio.CardException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a SecureChannel session with the card.
|
||||||
|
*/
|
||||||
|
public class SecureChannelSession {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(SecureChannelSession.class);
|
||||||
|
|
||||||
|
public static final int SC_SECRET_LENGTH = 16;
|
||||||
|
public static final int SC_BLOCK_SIZE = 16;
|
||||||
|
public static final int IV_SIZE = 16;
|
||||||
|
public static final int MAC_SIZE = 20;
|
||||||
|
|
||||||
|
// secure channel constants
|
||||||
|
private final static byte INS_INIT_SECURE_CHANNEL = (byte) 0x81;
|
||||||
|
private final static byte INS_PROCESS_SECURE_CHANNEL = (byte) 0x82;
|
||||||
|
private final static short SW_SECURE_CHANNEL_REQUIRED = (short) 0x9C20;
|
||||||
|
private final static short SW_SECURE_CHANNEL_UNINITIALIZED = (short) 0x9C21;
|
||||||
|
private final static short SW_SECURE_CHANNEL_WRONG_IV = (short) 0x9C22;
|
||||||
|
private final static short SW_SECURE_CHANNEL_WRONG_MAC = (short) 0x9C23;
|
||||||
|
|
||||||
|
private boolean initialized_secure_channel = false;
|
||||||
|
|
||||||
|
// secure channel keys
|
||||||
|
private byte[] secret;
|
||||||
|
private byte[] iv;
|
||||||
|
private int ivCounter;
|
||||||
|
byte[] derived_key;
|
||||||
|
byte[] mac_key;
|
||||||
|
|
||||||
|
// for ECDH
|
||||||
|
private SecretPoint secretPoint;
|
||||||
|
private final ECKey eckey;
|
||||||
|
|
||||||
|
// for session encryption
|
||||||
|
private final SecureRandom random;
|
||||||
|
private final AESKeyCrypter aesCipher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a SecureChannel session on the client.
|
||||||
|
*/
|
||||||
|
public SecureChannelSession() {
|
||||||
|
random = new SecureRandom();
|
||||||
|
|
||||||
|
// generate keypair
|
||||||
|
eckey = new ECKey();
|
||||||
|
aesCipher = new AESKeyCrypter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a pairing secret. This should be called before each session. The public key of the card is used as input
|
||||||
|
* for the EC-DH algorithm. The output is stored as the secret.
|
||||||
|
*
|
||||||
|
* @param pubkeyData the public key returned by the applet as response to the SELECT command
|
||||||
|
*/
|
||||||
|
public void initiateSecureChannel(byte[] pubkeyData) { //TODO: check keyData format
|
||||||
|
try {
|
||||||
|
byte[] privkeyData = this.eckey.getPrivKeyBytes();
|
||||||
|
secretPoint = new SecretPoint(privkeyData, pubkeyData);
|
||||||
|
secret = secretPoint.ECDHSecretAsBytes();
|
||||||
|
//log.trace("SATOCHIP SecureChannelSession initiateSecureChannel() secret: " + Utils.bytesToHex(secret));
|
||||||
|
|
||||||
|
// derive session encryption key
|
||||||
|
byte[] msg_key = "sc_key".getBytes();
|
||||||
|
byte[] derived_key_2Ob = getHmacSha1Hash(secret, msg_key);
|
||||||
|
derived_key = new byte[16];
|
||||||
|
System.arraycopy(derived_key_2Ob, 0, derived_key, 0, 16);
|
||||||
|
//log.trace("SATOCHIP SecureChannelSession initiateSecureChannel() derived_key: " + Utils.bytesToHex(derived_key));
|
||||||
|
// derive session mac key
|
||||||
|
byte[] msg_mac = "sc_mac".getBytes();
|
||||||
|
mac_key = getHmacSha1Hash(secret, msg_mac);
|
||||||
|
//log.trace("SATOCHIP SecureChannelSession initiateSecureChannel() mac_key: " + Utils.bytesToHex(mac_key));
|
||||||
|
|
||||||
|
ivCounter = 1;
|
||||||
|
initialized_secure_channel = true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error initiating secure channel", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUCommand encryptSecureChannel(APDUCommand plainApdu) throws CardException {
|
||||||
|
try {
|
||||||
|
byte[] plainBytes = plainApdu.serialize();
|
||||||
|
|
||||||
|
// set iv
|
||||||
|
iv = new byte[SC_BLOCK_SIZE];
|
||||||
|
random.nextBytes(iv);
|
||||||
|
ByteBuffer bb = ByteBuffer.allocate(4);
|
||||||
|
bb.putInt(ivCounter); // big endian
|
||||||
|
byte[] ivCounterBytes = bb.array();
|
||||||
|
System.arraycopy(ivCounterBytes, 0, iv, 12, 4);
|
||||||
|
ivCounter += 2;
|
||||||
|
|
||||||
|
// encrypt data
|
||||||
|
Key aesKey = new Key(derived_key, null, null);
|
||||||
|
byte[] encrypted = aesCipher.encrypt(plainBytes, iv, aesKey).getEncryptedBytes();
|
||||||
|
|
||||||
|
// mac
|
||||||
|
int offset = 0;
|
||||||
|
byte[] data_to_mac = new byte[IV_SIZE + 2 + encrypted.length];
|
||||||
|
System.arraycopy(iv, offset, data_to_mac, offset, IV_SIZE);
|
||||||
|
offset += IV_SIZE;
|
||||||
|
data_to_mac[offset++] = (byte) (encrypted.length >> 8);
|
||||||
|
data_to_mac[offset++] = (byte) (encrypted.length % 256);
|
||||||
|
System.arraycopy(encrypted, 0, data_to_mac, offset, encrypted.length);
|
||||||
|
// log.trace("SATOCHIP data_to_mac: "+ SatochipParser.toHexString(data_to_mac));
|
||||||
|
byte[] mac = getHmacSha1Hash(mac_key, data_to_mac);
|
||||||
|
|
||||||
|
// copy all data to new data buffer
|
||||||
|
offset = 0;
|
||||||
|
byte[] data = new byte[IV_SIZE + 2 + encrypted.length + 2 + MAC_SIZE];
|
||||||
|
System.arraycopy(iv, offset, data, offset, IV_SIZE);
|
||||||
|
offset += IV_SIZE;
|
||||||
|
data[offset++] = (byte) (encrypted.length >> 8);
|
||||||
|
data[offset++] = (byte) (encrypted.length % 256);
|
||||||
|
System.arraycopy(encrypted, 0, data, offset, encrypted.length);
|
||||||
|
offset += encrypted.length;
|
||||||
|
data[offset++] = (byte) (mac.length >> 8);
|
||||||
|
data[offset++] = (byte) (mac.length % 256);
|
||||||
|
System.arraycopy(mac, 0, data, offset, mac.length);
|
||||||
|
|
||||||
|
// convert to C-APDU
|
||||||
|
return new APDUCommand(0xB0, INS_PROCESS_SECURE_CHANNEL, 0x00, 0x00, data);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new CardException("Error encrypting secure channel", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public APDUResponse decryptSecureChannel(APDUResponse encryptedApdu) throws CardException {
|
||||||
|
try {
|
||||||
|
byte[] encryptedBytes = encryptedApdu.getData();
|
||||||
|
if(encryptedBytes.length == 0) {
|
||||||
|
return encryptedApdu; // no decryption needed
|
||||||
|
} else if(encryptedBytes.length < 40) {
|
||||||
|
// has at least (IV_SIZE + 2 + 2 + 20)
|
||||||
|
throw new RuntimeException("Encrypted response has wrong length: " + encryptedBytes.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
byte[] iv = new byte[IV_SIZE];
|
||||||
|
System.arraycopy(encryptedBytes, offset, iv, 0, IV_SIZE);
|
||||||
|
offset += IV_SIZE;
|
||||||
|
int ciphertext_size = ((encryptedBytes[offset++] & 0xff) << 8) + (encryptedBytes[offset++] & 0xff);
|
||||||
|
if((encryptedBytes.length - offset) != ciphertext_size) {
|
||||||
|
throw new RuntimeException("Encrypted response has wrong length ciphertext_size: " + ciphertext_size);
|
||||||
|
}
|
||||||
|
byte[] ciphertext = new byte[ciphertext_size];
|
||||||
|
System.arraycopy(encryptedBytes, offset, ciphertext, 0, ciphertext.length);
|
||||||
|
|
||||||
|
// decrypt data
|
||||||
|
Key aesKey = new Key(derived_key, null, null);
|
||||||
|
EncryptedData encryptedData = new EncryptedData(iv, ciphertext, null, null);
|
||||||
|
byte[] decrypted = aesCipher.decrypt(encryptedData, aesKey);
|
||||||
|
|
||||||
|
return new APDUResponse(decrypted, (byte) 0x90, (byte) 0x00);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new CardException("Error decrypting secure channel", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean initializedSecureChannel() {
|
||||||
|
return initialized_secure_channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPublicKey() {
|
||||||
|
return eckey.getPubKey(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetSecureChannel() {
|
||||||
|
initialized_secure_channel = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] getHmacSha1Hash(byte[] key, byte[] data) {
|
||||||
|
try {
|
||||||
|
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA1");
|
||||||
|
Mac mac = Mac.getInstance("HmacSHA1");
|
||||||
|
mac.init(secretKeySpec);
|
||||||
|
return mac.doFinal(data);
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new RuntimeException("Error computing HmacSHA1", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
import com.sparrowwallet.sparrow.io.*;
|
||||||
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
|
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
|
||||||
|
import com.sparrowwallet.sparrow.io.satochip.Satochip;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Accordion;
|
import javafx.scene.control.Accordion;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
@ -38,7 +39,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner());
|
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip());
|
||||||
for(KeystoreCardImport importer : cardImporters) {
|
for(KeystoreCardImport importer : cardImporters) {
|
||||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||||
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||||
|
|
|
@ -75,6 +75,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
@FXML
|
@FXML
|
||||||
private SegmentedButton cardServiceButtons;
|
private SegmentedButton cardServiceButtons;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton backupButton;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button importButton;
|
private Button importButton;
|
||||||
|
|
||||||
|
@ -300,6 +303,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
||||||
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
||||||
cardServiceButtons.setVisible(keystore.getWalletModel().isCard());
|
cardServiceButtons.setVisible(keystore.getWalletModel().isCard());
|
||||||
|
backupButton.setDisable(!keystore.getWalletModel().supportsBackup());
|
||||||
|
|
||||||
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
|
importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace...");
|
||||||
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
|
importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));
|
||||||
|
@ -465,7 +469,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardPinDialog cardPinDialog = new CardPinDialog(backupOnly);
|
CardPinDialog cardPinDialog = new CardPinDialog(keystore.getWalletModel(), backupOnly);
|
||||||
cardPinDialog.initOwner(cardServiceButtons.getScene().getWindow());
|
cardPinDialog.initOwner(cardServiceButtons.getScene().getWindow());
|
||||||
Optional<CardPinDialog.CardPinChange> optPinChange = cardPinDialog.showAndWait();
|
Optional<CardPinDialog.CardPinChange> optPinChange = cardPinDialog.showAndWait();
|
||||||
if(optPinChange.isPresent()) {
|
if(optPinChange.isPresent()) {
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<Tooltip text="Change the PIN of the current card"/>
|
<Tooltip text="Change the PIN of the current card"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton toggleGroup="$cardServiceToggleGroup" text="Backup" onAction="#backupCard">
|
<ToggleButton fx:id="backupButton" toggleGroup="$cardServiceToggleGroup" text="Backup" onAction="#backupCard">
|
||||||
<tooltip>
|
<tooltip>
|
||||||
<Tooltip text="Backup the current card"/>
|
<Tooltip text="Backup the current card"/>
|
||||||
</tooltip>
|
</tooltip>
|
||||||
|
|
8
src/main/resources/image/satochip-icon-invert.svg
Normal file
8
src/main/resources/image/satochip-icon-invert.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.4 KiB |
8
src/main/resources/image/satochip-icon.svg
Normal file
8
src/main/resources/image/satochip-icon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.3 KiB |
BIN
src/main/resources/image/satochip.png
Normal file
BIN
src/main/resources/image/satochip.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
src/main/resources/image/satochip@2x.png
Normal file
BIN
src/main/resources/image/satochip@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
BIN
src/main/resources/image/satochip@3x.png
Normal file
BIN
src/main/resources/image/satochip@3x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
Loading…
Reference in a new issue