Add Satochip support: Initial commit

This commit is contained in:
Toporin 2023-09-05 14:14:53 +01:00
parent 0a469a380b
commit 755208a8a8
19 changed files with 2553 additions and 60 deletions

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
@ -74,7 +75,7 @@ public class CardImportPane extends TitledDescriptionPane {
return;
}
if(pin.get().length() < 6) {
if(pin.get().length() < importer.getWalletModel().getMinPinLength()) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getPinAndDerivationEntry());
showHideLink.setVisible(false);

View file

@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
@ -673,7 +674,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
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");
setContent(getCardPinEntry(importButton));
showHideLink.setVisible(false);
@ -690,7 +691,7 @@ public class DevicePane extends TitledDescriptionPane {
}
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
handleCardOperation(importService, importButton, "Import", true, device.getModel().getMinPinLength(), event -> {
importKeystore(derivation, importService.getValue());
});
} catch(Exception e) {
@ -769,7 +770,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
handleCardOperation(signService, signButton, "Signing", true, event -> {
handleCardOperation(signService, signButton, "Signing", true, device.getModel().getMinPinLength(), event -> {
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
});
} catch(Exception e) {
@ -784,7 +785,7 @@ public class DevicePane extends TitledDescriptionPane {
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
});
signPSBTService.setOnFailed(workerStateEvent -> {
setError("Signing Error", signPSBTService.getException().getMessage());
setError("Signing Error ", signPSBTService.getException().getMessage());
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
signButton.setDisable(false);
});
@ -794,8 +795,8 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
if(pinRequired && pin.get().length() < 6) {
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, int pinMinLength, EventHandler<WorkerStateEvent> successHandler) {
if(pinRequired && pin.get().length() < pinMinLength) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(operationButton));
showHideLink.setVisible(false);
@ -838,7 +839,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
handleCardOperation(signMessageService, signMessageButton, "Signing", true, device.getModel().getMinPinLength(), event -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
@ -924,7 +925,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(slot, messageProperty);
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, device.getModel().getMinPinLength(), event -> {
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
});
} catch(Exception e) {
@ -940,7 +941,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
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");
setContent(getCardPinEntry(getAddressButton));
showHideLink.setVisible(false);
@ -957,7 +958,7 @@ public class DevicePane extends TitledDescriptionPane {
}
Service<Address> addressService = cardApi.getAddressService(messageProperty);
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
handleCardOperation(addressService, getAddressButton, "Address", false, device.getModel().getMinPinLength(), event -> {
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
});
} catch(Exception e) {
@ -1047,60 +1048,149 @@ public class DevicePane extends TitledDescriptionPane {
}
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
WalletModel cardModel=null;
try{
cardModel = cardApi.getCardType();
} catch (Exception e){
log.error("SATOCHIP DevicePane getCardInitializationPanel() exception: " + e);
//do nothing?
}
if (cardModel == WalletModel.SATOCHIP){
log.debug("SATOCHIP DevicePane getCardInitializationPanel() cardModel == Satochip");
VBox initTypeBox = new VBox(5);
// ideally, use UX like MnemonicKeystorePane to import seed
CustomPasswordField repeatedPin = new ViewPasswordField();
CustomPasswordField mnemonic = new ViewPasswordField();
CustomPasswordField passphrase = new ViewPasswordField();
repeatedPin.setPromptText("Confirm entered PIN to initialize your Satochip");
repeatedPin.setDisable(false);
mnemonic.setPromptText("Enter a Seed to initialize your Satochip");
mnemonic.setDisable(false);
passphrase.setPromptText("Enter a passphrase (optional)");
passphrase.setDisable(false);
initTypeBox.getChildren().addAll(repeatedPin, mnemonic, passphrase);
ToggleGroup toggleGroup = new ToggleGroup();
automatic.setToggleGroup(toggleGroup);
advanced.setToggleGroup(toggleGroup);
automatic.setSelected(true);
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
entropy.setDisable(newValue == automatic);
});
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
if(deviceOperation == DeviceOperation.IMPORT) {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
}
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
});
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
operationButton.setDisable(false);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
/*
log.trace("SATOCHIP DevicePane getCardInitializationPanel() pin.get(): " + pin.get());
log.trace("SATOCHIP DevicePane getCardInitializationPanel() repeatedPin.getText(): " + repeatedPin.getText());
log.trace("SATOCHIP DevicePane getCardInitializationPanel() mnemonic.getText(): "+ mnemonic.getText());
log.trace("SATOCHIP DevicePane getCardInitializationPanel() passphrase.getText(): "+ passphrase.getText());
*/
byte[] seedBytes;
// check that pin and previous pin match
if ( !pin.get().equals(repeatedPin.getText()) ){
seedBytes = null; // will display a proper error later in cardApi.getInitializationService()
messageProperty.set("The two entered Pin values do not correspond!");
} else {
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);
try{
// check bip39 is correct & convert seed to masterseed bytes
List<String> mnemonicWords = Arrays.asList(mnemonic.getText().split("[\\s,]+")); // \\s*,\\s*
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase.getText(), System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
seedBytes = seed.getSeedBytes();
} catch (Exception e) {
seedBytes = null; // will display a proper error later in cardApi.getInitializationService()
messageProperty.set("Failed to parse the seed with error: " + e);
}
}
Service<Void> cardInitializationService = cardApi.getInitializationService(seedBytes, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
log.debug("SATOCHIP DevicePane getCardInitializationPanel() Card initialized!");
if(deviceOperation == DeviceOperation.IMPORT) {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
}
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
});
cardInitializationService.setOnFailed(failEvent -> {
log.error("SATOCHIP DevicePane getCardInitializationPanel() failed to initialize card!");
/*Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
operationButton.setDisable(false);
} else {*/
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();
});
cardInitializationService.start();
});
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
return contentBox;
return contentBox;
} else {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
automatic.setToggleGroup(toggleGroup);
advanced.setToggleGroup(toggleGroup);
automatic.setSelected(true);
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
entropy.setDisable(newValue == automatic);
});
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
if(deviceOperation == DeviceOperation.IMPORT) {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
}
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
});
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
operationButton.setDisable(false);
} else {
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();
});
HBox contentBox = new HBox(20);
contentBox.getChildren().addAll(initTypeBox, initializeButton);
contentBox.setPadding(new Insets(10, 30, 10, 30));
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
return contentBox;
}
}
private Node getCardPinEntry(ButtonBase operationButton) {

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
import com.sparrowwallet.sparrow.io.satochip.SatoCardApi;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Service;
import org.controlsfx.tools.Platform;
@ -30,6 +31,7 @@ public abstract class CardApi {
private static final Logger log = LoggerFactory.getLogger(CardApi.class);
private static File[] LINUX_PCSC_LIBS = new File[] {
new File("/usr/lib/x86_64-linux-gnu/libpcsclite.so.1"),
new File("/usr/lib/libpcsclite.so.1"),
new File("/usr/local/lib/libpcsclite.so.1"),
new File("/lib/x86_64-linux-gnu/libpcsclite.so.1"),
@ -45,6 +47,14 @@ public abstract class CardApi {
//ignore
}
try {
log.error("SATOCHIP in CardApi getConnectedCards() new SatoCardApi()");
SatoCardApi satoCardApi = new SatoCardApi(null, null);
return List.of(satoCardApi.getCardType());
} catch(Exception e) {
//ignore
}
return Collections.emptyList();
}
@ -53,6 +63,10 @@ public abstract class CardApi {
return new CkCardApi(walletModel, pin);
}
if(walletModel == WalletModel.SATOCHIP) {
return new SatoCardApi(walletModel, pin);
}
throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
}

View file

@ -0,0 +1,148 @@
package com.sparrowwallet.sparrow.io.satochip;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.*;
/**
* ISO7816-4 APDU.
*/
public class APDUCommand {
protected int cla;
protected int ins;
protected int p1;
protected int p2;
protected int lc;
protected byte[] data;
protected boolean needsLE;
public static final String HEXES = "0123456789ABCDEF";
/**
* 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() throws IOException {
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);
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() {
try{
byte[] raw= this.serialize();
if ( raw == null ) {
return "";
}
final StringBuilder hex = new StringBuilder( 2 * raw.length );
for ( final byte b : raw ) {
hex.append(HEXES.charAt((b & 0xF0) >> 4)).append(HEXES.charAt((b & 0x0F)));
}
return hex.toString();
} catch (Exception e){
return "Exception in APDUCommand.toHexString()";
}
}
/**
* 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;
}
}

View file

@ -0,0 +1,125 @@
package com.sparrowwallet.sparrow.io.satochip;
import java.io.*;
/**
* 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 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;
try{
if ( raw == null ) {
return "";
}
final StringBuilder hex = new StringBuilder( 2 * raw.length );
for ( final byte b : raw ) {
hex.append(HEXES.charAt((b & 0xF0) >> 4))
.append(HEXES.charAt((b & 0x0F)));
}
return hex.toString();
} catch(Exception e){
return "Exception in APDUResponse.toHexString()";
}
}
}

View file

@ -0,0 +1,149 @@
package com.sparrowwallet.sparrow.io.satochip;
public final class Constants {
// Prevents instanciation of class
private Constants() {}
/****************************************
* Instruction codes *
****************************************/
public final static byte CLA = (byte)0xB0;
// Applet initialization
public final static byte INS_SETUP = (byte) 0x2A;
// Keys' use and management
public final static byte INS_IMPORT_KEY = (byte) 0x32;
public final static byte INS_RESET_KEY = (byte) 0x33;
public final static byte INS_GET_PUBLIC_FROM_PRIVATE= (byte)0x35;
// External authentication
public final static byte INS_CREATE_PIN = (byte) 0x40; //TODO: remove?
public final static byte INS_VERIFY_PIN = (byte) 0x42;
public final static byte INS_CHANGE_PIN = (byte) 0x44;
public final static byte INS_UNBLOCK_PIN = (byte) 0x46;
public final static byte INS_LOGOUT_ALL = (byte) 0x60;
// Status information
public final static byte INS_LIST_PINS = (byte) 0x48;
public final static byte INS_GET_STATUS = (byte) 0x3C;
public final static byte INS_CARD_LABEL = (byte) 0x3D;
// HD wallet
public final static byte INS_BIP32_IMPORT_SEED= (byte) 0x6C;
public final static byte INS_BIP32_RESET_SEED= (byte) 0x77;
public final static byte INS_BIP32_GET_AUTHENTIKEY= (byte) 0x73;
public final static byte INS_BIP32_SET_AUTHENTIKEY_PUBKEY= (byte)0x75;
public final static byte INS_BIP32_GET_EXTENDED_KEY= (byte) 0x6D;
public final static byte INS_BIP32_SET_EXTENDED_PUBKEY= (byte) 0x74;
public final static byte INS_SIGN_MESSAGE= (byte) 0x6E;
public final static byte INS_SIGN_SHORT_MESSAGE= (byte) 0x72;
public final static byte INS_SIGN_TRANSACTION= (byte) 0x6F;
public final static byte INS_PARSE_TRANSACTION = (byte) 0x71;
public final static byte INS_CRYPT_TRANSACTION_2FA = (byte) 0x76;
public final static byte INS_SET_2FA_KEY = (byte) 0x79;
public final static byte INS_RESET_2FA_KEY = (byte) 0x78;
public final static byte INS_SIGN_TRANSACTION_HASH= (byte) 0x7A;
// secure channel
public final static byte INS_INIT_SECURE_CHANNEL = (byte) 0x81;
public final static byte INS_PROCESS_SECURE_CHANNEL = (byte) 0x82;
// secure import from SeedKeeper
public final static byte INS_IMPORT_ENCRYPTED_SECRET = (byte) 0xAC;
public final static byte INS_IMPORT_TRUSTED_PUBKEY = (byte) 0xAA;
public final static byte INS_EXPORT_TRUSTED_PUBKEY = (byte) 0xAB;
public final static byte INS_EXPORT_AUTHENTIKEY= (byte) 0xAD;
// Personalization PKI support
public final static byte INS_IMPORT_PKI_CERTIFICATE = (byte) 0x92;
public final static byte INS_EXPORT_PKI_CERTIFICATE = (byte) 0x93;
public final static byte INS_SIGN_PKI_CSR = (byte) 0x94;
public final static byte INS_EXPORT_PKI_PUBKEY = (byte) 0x98;
public final static byte INS_LOCK_PKI = (byte) 0x99;
public final static byte INS_CHALLENGE_RESPONSE_PKI= (byte) 0x9A;
// reset to factory settings
public final static byte INS_RESET_TO_FACTORY = (byte) 0xFF;
/****************************************
* Error codes *
****************************************/
/** Entered PIN is not correct */
public final static short SW_PIN_FAILED = (short)0x63C0;// includes number of tries remaining
///** DEPRECATED - Entered PIN is not correct */
//public final static short SW_AUTH_FAILED = (short) 0x9C02;
/** Required operation is not allowed in actual circumstances */
public final static short SW_OPERATION_NOT_ALLOWED = (short) 0x9C03;
/** Required setup is not not done */
public final static short SW_SETUP_NOT_DONE = (short) 0x9C04;
/** Required setup is already done */
public final static short SW_SETUP_ALREADY_DONE = (short) 0x9C07;
/** Required feature is not (yet) supported */
final static short SW_UNSUPPORTED_FEATURE = (short) 0x9C05;
/** Required operation was not authorized because of a lack of privileges */
public final static short SW_UNAUTHORIZED = (short) 0x9C06;
/** Algorithm specified is not correct */
public final static short SW_INCORRECT_ALG = (short) 0x9C09;
/** There have been memory problems on the card */
public final static short SW_NO_MEMORY_LEFT = (short) 0x9C01;
///** DEPRECATED - Required object is missing */
//public final static short SW_OBJECT_NOT_FOUND= (short) 0x9C07;
/** Incorrect P1 parameter */
public final static short SW_INCORRECT_P1 = (short) 0x9C10;
/** Incorrect P2 parameter */
public final static short SW_INCORRECT_P2 = (short) 0x9C11;
/** Invalid input parameter to command */
public final static short SW_INVALID_PARAMETER = (short) 0x9C0F;
/** Eckeys initialized */
public final static short SW_ECKEYS_INITIALIZED_KEY = (short) 0x9C1A;
/** Verify operation detected an invalid signature */
public final static short SW_SIGNATURE_INVALID = (short) 0x9C0B;
/** Operation has been blocked for security reason */
public final static short SW_IDENTITY_BLOCKED = (short) 0x9C0C;
/** For debugging purposes */
public final static short SW_INTERNAL_ERROR = (short) 0x9CFF;
/** Very low probability error */
public final static short SW_BIP32_DERIVATION_ERROR = (short) 0x9C0E;
/** Incorrect initialization of method */
public final static short SW_INCORRECT_INITIALIZATION = (short) 0x9C13;
/** Bip32 seed is not initialized*/
public final static short SW_BIP32_UNINITIALIZED_SEED = (short) 0x9C14;
/** Bip32 seed is already initialized (must be reset before change)*/
public final static short SW_BIP32_INITIALIZED_SEED = (short) 0x9C17;
//** DEPRECATED - Bip32 authentikey pubkey is not initialized*/
//public final static short SW_BIP32_UNINITIALIZED_AUTHENTIKEY_PUBKEY= (short) 0x9C16;
/** Incorrect transaction hash */
public final static short SW_INCORRECT_TXHASH = (short) 0x9C15;
/** 2FA already initialized*/
public final static short SW_2FA_INITIALIZED_KEY = (short) 0x9C18;
/** 2FA uninitialized*/
public final static short SW_2FA_UNINITIALIZED_KEY = (short) 0x9C19;
/** HMAC errors */
static final short SW_HMAC_UNSUPPORTED_KEYSIZE = (short) 0x9c1E;
static final short SW_HMAC_UNSUPPORTED_MSGSIZE = (short) 0x9c1F;
/** Secure channel */
public final static short SW_SECURE_CHANNEL_REQUIRED = (short) 0x9C20;
public final static short SW_SECURE_CHANNEL_UNINITIALIZED = (short) 0x9C21;
public final static short SW_SECURE_CHANNEL_WRONG_IV= (short) 0x9C22;
public final static short SW_SECURE_CHANNEL_WRONG_MAC= (short) 0x9C23;
/** Secret data is too long for import **/
public final static short SW_IMPORTED_DATA_TOO_LONG = (short) 0x9C32;
/** Wrong HMAC when importing Secret through Secure import **/
public final static short SW_SECURE_IMPORT_WRONG_MAC = (short) 0x9C33;
/** Wrong Fingerprint when importing Secret through Secure import **/
public final static short SW_SECURE_IMPORT_WRONG_FINGERPRINT = (short) 0x9C34;
/** No Trusted Pubkey when importing Secret through Secure import **/
public final static short SW_SECURE_IMPORT_NO_TRUSTEDPUBKEY = (short) 0x9C35;
/** PKI perso error */
public final static short SW_PKI_ALREADY_LOCKED = (short) 0x9C40;
/** CARD HAS BEEN RESET TO FACTORY */
public final static short SW_RESET_TO_FACTORY = (short) 0xFF00;
/** For instructions that have been deprecated*/
public final static short SW_INS_DEPRECATED = (short) 0x9C26;
/** For debugging purposes 2 */
public final static short SW_DEBUG_FLAG = (short) 0x9FFF;
}

View file

@ -0,0 +1,102 @@
package com.sparrowwallet.sparrow.io.satochip;
import java.util.StringTokenizer;
/**
* Keypath object to be used with the SatochipCommandSet
*/
public class KeyPath {
private byte[] data;
/**
* Parses a keypath into a byte array to be used with the SatochipCommandSet object.
*
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
*
* The first component should be "m", indicating the master key.
*
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
* which indicates an hardened key.
*
* 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('\'');
}
}
}

View file

@ -0,0 +1,560 @@
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.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
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 SatochipCommandSet cardProtocol;
private String pin;
private String basePath = null; // = "m/84'/1'/0'"
public SatoCardApi(WalletModel cardType, String pin) throws CardException {
log.debug("SATOCHIP SatoCardApi() cardType: " + cardType);
this.cardType = cardType;
this.cardProtocol = new SatochipCommandSet();
this.pin = pin;
}
@Override
public boolean isInitialized() throws CardException {
log.debug("SATOCHIP SatoCardApi isInitialized() START");
SatoCardStatus cardStatus = this.getStatus();
return cardStatus.isInitialized(); // setupDone && isSeeded
}
//TODO
@Override
public void initialize(int slot, byte[] seedBytes) throws CardException {
log.debug("SATOCHIP SatoCardApi initialize() START");
// 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 'getCurrentSlot' !");
}
//TODO
@Override
public ScriptType getDefaultScriptType() {
return ScriptType.P2WPKH;
}
SatoCardStatus getStatus() throws CardException {
log.debug("SATOCHIP SatoCardApi getStatus() START");
SatoCardStatus cardStatus = this.cardProtocol.getApplicationStatus();
return cardStatus;
}
@Override
public Service<Void> getAuthDelayService() throws CardException {
log.debug("SATOCHIP SatoCardApi getAuthDelayService() START");
return null;
}
@Override
public boolean requiresBackup() throws CardException {
log.debug("SATOCHIP SatoCardApi requiresBackup() START");
return false; // todo?
}
@Override
public Service<String> getBackupService() {
log.debug("SATOCHIP SatoCardApi getBackupService() START");
return null; //new BackupService();
}
@Override
public boolean changePin(String newPin) throws CardException {
log.debug("SATOCHIP SatoCardApi changePin() START");
try{
this.cardProtocol.cardChangePIN((byte)0, this.pin, newPin);
return true;
} catch(Exception e) {
log.error("SATOCHIP SatoCardApi changePin() exception: " + e);
return false;
}
}
// TODO: remove?
void setDerivation(List<ChildNumber> derivation) throws CardException {
log.debug("SATOCHIP SatoCardApi setDerivation() START");
// convert to string representation
String derivationString = "m";
for(int i=0;i<derivation.size();i++){
derivationString += "/" + derivation.get(i).toString();
}
log.debug("SATOCHIP SatoCardApi setDerivation() derivationString: " + derivationString);
// this basePath will be used when deriving keys
this.basePath = KeyDerivation.writePath(derivation);
log.debug("SATOCHIP SatoCardApi setDerivation() basePath: " + this.basePath);
}
@Override
public Service<Void> getInitializationService(byte[] seedBytes, StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getInitializationService() START");
return new CardInitializationService(seedBytes, messageProperty);
}
@Override
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getImportService() START");
log.debug("SATOCHIP SatoCardApi getImportService() derivation: " + derivation);
return new CardImportPane.CardImportService(new Satochip(), pin, derivation, messageProperty);
}
/* todo: provide derivation path?
* 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 {
log.debug("SATOCHIP SatoCardApi getKeystore() START");
APDUResponse rapdu = this.cardProtocol.cardVerifyPIN(0, pin);
log.debug("SATOCHIP SatoCardApi getKeystore() cardVerifyPIN rapdu: " + Utils.bytesToHex(rapdu.getBytes()));
String keyDerivationString = (this.basePath != null)? this.basePath : "m/84'/1'/0'";
log.debug("SATOCHIP SatoCardApi getKeystore() keyDerivationString: " + keyDerivationString);
ExtendedKey.Header xtype = Network.get().getXpubHeader();
String xpub = this.cardProtocol.cardBip32GetXpub(keyDerivationString, xtype);
log.debug("SATOCHIP SatoCardApi getKeystore() xpub: " + xpub);
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(xpub);
log.debug("SATOCHIP SatoCardApi getKeystore() extendedKey: " + extendedKey);
String masterFingerprint = Utils.bytesToHex(extendedKey.getKey().getFingerprint());
log.debug("SATOCHIP SatoCardApi getKeystore() masterFingerprint: " + masterFingerprint);
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString);
log.debug("SATOCHIP SatoCardApi getKeystore() keyDerivation: " + keyDerivation);
Keystore keystore = new Keystore();
keystore.setLabel(WalletModel.SATOCHIP.toDisplayString());
keystore.setKeyDerivation(keyDerivation);
keystore.setSource(KeystoreSource.HW_USB);
keystore.setExtendedPublicKey(extendedKey);
keystore.setWalletModel(WalletModel.SATOCHIP);
log.debug("SATOCHIP SatoCardApi getKeystore() keystore: " + keystore);
return keystore;
}
@Override
public Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getSignService() START");
log.debug("SATOCHIP SatoCardApi getSignService() wallet: " + wallet);
//log.debug("SATOCHIP SatoCardApi getSignService() psbt: " + psbt);
return new SignService(wallet, psbt, messageProperty);
}
void sign(Wallet wallet, PSBT psbt) throws CardException {
log.debug("SATOCHIP SatoCardApi sign() START");
log.debug("SATOCHIP SatoCardApi sign() wallet: " + wallet);
log.debug("SATOCHIP SatoCardApi sign() psbt: " + psbt);
// debug psbt
/*log.debug("SATOCHIP SatoCardApi sign() psbt.hasSignatures(): " + psbt.hasSignatures());
log.debug("SATOCHIP SatoCardApi sign() psbt.isSigned(): " + psbt.isSigned());
log.debug("SATOCHIP SatoCardApi sign() psbt.isFinalized(): " + psbt.isFinalized());
log.debug("SATOCHIP SatoCardApi sign() psbt.verifySignatures:");*/
/*try{
psbt.verifySignatures();
log.debug("SATOCHIP SatoCardApi sign() psbt signature verified!");
} catch(Exception e) {
log.debug("SATOCHIP SatoCardApi sign() failed to verify signatures with error: " + e);
}
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(false):");
try{
psbt.parse(false);
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(false) finished!");
} catch(Exception e) {
log.debug("SATOCHIP SatoCardApi sign() failed psbt.parse(false) with error: " + e);
}
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(false) end:");
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(true):");
try{
psbt.parse(true);
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(true) finished!");
} catch(Exception e) {
log.debug("SATOCHIP SatoCardApi sign() failed psbt.parse(true) with error: " + e);
}
log.debug("SATOCHIP SatoCardApi sign() psbt.parse(true) end:");*/
// endbug psbt
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
//log.debug("SATOCHIP SatoCardApi sign() signingNodes: " + signingNodes);
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
if(!psbtInput.isSigned()) {
WalletNode signingNode = signingNodes.get(psbtInput);
log.debug("SATOCHIP SatoCardApi sign() signingNode: " + signingNode);
log.debug("SATOCHIP SatoCardApi sign() signingNode.getDerivationPath(): " + signingNode.getDerivationPath()); // m/0/0
try {
/*// debug
Map<ECKey, KeyDerivation> mapPubKey = psbtInput.getDerivedPublicKeys();
log.debug("SATOCHIP SatoCardApi sign() mapPubKey.size(): " + mapPubKey.size());
log.debug("SATOCHIP SatoCardApi sign() mapPubKey: " + mapPubKey);
for (Map.Entry<ECKey, KeyDerivation> entry : mapPubKey.entrySet()) {
ECKey key = entry.getKey();
KeyDerivation value = entry.getValue();
log.debug("SATOCHIP SatoCardApi sign() mapPubKey pubkey: " + Utils.bytesToHex(key.getPubKey()));
log.debug("SATOCHIP SatoCardApi sign() mapPubKey derivation: " + value.getDerivationPath());
}
log.debug("SATOCHIP SatoCardApi sign() mapPubKey END");
// endbug*/
String fullPath= null;
List<Keystore> keystores = wallet.getKeystores();
log.debug("SATOCHIP SatoCardApi sign() keystores.size(): " + keystores.size());
for(int i=0;i<keystores.size();i++){
Keystore keystore = keystores.get(i);
log.debug("SATOCHIP SatoCardApi sign() i: " + i);
log.debug("SATOCHIP SatoCardApi sign() keystore.getLabel(): " + keystore.getLabel());
log.debug("SATOCHIP SatoCardApi sign() keystore.getSource(): " + keystore.getSource());
log.debug("SATOCHIP SatoCardApi sign() keystore.getWalletModel(): " + keystore.getWalletModel());
log.debug("SATOCHIP SatoCardApi sign() keystore.getKeyDerivation().getDerivationPath(): " + keystore.getKeyDerivation().getDerivationPath()); // m/66
log.debug("SATOCHIP SatoCardApi sign() keystore.getExtendedPublicKey(): " + keystore.getExtendedPublicKey());
WalletModel walletModel = keystore.getWalletModel();
if (walletModel==WalletModel.SATOCHIP){
String basePath = keystore.getKeyDerivation().getDerivationPath();
log.debug("SATOCHIP SatoCardApi sign() basePath: " + basePath);
String extendedPath= signingNode.getDerivationPath().substring(1);
log.debug("SATOCHIP SatoCardApi sign() extendedPath: " + extendedPath);
fullPath = basePath + extendedPath;
log.debug("SATOCHIP SatoCardApi sign() fullPath: " + fullPath);
ECKey keystorePubkey = keystore.getPubKey(signingNode);
log.debug("SATOCHIP SatoCardApi sign() keystore.getPubKey(signingNode): " + Utils.bytesToHex(keystorePubkey.getPubKey()));
break;
}
}
//psbtInput.printDebugInfo();
psbtInput.sign(new CardPSBTInputSigner(signingNode, fullPath));
//psbtInput.printDebugInfo();
} finally {
}
}// endif
else {
log.debug("SATOCHIP SatoCardApi sign() psbtInput already signed!");
//psbtInput.printDebugInfo();
}
} // endfor
}
@Override
public Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getSignMessageService() START");
return new SignMessageService(message, scriptType, derivation, messageProperty);
}
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
log.debug("SATOCHIP SatoCardApi signMessage() START");
log.debug("SATOCHIP SatoCardApi signMessage() message: " + message);
log.debug("SATOCHIP SatoCardApi signMessage() scriptType: " + scriptType);
log.debug("SATOCHIP SatoCardApi signMessage() derivation: " + derivation);
String fullpath = KeyDerivation.writePath(derivation);
log.debug("SATOCHIP SatoCardApi signMessage() fullpath: " + fullpath);
try {
APDUResponse rapdu0 = 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 with 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]);
log.debug("SATOCHIP SatoCardApi signMessage() pubkey: " + Utils.bytesToHex(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();
log.debug("SATOCHIP SatoCardApi sign() sigBytes: " + Utils.bytesToHex(sigBytes));
ECDSASignature ecdsaSig = ECDSASignature.decodeFromDER(sigBytes);
return ecdsaSig;
} catch(Exception e) {
throw new RuntimeException(e);
}
});
} finally {
//
}
}
@Override
public Service<ECKey> getPrivateKeyService(Integer slot, StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getPrivateKeyService() START");
throw new RuntimeException("Satochip does not support private key export!");
}
// todo: remove?
ECKey getPrivateKey(int slot, int currentSlot) throws CardException {
log.debug("SATOCHIP SatoCardApi getPrivateKey() START");
throw new CardException("Satochip does not support 'getPrivateKey'!");
}
@Override
public Service<Address> getAddressService(StringProperty messageProperty) {
log.debug("SATOCHIP SatoCardApi getAddressService() START");
// throw new runtimeException("Satochip does not support 'getAddress' currently!");
return null; //new AddressService(messageProperty);
}
// todo: remove?
Address getAddress(int currentSlot, int lastSlot, String addr) throws CardException {
log.debug("SATOCHIP SatoCardApi getAddress() START");
throw new CardException("Satochip does not support 'getAddress' currently!");
}
@Override
public void disconnect() {
log.debug("SATOCHIP SatoCardApi disconnect() START");
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 {
log.debug("SATOCHIP CardInitializationService createTask() START");
if (seedBytes == null){
// will show error message to user
log.debug("SATOCHIP CardInitializationService createTask() Error: seedBytes is null");
throw new Exception("Failed to initialize Satochip: " + messageProperty.get());
}
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 {
log.debug("SATOCHIP SatoCardApi.SignService createTask() START");
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) {
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() START");
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() hash:" + hash);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() sigHash:" + sigHash);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() signatureType:" + signatureType);
try {
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() fullPath:" + this.fullPath);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() signingNode.getDerivationPath():" + signingNode.getDerivationPath());
// 2FA is optionnal, 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 with 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]);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() pubkey: " + Utils.bytesToHex(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();
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() sigBytes: " + Utils.bytesToHex(sigBytes));
ECDSASignature ecdsaSig = ECDSASignature.decodeFromDER(sigBytes).toCanonicalised();
TransactionSignature txSig = new TransactionSignature(ecdsaSig, sigHash);
// verify
boolean isCorrect = pubkey.verify(hash, txSig);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() ECDSA verify with pubkey: " + isCorrect);
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 RuntimeException(WalletModel.SATOCHIP.toDisplayString() + " (with version below v0.14) cannot sign " + signatureType + " transactions!");
}
// tweak the bip32 key according to bip341
byte keynbr = (byte)0xFF;
byte[] tweak = null;
APDUResponse rapduTweak = cardProtocol.cardTaprootTweakPrivkey(keynbr, tweak);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() cardTaprootTweakPrivkey(): " + rapduTweak.toHexString());
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();
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() SCHNORR sigBytes: " + Utils.bytesToHex(sigBytes));
SchnorrSignature schnorrSig = SchnorrSignature.decode(sigBytes);
TransactionSignature txSig = new TransactionSignature(schnorrSig, sigHash);
// verify sig with outputPubkey...
boolean isCorrect2 = pubkey.verify(hash, txSig);
log.debug("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() SCHNORR verify with outputPubkey: " + isCorrect2);
return txSig; //new TransactionSignature(schnorrSig, sigHash);
}
} catch(Exception e) {
e.printStackTrace();
log.error("SATOCHIP SatoCardApi.CardPSBTInputSigner sign() 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 {
log.debug("SATOCHIP SatoCardApi.SignMessageService createTask() message: " + message);
log.debug("SATOCHIP SatoCardApi.SignMessageService createTask() scriptType: " + scriptType);
log.debug("SATOCHIP SatoCardApi.SignMessageService createTask() derivation: " + derivation);
return signMessage(message, scriptType, derivation);
}
};
}
}
}

View file

@ -0,0 +1,124 @@
package com.sparrowwallet.sparrow.io.satochip;
//import org.satochip.io.APDUResponse;
/**
* 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 tlvData 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)? false : true;
}
if (data.length >=10){
is_seeded= (data[9]==0X00)? false : true;
}
if (data.length >=11){
setup_done= (data[10]==0X00)? false : true;
} else {
setup_done= true;
}
if (data.length >=12){
needs_secure_channel= (data[11]==0X00)? false : true;
} 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{
//throws IllegalArgumentException("Wrong getStatus data!"); // should not happen
}
}
// 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;
}
// TODO: other gettters
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;
}
public String toString(){
String status_info= "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;
return status_info;
}
}

View file

@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.io.satochip;
import com.sparrowwallet.drongo.Utils;
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 static final int SW_OKAY = 0x9000;
private final Card connection;
SatoCardTransport(byte[] applet_aid) 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(), applet_aid);
break;
} catch(CardException e) {
if(!iter.hasNext()) {
log.error(e.getMessage());
throw e;
}
}
}
this.connection = connection;
}
private Card getConnection(CardTerminal cardTerminal, byte[] applet_aid) throws CardException {
Card connection = cardTerminal.connect("*");
CardChannel cardChannel = connection.getBasicChannel();
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, applet_aid));
if(resp.getSW() != SW_OKAY) {
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();
// todo: convert APDUCommand to CommansdApdu??
log.trace("SATOCHIP SatoCardTransport send capdu:" + capdu.toHexString());
CommandAPDU cmd = new CommandAPDU(capdu.getCla(), capdu.getIns(), capdu.getP1(), capdu.getP2(), capdu.getData());
ResponseAPDU resp = cardChannel.transmit(cmd);
// convert back to APDUResponse... (todo?)
APDUResponse rapdu = new APDUResponse(resp.getBytes());
log.trace("SATOCHIP SatoCardTransport send rapdu:" + rapdu.toHexString());
return rapdu;
}
void disconnect() throws CardException {
connection.disconnect(true);
}
}

View file

@ -0,0 +1,110 @@
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.SimpleIntegerProperty;
import javafx.beans.property.StringProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.smartcardio.CardException;
import java.util.List;
public class Satochip implements KeystoreCardImport {
private static final Logger log = LoggerFactory.getLogger(Satochip.class);
@Override
public boolean isInitialized() throws CardException {
log.debug("SATOCHIP Satochip isInitialized()");
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[] chainCode, StringProperty messageProperty) throws CardException {
log.debug("SATOCHIP Satochip initialize()");
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.");
}
// TODO!
// not used currently
// initialization is done through SatoCardApi.initialize()
} finally {
if(cardApi != null) {
cardApi.disconnect();
}
}
}
@Override
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
log.debug("SATOCHIP Satochip getKeystore() derivation:" + derivation);
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) {
e.printStackTrace();
log.error("SATOCHIP Satochip getKeystore() Exception: " + e);
throw new ImportException(e);
} finally {
if(cardApi != null) {
cardApi.disconnect();
}
}
}
@Override
public String getKeystoreImportDescription(int account) {
log.debug("SATOCHIP Satochip getKeystoreImportDescription()");
return "Import the keystore from your Satochip by inserting it in the card reader.";
}
@Override
public String getName() {
log.debug("SATOCHIP Satochip getName()");
return "Satochip";
}
@Override
public WalletModel getWalletModel() {
log.debug("SATOCHIP Satochip getWalletModel()");
return WalletModel.SATOCHIP;
}
}

View file

@ -0,0 +1,591 @@
package com.sparrowwallet.sparrow.io.satochip;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.Base58;
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 SecureChannelSession secureChannel;
private SatoCardStatus status;
private SatochipParser parser=null;
private String pinCached = null;
private byte[] authentikey= null;
private String authentikeyHex= null;
private String defaultBip32path= null;
// Satodime, SeedKeeper or Satochip?
private String cardType= null;
private String certPem= null; // PEM certificate of device, if any
public static final byte[] SATOCHIP_AID = Utils.hexToBytes("5361746f43686970"); //SatoChip
/**
* Creates a SatochipCommandSet using the given APDU Channel
* @param apduChannel 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) {
APDUResponse rapdu = this.cardGetStatus();
}
return this.status;
}
/****************************************
* AUTHENTIKEY *
****************************************/
public APDUResponse cardTransmit(APDUCommand plainApdu) {
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() START");
// 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){
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() get cardStatus...");
APDUCommand statusCapdu = new APDUCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
APDUResponse statusRapdu = this.cardTransport.send(statusCapdu);
status= new SatoCardStatus(statusRapdu);
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() status: " + status.toString());
}
APDUCommand capdu=plainApdu;
if (status.needsSecureChannel() && (ins!=0xA4) && (ins!=0x81) && (ins!=0x82) && (ins!=INS_GET_STATUS)){
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() needsSecureChannel...");
if (!secureChannel.initializedSecureChannel()){
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() initiateSecureChannel...");
// get card's public key
APDUResponse secChannelRapdu= this.cardInitiateSecureChannel();
byte[] pubkey= this.parser.parseInitiateSecureChannel(secChannelRapdu);
// setup secure channel
this.secureChannel.initiateSecureChannel(pubkey);
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() secureChannel initiated cardPubkey: " + Utils.bytesToHex(pubkey));
}
// encrypt apdu
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() capdu plain: " + plainApdu.toHexString());
capdu= secureChannel.encrypt_secure_channel(plainApdu);
isEncrypted=true;
log.trace("SATOCHIP: SatochipCommandSet cardTransmit() capdu cipher: " + capdu.toHexString());
}
APDUResponse rapdu = this.cardTransport.send(capdu);
int sw12= rapdu.getSw() ;
// check answer
if (sw12==0x9000){ // ok!
if (isEncrypted){
// decrypt
//log.info("SATOCHIP Rapdu encrypted:"+ rapdu.toHexString());
rapdu = secureChannel.decrypt_secure_channel(rapdu);
//log.info("SATOCHIP Rapdu decrypted:"+ rapdu.toHexString());
}
isApduTransmitted= true; // leave loop
return rapdu;
}
// PIN authentication is required
else if (sw12==0x9C06){
//cardVerifyPIN();
log.error("SATOCHIP: SatochipCommandSet cardTransmit() sw12==0x9C06: PIN required!");
//TODO: throw?
//TODO: verify PIN?
}
// SecureChannel is not initialized
else if (sw12==0x9C21){
log.error("SATOCHIP: SatochipCommandSet cardTransmit() sw12==0x9C21: secureChannel required!");
secureChannel.resetSecureChannel();
}
else {
// cannot resolve issue at this point
isApduTransmitted= true; // leave loop
return rapdu;
}
} catch(Exception e) {
log.warn("SATOCHIP: SatochipCommandSet cardTransmit() Exception: "+ 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("SATOCHIP SatochipCommandSet cardDisconnect() Exception: " + e);
}
}
public APDUResponse cardGetStatus() {
APDUCommand plainApdu = new APDUCommand(0xB0, INS_GET_STATUS, 0x00, 0x00, new byte[0]);
log.trace("SATOCHIP SatochipCommandSet cardGetStatus() capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardGetStatus() rapdu: "+ respApdu.toHexString());
this.status= new SatoCardStatus(respApdu);
log.debug("SATOCHIP SatochipCommandSet cardGetStatus(): "+ this.status.toString());
return respApdu;
}
public APDUResponse cardInitiateSecureChannel() throws CardException{
byte[] pubkey= secureChannel.getPublicKey();
APDUCommand plainApdu = new APDUCommand(0xB0, INS_INIT_SECURE_CHANNEL, 0x00, 0x00, pubkey);
log.trace("SATOCHIP SatochipCommandSet cardInitiateSecureChannel capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransport.send(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardInitiateSecureChannel rapdu: "+ respApdu.toHexString());
return respApdu;
}
/****************************************
* CARD MGMT *
****************************************/
public APDUResponse cardSetup(byte pin_tries0, byte[] pin0){
log.debug("SATOCHIP SatochipCommandSet cardSetup()");
// 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){
log.debug("SATOCHIP SatochipCommandSet cardSetup()");
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);
log.trace("SATOCHIP SatochipCommandSet cardSetup capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardSetup rapdu:"+ respApdu.toHexString());
if (respApdu.getSw() == 0x9000){
//setPin0(pin0); // todo: cache value...
} else {
log.error("SATOCHIP SatochipCommandSet cardSetup error:"+ respApdu.toHexString());
}
return respApdu;
}
/****************************************
* PIN MGMT *
****************************************/
public APDUResponse cardVerifyPIN() {
return this.cardVerifyPIN((byte)0, pinCached);
}
public APDUResponse cardVerifyPIN(int pinNbr, String pin) {
log.debug("SATOCHIP SatochipCommandSet cardVerifyPIN()");
if (pin == null){
if (pinCached == null){
// TODO: specific exception
throw new RuntimeException("PIN required!");
}
pin = this.pinCached;
}
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
APDUCommand capdu = new APDUCommand(0xB0, INS_VERIFY_PIN, (byte)pinNbr, 0x00, pinBytes);
log.trace("SATOCHIP SatochipCommandSet cardVerifyPIN() capdu:"+ capdu.toHexString());
APDUResponse rapdu = this.cardTransmit(capdu);
log.trace("SATOCHIP SatochipCommandSet cardVerifyPIN() rapdu: "+ rapdu.toHexString());
// 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 RuntimeException("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 RuntimeException("Wrong PIN, remaining tries: " + pinLeft);
}
// blocked PIN
else if (sw == 0x9c0c){
throw new RuntimeException("Card is blocked!");
}
return rapdu;
}
public APDUResponse cardChangePIN(int pinNbr, String oldPin, String newPin) {
log.debug("SATOCHIP SatochipCommandSet cardChangePIN()");
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);
log.trace("SATOCHIP SatochipCommandSet cardChangePIN() capdu: "+ capdu.toHexString());
APDUResponse rapdu = this.cardTransmit(capdu);
log.trace("SATOCHIP SatochipCommandSet cardChangePIN() rapdu: "+ rapdu.toHexString());
// 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 RuntimeException("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 RuntimeException("Wrong PIN, remaining tries: " + pinLeft);
}
// blocked PIN
else if (sw == 0x9c0c){
throw new RuntimeException("Card is blocked!");
}
return rapdu;
}
/****************************************
* BIP32 *
****************************************/
public APDUResponse cardBip32ImportSeed(byte[] masterseed){
//TODO: check seed (length...)
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_IMPORT_SEED, masterseed.length, 0x00, masterseed);
log.trace("SATOCHIP SatochipCommandSet cardBip32ImportSeed capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardBip32ImportSeed rapdu: "+ respApdu.toHexString());
return respApdu;
}
public APDUResponse cardBip32GetExtendedKey(String stringPath){
log.debug("SATOCHIP SatochipCommandSet cardBip32GetExtendedKey() stringPath: " + stringPath);
KeyPath keyPath = new KeyPath(stringPath);
byte[] bytePath = keyPath.getData();
//log.trace("SATOCHIP SatochipCommandSet cardBip32GetExtendedKey() bytePath: " + Utils.bytesToHex(bytePath));
return cardBip32GetExtendedKey(bytePath);
}
public APDUResponse cardBip32GetExtendedKey(byte[] bytePath){
log.debug("SATOCHIP SatochipCommandSet cardBip32GetExtendedKey() bytePath: " + Utils.bytesToHex(bytePath));
byte p1= (byte) (bytePath.length/4);
APDUCommand plainApdu = new APDUCommand(0xB0, INS_BIP32_GET_EXTENDED_KEY, p1, 0x40, bytePath);
log.trace("SATOCHIP SatochipCommandSet cardBip32GetExtendedKey() capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardBip32GetExtendedKey() rapdu: "+ respApdu.toHexString());
// TODO: check SW code for particular status
// TODO: parse apdu to extract data?
return respApdu;
}
/*
* 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){
// path is of the form 44'/0'/1'
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() path: " + stringPath);
KeyPath keyPath = new KeyPath(stringPath);
byte[] bytePath = keyPath.getData();
int depth = bytePath.length/4;
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() bytePath: " + Utils.bytesToHex(bytePath));
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);
}
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() xtype: " + xtype);
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() Network.get().getXpubHeader(): " + Network.get().getXpubHeader());
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();
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() xpubByte: " + Utils.bytesToHex(xpubByte));
String xpub = Base58.encodeChecked(xpubByte);
log.debug("SATOCHIP SatochipCommandSet cardBip32GetXpub() xpub: " + xpub);
return xpub;
}
/****************************************
* SIGNATURES *
****************************************/
public APDUResponse cardSignTransactionHash(byte keynbr, byte[] txhash, byte[] chalresponse){
log.debug("SATOCHIP SatochipCommandSet cardSignTransactionHash()");
byte[] data;
if (txhash.length !=32){
throw new RuntimeException("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 {
log.error("SATOCHIP SatochipCommandSet cardSignTransactionHash() error: " + "Wrong challenge-response length (should be 20)");
throw new RuntimeException("Wrong challenge-response length (should be 20)");
}
APDUCommand plainApdu = new APDUCommand(0xB0, INS_SIGN_TRANSACTION_HASH, keynbr, 0x00, data);
log.trace("SATOCHIP SatochipCommandSet cardSignTransactionHash() capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardSignTransactionHash() rapdu: "+ respApdu.toHexString());
// TODO: check SW code for particular status
return respApdu;
}
/**
* 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){
log.debug("SATOCHIP SatochipCommandSet cardSignSchnorrHash()");
byte[] data;
if (txhash.length !=32){
throw new RuntimeException("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 {
log.error("SATOCHIP SatochipCommandSet cardSignSchnorrHash() error: " + "Wrong challenge-response length (should be 20)");
throw new RuntimeException("Wrong challenge-response length (should be 20)");
}
APDUCommand plainApdu = new APDUCommand(0xB0, 0x7B, keynbr, 0x00, data);
log.trace("SATOCHIP SatochipCommandSet cardSignSchnorrHash() capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardSignSchnorrHash() rapdu: "+ respApdu.toHexString());
// TODO: check SW code for particular status
return respApdu;
}
/**
* 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
*
* 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){
log.debug("SATOCHIP SatochipCommandSet cardTaprootTweakPrivkey()");
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 RuntimeException("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);
log.trace("SATOCHIP SatochipCommandSet cardTaprootTweakPrivkey() capdu: "+ plainApdu.toHexString());
APDUResponse respApdu = this.cardTransmit(plainApdu);
log.trace("SATOCHIP SatochipCommandSet cardTaprootTweakPrivkey() rapdu: "+ respApdu.toHexString());
// TODO: check SW code for particular status
return respApdu;
}
/****************************************
* 2FA commands *
****************************************/
//todo
/****************************************
* PKI commands *
****************************************/
// todo
}

View file

@ -0,0 +1,155 @@
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 com.sparrowwallet.drongo.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
public class SatochipParser{
private static final Logger log = LoggerFactory.getLogger(SatochipParser.class);
private byte[] authentikey= null;
public SatochipParser(){
}
/****************************************
* PARSER *
****************************************/
public byte[] parseInitiateSecureChannel(APDUResponse rapdu){
try{
byte[] data= rapdu.getData();
log.trace("SATOCHIP SatochipParser parseInitiateSecureChannel() data: " + Utils.bytesToHex(data));
// 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;
byte[] pubkey= recoverPubkey(msg1, sig1, coordx, false); // false: uncompressed
log.trace("SATOCHIP SatochipParser parseInitiateSecureChannel() pubkey: " + Utils.bytesToHex(pubkey));
// todo: recover from sig2
return pubkey;
} catch(Exception e) {
log.error("SATOCHIP SatochipParser parseInitiateSecureChannel() exception: " + e);
throw new RuntimeException("SATOCHIP SatochipParser parseInitiateSecureChannel() exception: ", e);
}
}
public byte[][] parseBip32GetExtendedKey(APDUResponse rapdu){
log.trace("SATOCHIP SatochipParser parseBip32GetExtendedKey()");
try{
byte[][] extendedkey = new byte[2][];
extendedkey[0] = new byte[33]; // pubkey
extendedkey[1] = new byte[32]; // chaincode
byte[] data= rapdu.getData();
log.trace("SATOCHIP SatochipParser parseBip32GetExtendedKey data: " + Utils.bytesToHex(data));
//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)
log.trace("SATOCHIP SatochipParser parseBip32GetExtendedKey pubkey: " + Utils.bytesToHex(pubkey));
// 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) {
log.error("SATOCHIP SatochipParser parseBip32GetExtendedKey() exception: " + e);
throw new RuntimeException("SATOCHIP SatochipParser parseBip32GetExtendedKey() exception: ", e);
}
}
/****************************************
* recovery methods *
****************************************/
public byte[] recoverPubkey(byte[] msg, byte[] dersig, byte[] coordx, Boolean compressed) {
log.trace("SATOCHIP SatochipParser recoverPubkey() coordx: " + Utils.bytesToHex(coordx));
// 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 RuntimeException("SATOCHIP SatochipParser recoverPubkey() Exception: Could not construct a recoverable key. This should never happen.");
}
log.trace("SATOCHIP SatochipParser recoverPubkey() recoveredPubkey: " + Utils.bytesToHex(k.getPubKey()));
return k.getPubKey();
}
}

View file

@ -0,0 +1,245 @@
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 com.sparrowwallet.drongo.Utils;
import javax.crypto.Mac;
import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.security.*; // todo remove?
import java.security.SecureRandom;
import java.util.Arrays;
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 ECKey eckey;
// for session encryption
private SecureRandom random;
private AESKeyCrypter aesCipher;
/**
* Constructs a SecureChannel session on the client.
*/
public SecureChannelSession() {
random = new SecureRandom();
try {
// generate keypair
eckey = new ECKey();
aesCipher = new AESKeyCrypter();
} catch (Exception e) {
log.error("SATOCHIP SecureChannelSession() exception: " + e);
}
}
/**
* 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
log.trace("SATOCHIP SecureChannelSession initiateSecureChannel()");
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 = this.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 = this.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("SATOCHIP SecureChannelSession initiateSecureChannel() exception:" + e);
}
}
public APDUCommand encrypt_secure_channel(APDUCommand plainApdu){
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel()");
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;
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() ivCounter: "+ ivCounter);
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() ivCounterBytes: "+ Utils.bytesToHex(ivCounterBytes));
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() iv: "+ Utils.bytesToHex(iv));
// encrypt data
Key aesKey = new Key(derived_key, null, null);
byte[] encrypted = aesCipher.encrypt(plainBytes, iv, aesKey).getEncryptedBytes();
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() encrypted: "+ Utils.bytesToHex(encrypted));
// 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 = this.getHmacSha1Hash(mac_key, data_to_mac);
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() mac: "+ Utils.bytesToHex(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);
log.trace("SATOCHIP: SecureChannelSession encrypt_secure_channel() full_encrypted_data: " + Utils.bytesToHex(data));
// convert to C-APDU
APDUCommand encryptedApdu= new APDUCommand(0xB0, INS_PROCESS_SECURE_CHANNEL, 0x00, 0x00, data);
return encryptedApdu;
} catch (Exception e) {
e.printStackTrace();
log.error("SATOCHIP: Exception in encrypt_secure_channel: "+ e);
throw new RuntimeException("SATOCHIP: Exception in encrypt_secure_channel:", e);
}
}
public APDUResponse decrypt_secure_channel(APDUResponse encryptedApdu){
log.trace("SATOCHIP SecureChannelSession decrypt_secure_channel()");
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);
log.trace("SATOCHIP SecureChannelSession decrypt_secure_channel() iv: " + Utils.bytesToHex(iv));
log.trace("SATOCHIP SecureChannelSession decrypt_secure_channel() ciphertext: " + Utils.bytesToHex(ciphertext));
// decrypt data
Key aesKey = new Key(derived_key, null, null);
EncryptedData encryptedData = new EncryptedData(iv, ciphertext, null, null);
byte[] decrypted = aesCipher.decrypt(encryptedData, aesKey);
log.trace("SATOCHIP SecureChannelSession decrypt_secure_channel() decrypted: " + Utils.bytesToHex(decrypted));
APDUResponse plainResponse= new APDUResponse(decrypted, (byte)0x90, (byte)0x00);
return plainResponse;
} catch (Exception e) {
e.printStackTrace();
log.error("SATOCHIP SecureChannelSession decrypt_secure_channel() Exception: " + e);
throw new RuntimeException("Exception during secure channel decryption: ", e);
}
}
public boolean initializedSecureChannel(){
return initialized_secure_channel;
}
public byte[] getPublicKey(){
log.trace("SATOCHIP SecureChannelSession getPublicKey() eckey.getPubKey(): " + Utils.bytesToHex(eckey.getPubKey(false)));
return eckey.getPubKey(false); // false: uncompressed
}
public void resetSecureChannel(){
initialized_secure_channel= false;
// todo: generate new eckey?
return;
}
public static byte[] getHmacSha1Hash(byte[] key, byte[] data) {
try{
//log.trace("SATOCHIP SecureChannelSession getHmacSha1Hash() START");
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA1");
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(secretKeySpec);
byte[] macData = mac.doFinal(data);
return macData;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Failed to compute HMACSHA1: ", e);
}
}
}

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
import com.sparrowwallet.sparrow.io.satochip.Satochip;
import javafx.fxml.FXML;
import javafx.scene.control.Accordion;
import org.slf4j.Logger;
@ -22,6 +23,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
private Accordion importAccordion;
public void initializeView() {
log.debug("SATOCHIP HwAirgappedController START");
List<KeystoreFileImport> fileImporters = Collections.emptyList();
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
@ -38,7 +40,8 @@ public class HwAirgappedController extends KeystoreImportDetailController {
}
}
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner());
log.debug("SATOCHIP HwAirgappedController initializeView() - BEFORE cardImporters");
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip());
for(KeystoreCardImport importer : cardImporters) {
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
@ -47,6 +50,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
}
}
}
log.debug("SATOCHIP HwAirgappedController initializeView() - AFTER cardImporters");
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
}

View file

@ -12,7 +12,9 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
private Accordion deviceAccordion;
public void initializeView(List<Device> devices) {
//log.debug("SATOCHIP HwUsbDevicesController initializeView START");
for(Device device : devices) {
//log.debug("SATOCHIP HwUsbDevicesController initializeView device: " + device);
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
deviceAccordion.getPanes().add(devicePane);

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB