mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-11 09:08:02 +00:00
add support for keycard via smart card interface
This commit is contained in:
@@ -11,6 +11,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.keycard.KeycardApi;
|
||||
import com.sparrowwallet.sparrow.io.satochip.SatoCardApi;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -58,6 +59,13 @@ public abstract class CardApi {
|
||||
//ignore
|
||||
}
|
||||
|
||||
try {
|
||||
KeycardApi keycardApi = new KeycardApi(null, null);
|
||||
cards.add(keycardApi.getCardType());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
@@ -70,6 +78,10 @@ public abstract class CardApi {
|
||||
return new SatoCardApi(walletModel, pin);
|
||||
}
|
||||
|
||||
if(walletModel == WalletModel.KEYCARD) {
|
||||
return new KeycardApi(walletModel, pin);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CLA of the APDU
|
||||
*
|
||||
* @return the CLA of the APDU
|
||||
*/
|
||||
public int getCla() {
|
||||
return cla;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the INS of the APDU
|
||||
*
|
||||
* @return the INS of the APDU
|
||||
*/
|
||||
public int getIns() {
|
||||
return ins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the P1 of the APDU
|
||||
*
|
||||
* @return the P1 of the APDU
|
||||
*/
|
||||
public int getP1() {
|
||||
return p1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the P2 of the APDU
|
||||
*
|
||||
* @return the P2 of the APDU
|
||||
*/
|
||||
public int getP2() {
|
||||
return p2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data field of the APDU
|
||||
*
|
||||
* @return the data field of the APDU
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether LE is sent or not.
|
||||
*
|
||||
* @return whether LE is sent or not
|
||||
*/
|
||||
public boolean getNeedsLE() {
|
||||
return this.needsLE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
/**
|
||||
* Exception thrown when the response APDU from the card contains unexpected SW or data.
|
||||
*/
|
||||
public class APDUException extends Exception {
|
||||
public final int sw;
|
||||
|
||||
/**
|
||||
* Creates an exception with SW and message.
|
||||
*
|
||||
* @param sw the status word
|
||||
* @param message a descriptive message of the error
|
||||
*/
|
||||
public APDUException(int sw, String message) {
|
||||
super(message + ", 0x" + String.format("%04X", sw));
|
||||
this.sw = sw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an exception with a message.
|
||||
*
|
||||
* @param message a descriptive message of the error
|
||||
*/
|
||||
public APDUException(String message) {
|
||||
super(message);
|
||||
this.sw = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 true if the SW is 0x9000.
|
||||
*
|
||||
* @return true if the SW is 0x9000.
|
||||
*/
|
||||
public boolean isOK() {
|
||||
return this.sw == SW_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is 0x9000. Throws an exception if it isn't
|
||||
*
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkOK() throws APDUException {
|
||||
return this.checkSW(SW_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is contained in the given list. Throws an exception if it isn't.
|
||||
*
|
||||
* @param codes the list of SWs to match.
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkSW(int... codes) throws APDUException {
|
||||
for(int code : codes) {
|
||||
if(this.sw == code) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
switch(this.sw) {
|
||||
case SW_SECURITY_CONDITION_NOT_SATISFIED:
|
||||
throw new APDUException(this.sw, "security condition not satisfied");
|
||||
case SW_AUTHENTICATION_METHOD_BLOCKED:
|
||||
throw new APDUException(this.sw, "authentication method blocked");
|
||||
default:
|
||||
throw new APDUException(this.sw, "Unexpected error SW");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is 0x9000. Throws an exception with the given message if it isn't
|
||||
*
|
||||
* @param message the error message
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkOK(String message) throws APDUException {
|
||||
return checkSW(message, SW_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the SW is contained in the given list. Throws an exception with the given message if it isn't.
|
||||
*
|
||||
* @param message the error message
|
||||
* @param codes the list of SWs to match.
|
||||
* @return this object, to simplify chaining
|
||||
* @throws APDUException if the SW is not 0x9000
|
||||
*/
|
||||
public APDUResponse checkSW(String message, int... codes) throws APDUException {
|
||||
for(int code : codes) {
|
||||
if(this.sw == code) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
throw new APDUException(this.sw, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks response from an authentication command (VERIFY PIN, UNBLOCK PUK)
|
||||
*
|
||||
* @throws CardAuthorizationException wrong PIN
|
||||
* @throws APDUException unexpected response
|
||||
*/
|
||||
public APDUResponse checkAuthOK() throws CardAuthorizationException, APDUException {
|
||||
if((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
|
||||
int retryAttempts = sw2 & 0x0F;
|
||||
throw new CardAuthorizationException("Wrong PIN, remaining tries: " + retryAttempts);
|
||||
} else {
|
||||
return checkOK();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
/**
|
||||
* Parses the response from a SELECT command. If the card has not yet received the INIT command the isInitializedCard
|
||||
* will return false and only the getSecureChannelPubKey method will return a valid value.
|
||||
*/
|
||||
public class ApplicationInfo {
|
||||
private boolean initializedCard;
|
||||
private byte[] instanceUID;
|
||||
private byte[] secureChannelPubKey;
|
||||
private short appVersion;
|
||||
private byte freePairingSlots;
|
||||
private byte[] keyUID;
|
||||
private byte capabilities;
|
||||
|
||||
public static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
|
||||
public static final byte TLV_PUB_KEY = (byte) 0x80;
|
||||
public static final byte TLV_UID = (byte) 0x8F;
|
||||
public static final byte TLV_KEY_UID = (byte) 0x8E;
|
||||
public static final byte TLV_CAPABILITIES = (byte) 0x8D;
|
||||
|
||||
static final byte CAPABILITY_SECURE_CHANNEL = (byte) 0x01;
|
||||
static final byte CAPABILITY_KEY_MANAGEMENT = (byte) 0x02;
|
||||
static final byte CAPABILITY_CREDENTIALS_MANAGEMENT = (byte) 0x04;
|
||||
static final byte CAPABILITY_NDEF = (byte) 0x08;
|
||||
static final byte CAPABILITY_FACTORY_RESET = (byte) 0x10;
|
||||
|
||||
static final byte CAPABILITIES_ALL = CAPABILITY_SECURE_CHANNEL | CAPABILITY_KEY_MANAGEMENT | CAPABILITY_CREDENTIALS_MANAGEMENT | CAPABILITY_NDEF | CAPABILITY_FACTORY_RESET;
|
||||
|
||||
/**
|
||||
* Constructs an object by parsing the TLV data.
|
||||
*
|
||||
* @param tlvData the raw response data from the card
|
||||
* @throws IllegalArgumentException the TLV does not follow the allowed format
|
||||
*/
|
||||
public ApplicationInfo(byte[] tlvData) throws IllegalArgumentException {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
|
||||
int topTag = tlv.readTag();
|
||||
tlv.unreadLastTag();
|
||||
|
||||
if(topTag == TLV_PUB_KEY) {
|
||||
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
initializedCard = false;
|
||||
capabilities = CAPABILITY_CREDENTIALS_MANAGEMENT;
|
||||
|
||||
if(secureChannelPubKey.length > 0) {
|
||||
capabilities |= CAPABILITY_SECURE_CHANNEL;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
tlv.enterConstructed(TLV_APPLICATION_INFO_TEMPLATE);
|
||||
instanceUID = tlv.readPrimitive(TLV_UID);
|
||||
secureChannelPubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
appVersion = (short) tlv.readInt();
|
||||
freePairingSlots = (byte) tlv.readInt();
|
||||
keyUID = tlv.readPrimitive(TLV_KEY_UID);
|
||||
|
||||
if(tlv.readTag() != TinyBERTLV.END_OF_TLV) {
|
||||
tlv.unreadLastTag();
|
||||
capabilities = tlv.readPrimitive(TLV_CAPABILITIES)[0];
|
||||
} else {
|
||||
capabilities = CAPABILITIES_ALL;
|
||||
}
|
||||
|
||||
initializedCard = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the card is initialized or not. If this method returns false, only the getSecureChannelPubKey method
|
||||
* will return a valid value.
|
||||
*
|
||||
* @return true if initialized, false otherwise
|
||||
*/
|
||||
public boolean isInitializedCard() {
|
||||
return initializedCard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to discover if the card has a master key.
|
||||
*
|
||||
* @return true if the card has a master key, false otherwise
|
||||
*/
|
||||
public boolean hasMasterKey() {
|
||||
return (keyUID != null) && (keyUID.length != 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The instance UID of the applet. This ID never changes for the lifetime of the applet.
|
||||
*
|
||||
* @return the instance UID
|
||||
*/
|
||||
public byte[] getInstanceUID() {
|
||||
return instanceUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public key to be used for secure channel opening. Usually handled internally by the KeycardCommandSet.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getSecureChannelPubKey() {
|
||||
return secureChannelPubKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The application version, encoded as a short. The msb is the major revision number and the lsb is the minor one.
|
||||
*
|
||||
* @return the application version
|
||||
*/
|
||||
public short getAppVersion() {
|
||||
return appVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A formatted application version.
|
||||
*
|
||||
* @return the string representation of the application version
|
||||
*/
|
||||
public String getAppVersionString() {
|
||||
return getAppVersionString(appVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* A formatted application version.
|
||||
*
|
||||
* @return the string representation of the application version
|
||||
*/
|
||||
static String getAppVersionString(short appVersion) {
|
||||
return (appVersion >> 8) + "." + (appVersion & 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of remaining pairing slots. If zero is returned, no further pairing is possible.
|
||||
*
|
||||
* @return the number of remaining pairing slots
|
||||
*/
|
||||
public byte getFreePairingSlots() {
|
||||
return freePairingSlots;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UID of the master key on this card. Changes every time a different master key is stored. It has zero length if
|
||||
* no key is on the card.
|
||||
*
|
||||
* @return the Key UID.
|
||||
*/
|
||||
public byte[] getKeyUID() {
|
||||
return keyUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the capability descriptor for the device.
|
||||
*
|
||||
* @return the capability descriptor for the device.
|
||||
*/
|
||||
public byte getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Secure Channel capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasSecureChannelCapability() {
|
||||
return (capabilities & CAPABILITY_SECURE_CHANNEL) == CAPABILITY_SECURE_CHANNEL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Key Management capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasKeyManagementCapability() {
|
||||
return (capabilities & CAPABILITY_KEY_MANAGEMENT) == CAPABILITY_KEY_MANAGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Credentials Management capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasCredentialsManagementCapability() {
|
||||
return (capabilities & CAPABILITY_CREDENTIALS_MANAGEMENT) == CAPABILITY_CREDENTIALS_MANAGEMENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the NDEF capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasNDEFCapability() {
|
||||
return (capabilities & CAPABILITY_NDEF) == CAPABILITY_NDEF;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the device supports the Factory Reset capability.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean hasFactoryResetCapability() {
|
||||
return (capabilities & CAPABILITY_FACTORY_RESET) == CAPABILITY_FACTORY_RESET;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
|
||||
/**
|
||||
* Represents a BIP32 keypair. This can be a master key or any other key in the path. Contains convenience method to
|
||||
* read and write formats the the card understands.
|
||||
*/
|
||||
public class BIP32KeyPair {
|
||||
private byte[] privateKey;
|
||||
private byte[] chainCode;
|
||||
private byte[] publicKey;
|
||||
|
||||
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
|
||||
static final byte TLV_PUB_KEY = (byte) 0x80;
|
||||
static final byte TLV_PRIV_KEY = (byte) 0x81;
|
||||
static final byte TLV_CHAIN_CODE = (byte) 0x82;
|
||||
|
||||
/**
|
||||
* Constructs a BIP32 keypair from a KEY TEMPLATE TLV. Data can be the output of the EXPORT KEY command.
|
||||
*
|
||||
* @param tlvData the TLV data
|
||||
* @return the BIP32 keypair
|
||||
*/
|
||||
public static BIP32KeyPair fromTLV(byte[] tlvData) {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
tlv.enterConstructed(TLV_KEY_TEMPLATE);
|
||||
|
||||
byte[] pubKey = null;
|
||||
byte[] privKey = null;
|
||||
byte[] chainCode = null;
|
||||
|
||||
int tag = tlv.readTag();
|
||||
|
||||
if(tag == TLV_PUB_KEY) {
|
||||
tlv.unreadLastTag();
|
||||
pubKey = tlv.readPrimitive(TLV_PUB_KEY);
|
||||
tag = tlv.readTag();
|
||||
}
|
||||
|
||||
if(tag == TLV_PRIV_KEY) {
|
||||
tlv.unreadLastTag();
|
||||
privKey = tlv.readPrimitive(TLV_PRIV_KEY);
|
||||
tag = tlv.readTag();
|
||||
}
|
||||
|
||||
if(tag == TLV_CHAIN_CODE) {
|
||||
tlv.unreadLastTag();
|
||||
chainCode = tlv.readPrimitive(TLV_CHAIN_CODE);
|
||||
}
|
||||
|
||||
return new BIP32KeyPair(privKey, chainCode, pubKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low level constructor. If the private key is not null, the public key can be omitted and it will be calculated
|
||||
* automatically.
|
||||
*
|
||||
* @param privateKey the private key
|
||||
* @param chainCode the chain code
|
||||
* @param publicKey the public key
|
||||
*/
|
||||
public BIP32KeyPair(byte[] privateKey, byte[] chainCode, byte[] publicKey) {
|
||||
this.privateKey = privateKey;
|
||||
this.chainCode = chainCode;
|
||||
|
||||
if(publicKey != null) {
|
||||
this.publicKey = publicKey;
|
||||
} else {
|
||||
calculatePublicKey();
|
||||
}
|
||||
}
|
||||
|
||||
private void calculatePublicKey() {
|
||||
ECKey key = ECKey.fromPrivate(this.privateKey, false);
|
||||
this.publicKey = key.getPubKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLV representation of this object.
|
||||
*
|
||||
* @return the TLV representation of this object.
|
||||
*/
|
||||
public byte[] toTLV() {
|
||||
return toTLV(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TLV representation of this object, optionally omitting the public component.
|
||||
*
|
||||
* @return the TLV representation of this object.
|
||||
*/
|
||||
public byte[] toTLV(boolean includePublic) {
|
||||
int privLen = privateKey.length;
|
||||
int privOff = 0;
|
||||
|
||||
if(privateKey[0] == 0x00) {
|
||||
privOff++;
|
||||
privLen--;
|
||||
}
|
||||
|
||||
int off = 0;
|
||||
int totalLength = includePublic ? (publicKey.length + 2) : 0;
|
||||
totalLength += (privLen + 2);
|
||||
totalLength += isExtended() ? (chainCode.length + 2) : 0;
|
||||
|
||||
if(totalLength > 127) {
|
||||
totalLength += 3;
|
||||
} else {
|
||||
totalLength += 2;
|
||||
}
|
||||
|
||||
byte[] data = new byte[totalLength];
|
||||
data[off++] = TLV_KEY_TEMPLATE;
|
||||
|
||||
if(totalLength > 127) {
|
||||
data[off++] = (byte) 0x81;
|
||||
data[off++] = (byte) (totalLength - 3);
|
||||
} else {
|
||||
data[off++] = (byte) (totalLength - 2);
|
||||
}
|
||||
|
||||
if(includePublic) {
|
||||
data[off++] = TLV_PUB_KEY;
|
||||
data[off++] = (byte) publicKey.length;
|
||||
System.arraycopy(publicKey, 0, data, off, publicKey.length);
|
||||
off += publicKey.length;
|
||||
}
|
||||
|
||||
data[off++] = TLV_PRIV_KEY;
|
||||
data[off++] = (byte) privLen;
|
||||
System.arraycopy(privateKey, privOff, data, off, privLen);
|
||||
off += privLen;
|
||||
|
||||
if(isExtended()) {
|
||||
data[off++] = (byte) TLV_CHAIN_CODE;
|
||||
data[off++] = (byte) chainCode.length;
|
||||
System.arraycopy(chainCode, 0, data, off, chainCode.length);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the private key. Might be null.
|
||||
*
|
||||
* @return the private key
|
||||
*/
|
||||
public byte[] getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chain code. Might be null.
|
||||
*
|
||||
* @return the chain code
|
||||
*/
|
||||
public byte[] getChainCode() {
|
||||
return chainCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key. Is never null.
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if only the public key is contained, false otherwise.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean isPublicOnly() {
|
||||
return privateKey == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the chain code is contained, false otherwise.
|
||||
*
|
||||
* @return true or false
|
||||
*/
|
||||
public boolean isExtended() {
|
||||
return chainCode != null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A channel to transcieve ISO7816-4 APDUs.
|
||||
*/
|
||||
public interface CardChannel {
|
||||
/**
|
||||
* Sends the given C-APDU and returns an R-APDU.
|
||||
*
|
||||
* @param cmd the command to send
|
||||
* @return the card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
APDUResponse send(APDUCommand cmd) throws IOException;
|
||||
|
||||
/**
|
||||
* True if connected, false otherwise
|
||||
*
|
||||
* @return true if connected, false otherwise
|
||||
*/
|
||||
boolean isConnected();
|
||||
|
||||
/**
|
||||
* Returns the iteration count for deriving the pairing key from the pairing password. The default is 50000 and is
|
||||
* should only be changed for devices where the PBKDF2 is calculated on-board and the resource do not permit a
|
||||
* high iteration count. If a lower count is used other security mechanism should be used to prevent brute force
|
||||
* attacks.
|
||||
*
|
||||
* @return the iteration count
|
||||
*/
|
||||
default int pairingPasswordPBKDF2IterationCount() {
|
||||
return 50000;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Identifiers {
|
||||
public static final byte[] PACKAGE_AID = Utils.hexToBytes("A0000008040001");
|
||||
|
||||
public static final byte[] KEYCARD_AID = Utils.hexToBytes("A000000804000101");
|
||||
public static final int KEYCARD_DEFAULT_INSTANCE_IDX = 1;
|
||||
|
||||
public static final byte[] NDEF_AID = Utils.hexToBytes("A000000804000102");
|
||||
public static final byte[] NDEF_INSTANCE_AID = Utils.hexToBytes("D2760000850101");
|
||||
|
||||
public static final byte[] CASH_AID = Utils.hexToBytes("A000000804000103");
|
||||
public static final byte[] CASH_INSTANCE_AID = Utils.hexToBytes("A00000080400010301");
|
||||
|
||||
public static final byte[] IDENT_AID = Utils.hexToBytes("A000000804000104");
|
||||
public static final byte[] IDENT_INSTANCE_AID = Utils.hexToBytes("A00000080400010401");
|
||||
|
||||
/**
|
||||
* Gets the instance AID of the default instance of the Keycard applet.
|
||||
*
|
||||
* @return the instance AID of the Keycard applet
|
||||
*/
|
||||
public static byte[] getKeycardInstanceAID() {
|
||||
return getKeycardInstanceAID(KEYCARD_DEFAULT_INSTANCE_IDX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the instance AID of the Keycard applet with the given index. Since multiple instances of the Keycard applet
|
||||
* could be installed in parallel, this method allows selecting a specific instance. The index is between 01 and ff
|
||||
*
|
||||
* @return the instance AID of the Keycard applet
|
||||
*/
|
||||
public static byte[] getKeycardInstanceAID(int instanceIdx) {
|
||||
if(instanceIdx < 0x01 || instanceIdx > 0xff) {
|
||||
throw new IllegalArgumentException("The instance index must be between 1 and 255");
|
||||
}
|
||||
|
||||
byte[] instanceAID = Arrays.copyOf(KEYCARD_AID, KEYCARD_AID.length + 1);
|
||||
instanceAID[KEYCARD_AID.length] = (byte) instanceIdx;
|
||||
return instanceAID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import java.util.StringTokenizer;
|
||||
|
||||
/**
|
||||
* Keypath object to be used with the KeycardCommandSet
|
||||
*/
|
||||
public class KeyPath {
|
||||
private int source;
|
||||
private byte[] data;
|
||||
|
||||
/**
|
||||
* Parses a keypath into a byte array and source parameter to be used with the KeycardCommandSet object.
|
||||
* <p>
|
||||
* A valid string is composed of a minimum of one and a maximum of 11 components separated by "/".
|
||||
* <p>
|
||||
* The first component can be either "m", indicating the master key, "..", indicating the parent of the current key,
|
||||
* or "." indicating the current key. It can also be omitted, in which case it is considered the same as being ".".
|
||||
* <p>
|
||||
* All other components are positive integers fitting in 31 bit, eventually suffixed by an apostrophe (') sign,
|
||||
* which indicates an hardened key.
|
||||
* <p>
|
||||
* An example of a valid path is "m/44'/0'/0'/0/0"
|
||||
*
|
||||
* @param keypath the keypath as a string
|
||||
*/
|
||||
public KeyPath(String keypath) {
|
||||
StringTokenizer tokenizer = new StringTokenizer(keypath, "/");
|
||||
|
||||
String sourceOrFirstElement = tokenizer.nextToken();
|
||||
|
||||
switch(sourceOrFirstElement) {
|
||||
case "m":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_MASTER;
|
||||
break;
|
||||
case "..":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_PARENT;
|
||||
break;
|
||||
case ".":
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
|
||||
break;
|
||||
default:
|
||||
source = KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT;
|
||||
tokenizer = new StringTokenizer(keypath, "/"); // rewind
|
||||
break;
|
||||
}
|
||||
|
||||
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, int source) {
|
||||
this.data = data;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public KeyPath(byte[] data) {
|
||||
this(data, KeycardCommandSet.DERIVE_P1_SOURCE_MASTER);
|
||||
}
|
||||
|
||||
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 source of the derive command.
|
||||
*
|
||||
* @return the source of the derive command
|
||||
*/
|
||||
public int getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* The byte encoded key path.
|
||||
*
|
||||
* @return byte encoded key path
|
||||
*/
|
||||
public byte[] getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
switch(source) {
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_MASTER:
|
||||
sb.append('m');
|
||||
break;
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_PARENT:
|
||||
sb.append("..");
|
||||
break;
|
||||
case KeycardCommandSet.DERIVE_P1_SOURCE_CURRENT:
|
||||
sb.append('.');
|
||||
break;
|
||||
}
|
||||
|
||||
for(int i = 0; i < this.data.length; i += 4) {
|
||||
sb.append('/');
|
||||
appendComponent(sb, i);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private void appendComponent(StringBuffer sb, int i) {
|
||||
int num = ((this.data[i] & 0x7f) << 24) | ((this.data[i + 1] & 0xff) << 16) | ((this.data[i + 2] & 0xff) << 8) | (this.data[i + 3] & 0xff);
|
||||
sb.append(num);
|
||||
|
||||
if((this.data[i] & 0x80) == 0x80) {
|
||||
sb.append('\'');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.util.List;
|
||||
|
||||
public class Keycard implements KeystoreCardImport {
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() throws CardException {
|
||||
KeycardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new KeycardApi(WalletModel.KEYCARD, null);
|
||||
return cardApi.isInitialized();
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(String pin, byte[] entropy, StringProperty messageProperty) throws CardException {
|
||||
if(!StringUtils.isNumeric(pin)) {
|
||||
throw new CardException("PIN must be all digits.");
|
||||
}
|
||||
|
||||
if(pin.length() != 6) {
|
||||
throw new CardException("PIN must be 6 digit longs.");
|
||||
}
|
||||
|
||||
KeycardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new KeycardApi(WalletModel.KEYCARD, pin);
|
||||
if(cardApi.isInitialized()) {
|
||||
throw new IllegalStateException("Card is already initialized.");
|
||||
}
|
||||
|
||||
cardApi.initialize(0, entropy);
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
|
||||
if(!StringUtils.isNumeric(pin)) {
|
||||
throw new ImportException("PIN must be all digits.");
|
||||
}
|
||||
|
||||
if(pin.length() != 6) {
|
||||
throw new ImportException("PIN must be 6 digit longs.");
|
||||
}
|
||||
|
||||
KeycardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new KeycardApi(WalletModel.KEYCARD, pin);
|
||||
if(!cardApi.isInitialized()) {
|
||||
throw new IllegalStateException("Card is not initialized.");
|
||||
}
|
||||
cardApi.setDerivation(derivation);
|
||||
return cardApi.getKeystore();
|
||||
} catch(Exception e) {
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the keystore from your Keycard by inserting or placing it on the card reader.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Keycard";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.KEYCARD;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
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 com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class KeycardApi extends CardApi {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeycardApi.class);
|
||||
|
||||
private final WalletModel cardType;
|
||||
private final KeycardTransport cardTransport;
|
||||
private final KeycardCommandSet cardProtocol;
|
||||
private final String pin;
|
||||
private String basePath = null;
|
||||
|
||||
public KeycardApi(WalletModel cardType, String pin) throws CardException {
|
||||
this.cardType = cardType;
|
||||
this.cardTransport = new KeycardTransport(Identifiers.getKeycardInstanceAID());
|
||||
this.cardProtocol = new KeycardCommandSet(cardTransport);
|
||||
this.pin = pin;
|
||||
|
||||
try {
|
||||
this.cardProtocol.select().checkOK();
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() throws CardException {
|
||||
return getStatus().hasMasterKey();
|
||||
}
|
||||
|
||||
//TODO
|
||||
@Override
|
||||
public void initialize(int slot, byte[] seedBytes) throws CardException {
|
||||
// TODO check device certificate
|
||||
ApplicationInfo cardStatus = this.getStatus();
|
||||
|
||||
if(!cardStatus.isInitializedCard()) {
|
||||
try {
|
||||
String puk = String.format("%012d", new SecureRandom().nextLong(999999999999L));
|
||||
this.cardProtocol.init(pin, puk, "KeycardDefaultPairing").checkOK();
|
||||
this.cardProtocol.select().checkOK();
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
}
|
||||
|
||||
if(!cardStatus.hasMasterKey()) {
|
||||
try {
|
||||
this.authenticate();
|
||||
this.cardProtocol.autoUnpair();
|
||||
this.cardProtocol.loadKey(seedBytes).checkOK();
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getCardType() throws CardException {
|
||||
return WalletModel.KEYCARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentSlot() throws CardException {
|
||||
throw new CardException("Keycard does not support slots");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType getDefaultScriptType() {
|
||||
return ScriptType.P2WPKH;
|
||||
}
|
||||
|
||||
ApplicationInfo getStatus() {
|
||||
return this.cardProtocol.getApplicationInfo();
|
||||
}
|
||||
|
||||
void authenticate() throws IOException, APDUException, CardAuthorizationException {
|
||||
this.cardProtocol.autoPair("KeycardDefaultPairing");
|
||||
this.cardProtocol.autoOpenSecureChannel();
|
||||
this.cardProtocol.verifyPIN(pin).checkAuthOK();
|
||||
this.cardProtocol.autoUnpair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Void> getAuthDelayService() throws CardException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresBackup() throws CardException {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<String> getBackupService() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changePin(String newPin) throws CardException {
|
||||
try {
|
||||
this.authenticate();
|
||||
this.cardProtocol.changePIN(newPin).checkOK();
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void setDerivation(List<ChildNumber> derivation) throws CardException {
|
||||
this.basePath = KeyDerivation.writePath(derivation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Void> getInitializationService(byte[] seedBytes, StringProperty messageProperty) {
|
||||
return new KeycardApi.CardInitializationService(seedBytes, messageProperty);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
return new CardImportPane.CardImportService(new Keycard(), pin, derivation, messageProperty);
|
||||
}
|
||||
|
||||
private byte[] compressedPub(byte[] uncompressedPub) {
|
||||
byte[] compressed = Arrays.copyOfRange(uncompressedPub, 0, 33);
|
||||
compressed[0] = (byte) (0x02 | (uncompressedPub[64] & 0x1));
|
||||
return compressed;
|
||||
}
|
||||
|
||||
private String cardBip32GetXpub(String stringPath, ExtendedKey.Header xtype) throws IOException, APDUException {
|
||||
KeyPath keyPath = new KeyPath(stringPath);
|
||||
int bytepathLen = keyPath.getData().length;
|
||||
int depth = bytepathLen / 4;
|
||||
APDUResponse rapdu = this.cardProtocol.exportKey(keyPath.getData(), keyPath.getSource(), false, KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC).checkOK();
|
||||
BIP32KeyPair extendedkey = BIP32KeyPair.fromTLV(rapdu.getData());
|
||||
|
||||
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
|
||||
APDUResponse rapdu2 = this.cardProtocol.exportKey(Arrays.copyOfRange(keyPath.getData(), 0, bytepathLen - 4), keyPath.getSource(), false, KeycardCommandSet.EXPORT_KEY_P2_PUBLIC_ONLY).checkOK();
|
||||
BIP32KeyPair keyParent = BIP32KeyPair.fromTLV(rapdu2.getData());
|
||||
byte[] identifier = Utils.sha256hash160(compressedPub(keyParent.getPublicKey()));
|
||||
fingerprint = Arrays.copyOfRange(identifier, 0, 4);
|
||||
childNumber = Arrays.copyOfRange(keyPath.getData(), bytepathLen - 4, bytepathLen);
|
||||
}
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(78);
|
||||
buffer.putInt(xtype.getHeader());
|
||||
buffer.put((byte) depth);
|
||||
buffer.put(fingerprint);
|
||||
buffer.put(childNumber);
|
||||
buffer.put(extendedkey.getChainCode()); // chaincode
|
||||
buffer.put(compressedPub(extendedkey.getPublicKey())); // pubkey (compressed)
|
||||
byte[] xpubByte = buffer.array();
|
||||
|
||||
return Base58.encodeChecked(xpubByte);
|
||||
}
|
||||
|
||||
/*
|
||||
* Keycard 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 Keycard.java:getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty)
|
||||
* In Keycard: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 {
|
||||
String keyDerivationString = (this.basePath != null ? this.basePath : getDefaultScriptType().getDefaultDerivationPath());
|
||||
ExtendedKey.Header xtype = Network.get().getXpubHeader();
|
||||
|
||||
String xpub;
|
||||
String masterXpub;
|
||||
try {
|
||||
this.authenticate();
|
||||
xpub = this.cardBip32GetXpub(keyDerivationString, xtype);
|
||||
masterXpub = this.cardBip32GetXpub("m", xtype);
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
|
||||
ExtendedKey extendedKey = ExtendedKey.fromDescriptor(xpub);
|
||||
ExtendedKey masterExtendedKey = ExtendedKey.fromDescriptor(masterXpub);
|
||||
String masterFingerprint = Utils.bytesToHex(masterExtendedKey.getKey().getFingerprint());
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString, true);
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.KEYCARD.toDisplayString());
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
keystore.setWalletModel(WalletModel.KEYCARD);
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||
return new KeycardApi.SignService(wallet, psbt, messageProperty);
|
||||
}
|
||||
|
||||
void sign(Wallet wallet, PSBT psbt) throws CardException {
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(!psbtInput.isSigned()) {
|
||||
WalletNode signingNode = signingNodes.get(psbtInput);
|
||||
List<Keystore> keystores = wallet.getKeystores();
|
||||
// recover derivation path from Keycard keystore
|
||||
String fullPath = null;
|
||||
for(int i = 0; i < keystores.size(); i++) {
|
||||
Keystore keystore = keystores.get(i);
|
||||
WalletModel walletModel = keystore.getWalletModel();
|
||||
if(walletModel == WalletModel.KEYCARD) {
|
||||
String basePath = keystore.getKeyDerivation().getDerivationPath();
|
||||
String extendedPath = signingNode.getDerivationPath().substring(1);
|
||||
fullPath = basePath + extendedPath;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(fullPath == null) {
|
||||
// recover a default derivation path from first keystore
|
||||
Keystore keystore = keystores.get(0);
|
||||
String basePath = keystore.getKeyDerivation().getDerivationPath();
|
||||
String extendedPath = signingNode.getDerivationPath().substring(1);
|
||||
fullPath = basePath + extendedPath;
|
||||
}
|
||||
psbtInput.sign(new KeycardApi.CardPSBTInputSigner(signingNode, fullPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
return new KeycardApi.SignMessageService(message, scriptType, derivation, messageProperty);
|
||||
}
|
||||
|
||||
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
|
||||
String fullpath = KeyDerivation.writePath(derivation);
|
||||
ECKey pubkey;
|
||||
|
||||
try {
|
||||
authenticate();
|
||||
APDUResponse rapdu = cardProtocol.exportKey(fullpath, false, KeycardCommandSet.EXPORT_KEY_P2_PUBLIC_ONLY).checkOK();
|
||||
BIP32KeyPair keys = BIP32KeyPair.fromTLV(rapdu.getData());
|
||||
pubkey = ECKey.fromPublicOnly(compressedPub(keys.getPublicKey()));
|
||||
} catch(IOException | APDUException e) {
|
||||
throw new CardException(e);
|
||||
}
|
||||
|
||||
// sign msg
|
||||
return pubkey.signMessage(message, scriptType, hash -> {
|
||||
try {
|
||||
// do the signature with Keycard
|
||||
APDUResponse rapdu2 = cardProtocol.signWithPath(hash.getBytes(), fullpath, false).checkOK();
|
||||
RecoverableSignature sig = new RecoverableSignature(hash.getBytes(), rapdu2.getData());
|
||||
return new ECDSASignature(new BigInteger(1, sig.getR()), new BigInteger(1, sig.getS()));
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<ECKey> getPrivateKeyService(Integer slot, StringProperty messageProperty) {
|
||||
throw new UnsupportedOperationException("Keycard does not support private key export");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Address> getAddressService(StringProperty messageProperty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
try {
|
||||
cardTransport.disconnect();
|
||||
} catch(CardException e) {
|
||||
log.error("Error disconnecting Keycard" + e);
|
||||
}
|
||||
}
|
||||
|
||||
public class CardInitializationService extends Service<Void> {
|
||||
private final byte[] seedBytes;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardInitializationService(byte[] seedBytes, StringProperty messageProperty) {
|
||||
this.seedBytes = seedBytes;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
if(seedBytes == null) {
|
||||
throw new CardException("Failed to initialize Keycard - no seed provided");
|
||||
}
|
||||
|
||||
initialize(0, seedBytes);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SignService extends Service<PSBT> {
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public SignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<PSBT> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected PSBT call() throws Exception {
|
||||
sign(wallet, psbt);
|
||||
return psbt;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class CardPSBTInputSigner implements PSBTInputSigner {
|
||||
private final WalletNode signingNode;
|
||||
private final String fullPath;
|
||||
private ECKey pubkey;
|
||||
|
||||
// todo: provide derivationpath instead of WalletNode??
|
||||
public CardPSBTInputSigner(WalletNode signingNode, String fullPath) {
|
||||
this.signingNode = signingNode;
|
||||
this.fullPath = fullPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
|
||||
try {
|
||||
// verify PIN
|
||||
authenticate();
|
||||
|
||||
if(signatureType == TransactionSignature.Type.ECDSA) {
|
||||
// do the signature with Keycard
|
||||
APDUResponse rapdu = cardProtocol.signWithPath(hash.getBytes(), fullPath, false).checkOK();
|
||||
RecoverableSignature sig = new RecoverableSignature(hash.getBytes(), rapdu.getData());
|
||||
pubkey = ECKey.fromPublicOnly(compressedPub(sig.getPublicKey()));
|
||||
|
||||
ECDSASignature ecdsaSig = new ECDSASignature(new BigInteger(1, sig.getR()), new BigInteger(1, sig.getS())).toCanonicalised();
|
||||
TransactionSignature txSig = new TransactionSignature(ecdsaSig, sigHash);
|
||||
|
||||
boolean isCorrect = pubkey.verify(hash, txSig);
|
||||
return txSig;
|
||||
} else {
|
||||
throw new CardException(WalletModel.KEYCARD.toDisplayString() + " cannot sign Taproot transactions");
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getPubKey() {
|
||||
return pubkey;
|
||||
}
|
||||
}
|
||||
|
||||
public class SignMessageService extends Service<String> {
|
||||
private final String message;
|
||||
private final ScriptType scriptType;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public SignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.message = message;
|
||||
this.scriptType = scriptType;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
return signMessage(message, scriptType, derivation);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,870 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 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 KeycardCommandSet {
|
||||
static final byte INS_INIT = (byte) 0xFE;
|
||||
static final byte INS_FACTORY_RESET = (byte) 0xFD;
|
||||
static final byte INS_GET_STATUS = (byte) 0xF2;
|
||||
static final byte INS_SET_NDEF = (byte) 0xF3;
|
||||
static final byte INS_IDENTIFY_CARD = (byte) 0x14;
|
||||
static final byte INS_VERIFY_PIN = (byte) 0x20;
|
||||
static final byte INS_CHANGE_PIN = (byte) 0x21;
|
||||
static final byte INS_UNBLOCK_PIN = (byte) 0x22;
|
||||
static final byte INS_LOAD_KEY = (byte) 0xD0;
|
||||
static final byte INS_DERIVE_KEY = (byte) 0xD1;
|
||||
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
|
||||
static final byte INS_REMOVE_KEY = (byte) 0xD3;
|
||||
static final byte INS_GENERATE_KEY = (byte) 0xD4;
|
||||
static final byte INS_SIGN = (byte) 0xC0;
|
||||
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
|
||||
static final byte INS_EXPORT_KEY = (byte) 0xC2;
|
||||
static final byte INS_GET_DATA = (byte) 0xCA;
|
||||
static final byte INS_STORE_DATA = (byte) 0xE2;
|
||||
|
||||
public static final byte CHANGE_PIN_P1_USER_PIN = 0x00;
|
||||
public static final byte CHANGE_PIN_P1_PUK = 0x01;
|
||||
public static final byte CHANGE_PIN_P1_PAIRING_SECRET = 0x02;
|
||||
|
||||
public static final byte GET_STATUS_P1_APPLICATION = 0x00;
|
||||
public static final byte GET_STATUS_P1_KEY_PATH = 0x01;
|
||||
|
||||
public static final byte LOAD_KEY_P1_EC = 0x01;
|
||||
public static final byte LOAD_KEY_P1_EXT_EC = 0x02;
|
||||
public static final byte LOAD_KEY_P1_SEED = 0x03;
|
||||
|
||||
public static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
|
||||
public static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
|
||||
public static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
|
||||
|
||||
static final byte SIGN_P1_CURRENT_KEY = 0x00;
|
||||
static final byte SIGN_P1_DERIVE = 0x01;
|
||||
static final byte SIGN_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
|
||||
static final byte SIGN_P1_PINLESS = 0x03;
|
||||
|
||||
public static final byte SIGN_P2_ECDSA = 0x00;
|
||||
public static final byte SIGN_P2_BLS12_381 = 0x01;
|
||||
|
||||
public static final byte STORE_DATA_P1_PUBLIC = 0x00;
|
||||
public static final byte STORE_DATA_P1_NDEF = 0x01;
|
||||
public static final byte STORE_DATA_P1_CASH = 0x02;
|
||||
|
||||
public static final int GENERATE_MNEMONIC_12_WORDS = 0x04;
|
||||
public static final int GENERATE_MNEMONIC_15_WORDS = 0x05;
|
||||
public static final int GENERATE_MNEMONIC_18_WORDS = 0x06;
|
||||
public static final int GENERATE_MNEMONIC_21_WORDS = 0x07;
|
||||
public static final int GENERATE_MNEMONIC_24_WORDS = 0x08;
|
||||
|
||||
static final byte EXPORT_KEY_P1_CURRENT = 0x00;
|
||||
static final byte EXPORT_KEY_P1_DERIVE = 0x01;
|
||||
static final byte EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT = 0x02;
|
||||
|
||||
public static final byte EXPORT_KEY_P2_PRIVATE_AND_PUBLIC = 0x00;
|
||||
public static final byte EXPORT_KEY_P2_PUBLIC_ONLY = 0x01;
|
||||
public static final byte EXPORT_KEY_P2_EXTENDED_PUBLIC = 0x02;
|
||||
|
||||
static final byte FACTORY_RESET_P1_MAGIC = (byte) 0xAA;
|
||||
static final byte FACTORY_RESET_P2_MAGIC = 0x55;
|
||||
|
||||
static final byte TLV_APPLICATION_INFO_TEMPLATE = (byte) 0xA4;
|
||||
|
||||
private final CardChannel apduChannel;
|
||||
private SecureChannelSession secureChannel;
|
||||
private ApplicationInfo info;
|
||||
|
||||
/**
|
||||
* Creates a KeycardCommandSet using the given APDU Channel
|
||||
*
|
||||
* @param apduChannel APDU channel
|
||||
*/
|
||||
public KeycardCommandSet(CardChannel apduChannel) {
|
||||
this.apduChannel = apduChannel;
|
||||
this.secureChannel = new SecureChannelSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ApplicationInfo getApplicationInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SecureChannel object
|
||||
*
|
||||
* @param secureChannel secure channel
|
||||
*/
|
||||
protected void setSecureChannel(SecureChannelSession secureChannel) {
|
||||
this.secureChannel = secureChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current pairing data.
|
||||
*/
|
||||
public Pairing getPairing() {
|
||||
return secureChannel.getPairing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the pairing data.
|
||||
*
|
||||
* @param pairing data from an existing pairing
|
||||
*/
|
||||
public void setPairing(Pairing pairing) {
|
||||
secureChannel.setPairing(pairing);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects the default instance of the Keycard applet. The applet is assumed to have been installed with its default
|
||||
* AID. The returned data is a public key which must be used to initialize the secure channel.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse select() throws IOException {
|
||||
return select(Identifiers.KEYCARD_DEFAULT_INSTANCE_IDX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a Keycard instance. The applet is assumed to have been installed with its default AID. The returned data is
|
||||
* a public key which must be used to initialize the secure channel.
|
||||
*
|
||||
* @param instanceIdx the instance index
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse select(int instanceIdx) throws IOException {
|
||||
APDUCommand selectApplet = new APDUCommand(0x00, 0xA4, 4, 0, Identifiers.getKeycardInstanceAID(instanceIdx));
|
||||
APDUResponse resp = apduChannel.send(selectApplet);
|
||||
|
||||
|
||||
if(resp.getSw() == 0x9000) {
|
||||
info = new ApplicationInfo(resp.getData());
|
||||
|
||||
if(info.hasSecureChannelCapability()) {
|
||||
this.secureChannel.generateSecret(info.getSecureChannelPubKey());
|
||||
this.secureChannel.reset();
|
||||
}
|
||||
}
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the secure channel. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException secure channel error
|
||||
*/
|
||||
public void autoOpenSecureChannel() throws IOException, APDUException {
|
||||
secureChannel.autoOpenSecureChannel(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically pairs. Derives the secret from the given password.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException pairing error
|
||||
*/
|
||||
public void autoPair(String pairingPassword) throws IOException, APDUException {
|
||||
byte[] secret = pairingPasswordToSecret(pairingPassword);
|
||||
|
||||
secureChannel.autoPair(apduChannel, secret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a pairing password to a binary pairing secret.
|
||||
*
|
||||
* @param pairingPassword the pairing password
|
||||
* @return the pairing secret
|
||||
*/
|
||||
public byte[] pairingPasswordToSecret(String pairingPassword) {
|
||||
SecretKey key;
|
||||
|
||||
try {
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256", Drongo.getProvider());
|
||||
PBEKeySpec spec = new PBEKeySpec(pairingPassword.toCharArray(), "Keycard Pairing Password Salt".getBytes(), apduChannel.pairingPasswordPBKDF2IterationCount(), 32 * 8);
|
||||
key = skf.generateSecret(spec);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return key.getEncoded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically pairs. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException pairing error
|
||||
*/
|
||||
public void autoPair(byte[] sharedSecret) throws IOException, APDUException {
|
||||
secureChannel.autoPair(apduChannel, sharedSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically unpairs. Calls the corresponding method of the SecureChannel class.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unpairing error
|
||||
*/
|
||||
public void autoUnpair() throws IOException, APDUException {
|
||||
secureChannel.autoUnpair(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public APDUResponse openSecureChannel(byte index, byte[] data) throws IOException {
|
||||
return secureChannel.openSecureChannel(apduChannel, index, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public APDUResponse mutuallyAuthenticate() throws IOException {
|
||||
return secureChannel.mutuallyAuthenticate(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public APDUResponse mutuallyAuthenticate(byte[] data) throws IOException {
|
||||
return secureChannel.mutuallyAuthenticate(apduChannel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public APDUResponse pair(byte p1, byte[] data) throws IOException {
|
||||
return secureChannel.pair(apduChannel, p1, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a UNPAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public APDUResponse unpair(byte p1) throws IOException {
|
||||
return secureChannel.unpair(apduChannel, p1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpair all other clients.
|
||||
*
|
||||
* @throws IOException communication error
|
||||
* @throws APDUException unpairing error
|
||||
*/
|
||||
public void unpairOthers() throws IOException, APDUException {
|
||||
secureChannel.unpairOthers(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an IDENTIFY CARD APDU. The challenge is sent as APDU data as-is. It must be 32 bytes long
|
||||
*
|
||||
* @param challenge the data of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse identifyCard(byte[] challenge) throws IOException {
|
||||
APDUCommand identifyCard = secureChannel.protectedCommand(0x80, INS_IDENTIFY_CARD, 0, 0, challenge);
|
||||
return secureChannel.transmit(apduChannel, identifyCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET STATUS APDU. The info byte is the P1 parameter of the command, valid constants are defined in the applet
|
||||
* class itself.
|
||||
*
|
||||
* @param info the P1 of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse getStatus(byte info) throws IOException {
|
||||
APDUCommand getStatus = secureChannel.protectedCommand(0x80, INS_GET_STATUS, info, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, getStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a VERIFY PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse verifyPIN(String pin) throws IOException {
|
||||
APDUCommand verifyPIN = secureChannel.protectedCommand(0x80, INS_VERIFY_PIN, 0, 0, pin.getBytes());
|
||||
return secureChannel.transmit(apduChannel, verifyPIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the user PIN.
|
||||
*
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePIN(String pin) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_USER_PIN, pin.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the PUK.
|
||||
*
|
||||
* @param puk the new PUK
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePUK(String puk) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_PUK, puk.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU to change the pairing password. This does not break existing pairings, but new pairings
|
||||
* will be made using the new password.
|
||||
*
|
||||
* @param pairingPassword the new pairing password
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePairingPassword(String pairingPassword) throws IOException {
|
||||
return changePIN(CHANGE_PIN_P1_PAIRING_SECRET, pairingPasswordToSecret(pairingPassword));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePIN(int pinType, String pin) throws IOException {
|
||||
return changePIN(pinType, pin.getBytes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a CHANGE PIN APDU. The raw bytes of the given string are encrypted using the secure channel and used as APDU
|
||||
* data.
|
||||
*
|
||||
* @param pinType the PIN type
|
||||
* @param pin the new PIN
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse changePIN(int pinType, byte[] pin) throws IOException {
|
||||
APDUCommand changePIN = secureChannel.protectedCommand(0x80, INS_CHANGE_PIN, pinType, 0, pin);
|
||||
return secureChannel.transmit(apduChannel, changePIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an UNBLOCK PIN APDU. The PUK and PIN are concatenated and the raw bytes are encrypted using the secure
|
||||
* channel and used as APDU data.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse unblockPIN(String puk, String newPin) throws IOException {
|
||||
APDUCommand unblockPIN = secureChannel.protectedCommand(0x80, INS_UNBLOCK_PIN, 0, 0, (puk + newPin).getBytes());
|
||||
return secureChannel.transmit(apduChannel, unblockPIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a LOAD KEY APDU. The given seed is sent as-is and the P1 of the command is set to LOAD_KEY_P1_SEED (0x03).
|
||||
* This works on cards which support public key derivation. The loaded keyset is extended and support further
|
||||
* key derivation.
|
||||
*
|
||||
* @param seed the binary seed
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse loadKey(byte[] seed) throws IOException {
|
||||
return loadKey(seed, LOAD_KEY_P1_SEED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a LOAD KEY APDU. The key is sent in TLV format. The public key is included if not null. The chain code is
|
||||
* included if not null. P1 is set automatically to either LOAD_KEY_P1_EC or
|
||||
* LOAD_KEY_P1_EXT_EC depending on the presence of the chainCode.
|
||||
*
|
||||
* @param publicKey a raw public key
|
||||
* @param privateKey a raw private key
|
||||
* @param chainCode the chain code
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse loadKey(byte[] publicKey, byte[] privateKey, byte[] chainCode) throws IOException {
|
||||
return loadKey(new BIP32KeyPair(privateKey, chainCode, publicKey), publicKey == null);
|
||||
}
|
||||
|
||||
public APDUResponse loadKey(BIP32KeyPair keyPair) throws IOException {
|
||||
return loadKey(keyPair, false);
|
||||
}
|
||||
|
||||
public APDUResponse loadKey(BIP32KeyPair keyPair, boolean omitPublic) throws IOException {
|
||||
byte p1;
|
||||
|
||||
if(keyPair.isExtended()) {
|
||||
p1 = LOAD_KEY_P1_EXT_EC;
|
||||
} else {
|
||||
p1 = LOAD_KEY_P1_EC;
|
||||
}
|
||||
|
||||
return loadKey(keyPair.toTLV(!omitPublic), p1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a LOAD KEY APDU. The data is encrypted and sent as-is. The keyType parameter is used as P1.
|
||||
*
|
||||
* @param data key data
|
||||
* @param keyType the P1 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse loadKey(byte[] data, byte keyType) throws IOException {
|
||||
APDUCommand loadKey = secureChannel.protectedCommand(0x80, INS_LOAD_KEY, keyType, 0, data);
|
||||
return secureChannel.transmit(apduChannel, loadKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GENERATE MNEMONIC APDU. The cs parameter is the length of the checksum and is used as P1.
|
||||
*
|
||||
* @param cs the P1 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse generateMnemonic(int cs) throws IOException {
|
||||
APDUCommand generateMnemonic = secureChannel.protectedCommand(0x80, INS_GENERATE_MNEMONIC, cs, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, generateMnemonic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a REMOVE KEY APDU.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse removeKey() throws IOException {
|
||||
APDUCommand removeKey = secureChannel.protectedCommand(0x80, INS_REMOVE_KEY, 0, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, removeKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GENERATE KEY APDU.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse generateKey() throws IOException {
|
||||
APDUCommand generateKey = secureChannel.protectedCommand(0x80, INS_GENERATE_KEY, 0, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, generateKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long.
|
||||
*
|
||||
* @param hash the hash to sign
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse sign(byte[] hash) throws IOException {
|
||||
return sign(hash, SIGN_P1_CURRENT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The key used to sign is given
|
||||
* as a parameter.
|
||||
*
|
||||
* @param hash the hash to sign
|
||||
* @param makeCurrent ture if the key used to sign should become the current key, false otherwise
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
* @params path the path of the key to use
|
||||
*/
|
||||
public APDUResponse signWithPath(byte[] hash, String path, boolean makeCurrent) throws IOException {
|
||||
KeyPath keyPath = new KeyPath(path);
|
||||
byte[] pathData = keyPath.getData();
|
||||
byte[] data = Arrays.copyOf(hash, hash.length + pathData.length);
|
||||
System.arraycopy(pathData, 0, data, hash.length, pathData.length);
|
||||
return sign(data, keyPath.getSource() | (makeCurrent ? SIGN_P1_DERIVE_AND_MAKE_CURRENT : SIGN_P1_DERIVE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash that must be exactly 32-bytes long. The pinless path will be used
|
||||
* to sign. This command is the only variant of SIGN which can also be executed without a Secure Channel.
|
||||
*
|
||||
* @param hash the hash to sign
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse signPinless(byte[] hash) throws IOException {
|
||||
return sign(hash, SIGN_P1_PINLESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long, eventually followed by
|
||||
* a derivation path.
|
||||
*
|
||||
* @param p1 the p1 parameter
|
||||
* @param data the data to sign
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse sign(byte[] data, int p1) throws IOException {
|
||||
APDUCommand sign = secureChannel.protectedCommand(0x80, INS_SIGN, p1, 0x01, data);
|
||||
return secureChannel.transmit(apduChannel, sign);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU with the given key path.
|
||||
*
|
||||
* @param keypath the string key path
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deriveKey(String keypath) throws IOException {
|
||||
KeyPath path = new KeyPath(keypath);
|
||||
return deriveKey(path.getData(), path.getSource());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The P1 is forced to 0, meaning that the derivation
|
||||
* starts from the master key.
|
||||
*
|
||||
* @param data the raw key path
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deriveKey(byte[] data) throws IOException {
|
||||
return deriveKey(data, DERIVE_P1_SOURCE_MASTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a DERIVE KEY APDU. The data is encrypted and sent as-is. The source parameter is used as P1.
|
||||
*
|
||||
* @param data the raw key path or a public key
|
||||
* @param source the source to start derivation
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse deriveKey(byte[] data, int source) throws IOException {
|
||||
APDUCommand deriveKey = secureChannel.protectedCommand(0x80, INS_DERIVE_KEY, source, 0x00, data);
|
||||
return secureChannel.transmit(apduChannel, deriveKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SET PINLESS PATH APDU. The path must be absolute, that is starting from the master key.
|
||||
*
|
||||
* @param path the path. Must be an absolute path (i.e: starting from the master key)
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse setPinlessPath(String path) throws IOException {
|
||||
KeyPath keyPath = new KeyPath(path);
|
||||
if(keyPath.getSource() != DERIVE_P1_SOURCE_MASTER) {
|
||||
throw new IllegalArgumentException("Only absolute paths can be set as PINLESS path");
|
||||
}
|
||||
|
||||
return setPinlessPath(keyPath.getData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an empty SET PINLESS PATH APDU, resetting it. After this command the card does not have a PINless path until
|
||||
* a new one is set.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse resetPinlessPath() throws IOException {
|
||||
return setPinlessPath(new byte[]{});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a SET PINLESS PATH APDU. The data is encrypted and sent as-is.
|
||||
*
|
||||
* @param data the raw key path
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse setPinlessPath(byte[] data) throws IOException {
|
||||
APDUCommand setPinlessPath = secureChannel.protectedCommand(0x80, INS_SET_PINLESS_PATH, 0x00, 0x00, data);
|
||||
return secureChannel.transmit(apduChannel, setPinlessPath);
|
||||
}
|
||||
|
||||
private byte poToP2(boolean publicOnly) {
|
||||
return publicOnly ? EXPORT_KEY_P2_PUBLIC_ONLY : EXPORT_KEY_P2_PRIVATE_AND_PUBLIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU to export the current key.
|
||||
*
|
||||
* @param publicOnly exports only the public key
|
||||
* @return the raw card reponse
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportCurrentKey(boolean publicOnly) throws IOException {
|
||||
return exportCurrentKey(poToP2(publicOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU to export the current key.
|
||||
*
|
||||
* @param p2 the p2 parameter
|
||||
* @return the raw card reponse
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportCurrentKey(byte p2) throws IOException {
|
||||
return exportKey(EXPORT_KEY_P1_CURRENT, p2, new byte[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param publicOnly the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(String keyPath, boolean makeCurrent, boolean publicOnly) throws IOException {
|
||||
return exportKey(keyPath, makeCurrent, poToP2(publicOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param p2 the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(String keyPath, boolean makeCurrent, byte p2) throws IOException {
|
||||
KeyPath path = new KeyPath(keyPath);
|
||||
return exportKey(path.getData(), path.getSource(), makeCurrent, p2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param publicOnly the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, boolean publicOnly) throws IOException {
|
||||
return exportKey(keyPath, source, makeCurrent, poToP2(publicOnly));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. Performs derivation of the given keypath and optionally makes it the current key.
|
||||
*
|
||||
* @param keyPath the keypath to export
|
||||
* @param makeCurrent if the key should be made current or not
|
||||
* @param p2 the P2 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(byte[] keyPath, int source, boolean makeCurrent, byte p2) throws IOException {
|
||||
int p1 = source | (makeCurrent ? EXPORT_KEY_P1_DERIVE_AND_MAKE_CURRENT : EXPORT_KEY_P1_DERIVE);
|
||||
return exportKey(p1, p2, keyPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
|
||||
*
|
||||
* @param derivationOptions the P1 parameter
|
||||
* @param publicOnly the P2 parameter
|
||||
* @param keypath the data parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(int derivationOptions, boolean publicOnly, byte[] keypath) throws IOException {
|
||||
return exportKey(derivationOptions, poToP2(publicOnly), keypath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an EXPORT KEY APDU. The parameters are sent as-is.
|
||||
*
|
||||
* @param derivationOptions the P1 parameter
|
||||
* @param p2 the P2 parameter
|
||||
* @param keypath the data parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse exportKey(int derivationOptions, byte p2, byte[] keypath) throws IOException {
|
||||
APDUCommand exportKey = secureChannel.protectedCommand(0x80, INS_EXPORT_KEY, derivationOptions, p2, keypath);
|
||||
return secureChannel.transmit(apduChannel, exportKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET DATA APDU.
|
||||
*
|
||||
* @param dataType the type of data to be stored
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse getData(byte dataType) throws IOException {
|
||||
APDUCommand getData = secureChannel.protectedCommand(0x80, INS_GET_DATA, dataType, 0, new byte[0]);
|
||||
return secureChannel.transmit(apduChannel, getData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a STORE DATA APDU for NDEF.
|
||||
*
|
||||
* @param ndef the data field of the APDU
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse setNDEF(byte[] ndef) throws IOException {
|
||||
if((info.getAppVersion() >> 8) > 2) {
|
||||
if((ndef.length - 2) != ((ndef[0] << 8) | ndef[1])) {
|
||||
byte[] tmp = new byte[ndef.length + 2];
|
||||
tmp[0] = (byte) (ndef.length >> 8);
|
||||
tmp[1] = (byte) (ndef.length & 0xff);
|
||||
System.arraycopy(ndef, 0, tmp, 2, ndef.length);
|
||||
ndef = tmp;
|
||||
}
|
||||
|
||||
return storeData(ndef, STORE_DATA_P1_NDEF);
|
||||
} else {
|
||||
APDUCommand setNDEF = secureChannel.protectedCommand(0x80, INS_SET_NDEF, 0, 0, ndef);
|
||||
return secureChannel.transmit(apduChannel, setNDEF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a STORE DATA APDU.
|
||||
*
|
||||
* @param data the data field of the APDU
|
||||
* @param dataType the type of data to be stored
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse storeData(byte[] data, byte dataType) throws IOException {
|
||||
APDUCommand storeData = secureChannel.protectedCommand(0x80, INS_STORE_DATA, dataType, 0, data);
|
||||
return secureChannel.transmit(apduChannel, storeData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param pairingPassword pairing password
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String puk, String pairingPassword) throws IOException {
|
||||
return this.init(pin, puk, pairingPassword, (byte) 0, (byte) 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param pairingPassword pairing password
|
||||
* @param pinRetries the number of allowed PIN retries
|
||||
* @param pukRetries the number of allowed PUK retries
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
|
||||
return this.init(pin, null, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param altPin the alternative PIN
|
||||
* @param puk the PUK
|
||||
* @param pairingPassword pairing password
|
||||
* @param pinRetries the number of allowed PIN retries
|
||||
* @param pukRetries the number of allowed PUK retries
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String altPin, String puk, String pairingPassword, byte pinRetries, byte pukRetries) throws IOException {
|
||||
return this.init(pin, altPin, puk, pairingPasswordToSecret(pairingPassword), pinRetries, pukRetries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param puk the PUK
|
||||
* @param sharedSecret the shared secret for pairing
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String puk, byte[] sharedSecret) throws IOException {
|
||||
return init(pin, null, puk, sharedSecret, (byte) 0, (byte) 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the INIT command to the card. If either pinRetries or pukRetries is zero, neither will be sent.
|
||||
*
|
||||
* @param pin the PIN
|
||||
* @param pin the alternative
|
||||
* @param puk the PUK
|
||||
* @param sharedSecret the shared secret for pairing
|
||||
* @param pinRetries the number of allowed PIN retries
|
||||
* @param pukRetries the number of allowed PUK retries
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse init(String pin, String altPin, String puk, byte[] sharedSecret, byte pinRetries, byte pukRetries) throws IOException {
|
||||
int baselen = pin.length() + puk.length() + sharedSecret.length;
|
||||
int extlen;
|
||||
|
||||
if(altPin != null) {
|
||||
extlen = 2 + altPin.length();
|
||||
} else if((pinRetries != 0) || (pukRetries != 0)) {
|
||||
extlen = 2;
|
||||
} else {
|
||||
extlen = 0;
|
||||
}
|
||||
|
||||
byte[] initData = Arrays.copyOf(pin.getBytes(), baselen + extlen);
|
||||
System.arraycopy(puk.getBytes(), 0, initData, pin.length(), puk.length());
|
||||
System.arraycopy(sharedSecret, 0, initData, pin.length() + puk.length(), sharedSecret.length);
|
||||
|
||||
if(extlen > 0) {
|
||||
initData[baselen] = pinRetries;
|
||||
initData[baselen + 1] = pukRetries;
|
||||
|
||||
if(extlen > 2) {
|
||||
System.arraycopy(altPin.getBytes(), 0, initData, baselen + 2, altPin.length());
|
||||
}
|
||||
}
|
||||
|
||||
APDUCommand init = new APDUCommand(0x80, INS_INIT, 0, 0, secureChannel.oneShotEncrypt(initData));
|
||||
return apduChannel.send(init);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the FACTORY RESET command to the card.
|
||||
*
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse factoryReset() throws IOException {
|
||||
APDUCommand factoryReset = new APDUCommand(0x80, INS_FACTORY_RESET, FACTORY_RESET_P1_MAGIC, FACTORY_RESET_P2_MAGIC, new byte[0]);
|
||||
return apduChannel.send(factoryReset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class KeycardTransport implements CardChannel {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeycardTransport.class);
|
||||
|
||||
private final Card connection;
|
||||
|
||||
KeycardTransport(byte[] appletAid) throws CardException {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
List<CardTerminal> terminals = tf.terminals().list();
|
||||
if(terminals.isEmpty()) {
|
||||
throw new IllegalStateException("No reader connected");
|
||||
}
|
||||
|
||||
Card connection = null;
|
||||
for(Iterator<CardTerminal> iter = terminals.iterator(); iter.hasNext(); ) {
|
||||
try {
|
||||
connection = getConnection(iter.next(), appletAid);
|
||||
break;
|
||||
} catch(CardException e) {
|
||||
if(!iter.hasNext()) {
|
||||
log.info(e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private Card getConnection(CardTerminal cardTerminal, byte[] appletAid) throws CardException {
|
||||
Card connection = cardTerminal.connect("*");
|
||||
|
||||
javax.smartcardio.CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, appletAid));
|
||||
if(resp.getSW() != APDUResponse.SW_OK) {
|
||||
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()));
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
public APDUResponse send(APDUCommand capdu) throws IOException {
|
||||
javax.smartcardio.CardChannel cardChannel = this.connection.getBasicChannel();
|
||||
|
||||
CommandAPDU cmd = new CommandAPDU(capdu.getCla(), capdu.getIns(), capdu.getP1(), capdu.getP2(), capdu.getData());
|
||||
ResponseAPDU resp;
|
||||
|
||||
try {
|
||||
resp = cardChannel.transmit(cmd);
|
||||
} catch(CardException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
return new APDUResponse(resp.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void disconnect() throws CardException {
|
||||
connection.disconnect(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Stores pairing information.
|
||||
*/
|
||||
public class Pairing {
|
||||
private byte[] pairingKey;
|
||||
private byte pairingIndex;
|
||||
|
||||
/**
|
||||
* Constructor. The pairingKey and pairingIndex are those generated at the end of a successful pairing.
|
||||
*
|
||||
* @param pairingKey the pairing key
|
||||
* @param pairingIndex the pairing index
|
||||
*/
|
||||
public Pairing(byte[] pairingKey, byte pairingIndex) {
|
||||
this.pairingKey = pairingKey;
|
||||
this.pairingIndex = pairingIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Initializes from a byte array previously generated from the toByteArray method
|
||||
*
|
||||
* @param fromByteArray the result of a previous toByteArray invocation
|
||||
*/
|
||||
public Pairing(byte[] fromByteArray) {
|
||||
pairingIndex = fromByteArray[0];
|
||||
pairingKey = Arrays.copyOfRange(fromByteArray, 1, fromByteArray.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor. Initializes from a String previously generated from the toBase64 method
|
||||
*
|
||||
* @param base64 the result of a previous toBase64 invocation
|
||||
*/
|
||||
public Pairing(String base64) {
|
||||
this(Base64.getDecoder().decode(base64));
|
||||
}
|
||||
|
||||
public byte[] getPairingKey() {
|
||||
return pairingKey;
|
||||
}
|
||||
|
||||
public byte getPairingIndex() {
|
||||
return pairingIndex;
|
||||
}
|
||||
|
||||
public byte[] toByteArray() {
|
||||
byte[] res = new byte[pairingKey.length + 1];
|
||||
res[0] = pairingIndex;
|
||||
System.arraycopy(pairingKey, 0, res, 1, pairingKey.length);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public String toBase64() {
|
||||
return Base64.getEncoder().encodeToString(toByteArray());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Signature with recoverable public key.
|
||||
*/
|
||||
public class RecoverableSignature {
|
||||
private byte[] publicKey;
|
||||
private int recId;
|
||||
private byte[] r;
|
||||
private byte[] s;
|
||||
private boolean compressed;
|
||||
|
||||
public static final byte TLV_SIGNATURE_TEMPLATE = (byte) 0xA0;
|
||||
public static final byte TLV_RAW_SIGNATURE = (byte) 0x80;
|
||||
public static final byte TLV_ECDSA_TEMPLATE = (byte) 0x30;
|
||||
|
||||
/**
|
||||
* Parses a signature from the card and calculates the recovery ID.
|
||||
*
|
||||
* @param hash the message being signed
|
||||
* @param tlvData the signature as returned from the card
|
||||
*/
|
||||
public RecoverableSignature(byte[] hash, byte[] tlvData) {
|
||||
TinyBERTLV tlv = new TinyBERTLV(tlvData);
|
||||
int tag = tlv.readTag();
|
||||
tlv.unreadLastTag();
|
||||
|
||||
if(tag == TLV_RAW_SIGNATURE) {
|
||||
initFromRawSignature(hash, tlv.readPrimitive(tag));
|
||||
} else if(tag == TLV_SIGNATURE_TEMPLATE) {
|
||||
initFromLegacy(hash, tlv);
|
||||
} else {
|
||||
throw new IllegalArgumentException("invalid tlv");
|
||||
}
|
||||
}
|
||||
|
||||
private void initFromLegacy(byte[] hash, TinyBERTLV tlv) {
|
||||
tlv.enterConstructed(TLV_SIGNATURE_TEMPLATE);
|
||||
this.publicKey = tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY);
|
||||
tlv.enterConstructed(TLV_ECDSA_TEMPLATE);
|
||||
this.r = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
|
||||
this.s = toUInt(tlv.readPrimitive(TinyBERTLV.TLV_INT));
|
||||
this.compressed = false;
|
||||
|
||||
calculateRecID(hash);
|
||||
}
|
||||
|
||||
private void initFromRawSignature(byte[] hash, byte[] signature) {
|
||||
this.r = Arrays.copyOfRange(signature, 0, 32);
|
||||
this.s = Arrays.copyOfRange(signature, 32, 64);
|
||||
this.recId = signature[64];
|
||||
this.compressed = false;
|
||||
this.publicKey = recoverFromSignature(this.recId, hash, this.r, this.s, this.compressed);
|
||||
}
|
||||
|
||||
public RecoverableSignature(byte[] publicKey, boolean compressed, byte[] r, byte[] s, int recId) {
|
||||
this.publicKey = publicKey;
|
||||
this.r = r;
|
||||
this.s = s;
|
||||
this.compressed = compressed;
|
||||
this.recId = recId;
|
||||
}
|
||||
|
||||
void calculateRecID(byte[] hash) {
|
||||
recId = -1;
|
||||
|
||||
for(int i = 0; i < 4; i++) {
|
||||
byte[] candidate = recoverFromSignature(i, hash, r, s, compressed);
|
||||
|
||||
if(Arrays.equals(candidate, publicKey)) {
|
||||
recId = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(recId == -1) {
|
||||
throw new IllegalArgumentException("Unrecoverable signature, cannot find recId");
|
||||
}
|
||||
}
|
||||
|
||||
static byte[] toUInt(byte[] signedInt) {
|
||||
if(signedInt[0] == 0) {
|
||||
return Arrays.copyOfRange(signedInt, 1, signedInt.length);
|
||||
} else {
|
||||
return signedInt;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The public key associated to this signature.
|
||||
*
|
||||
* @return the public key associated to this signature
|
||||
*/
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The recovery ID
|
||||
*
|
||||
* @return recovery ID
|
||||
*/
|
||||
public int getRecId() {
|
||||
return recId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The R value.
|
||||
*
|
||||
* @return r
|
||||
*/
|
||||
public byte[] getR() {
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* The S value
|
||||
*
|
||||
* @return s
|
||||
*/
|
||||
public byte[] getS() {
|
||||
return s;
|
||||
}
|
||||
|
||||
static byte[] recoverFromSignature(int recId, byte[] hash, byte[] r, byte[] s, boolean compressed) {
|
||||
ECDSASignature sig = new ECDSASignature(new BigInteger(1, r), new BigInteger(1, s));
|
||||
ECKey key = ECKey.recoverFromSignature(recId, sig, Sha256Hash.wrap(hash), compressed);
|
||||
|
||||
if(key == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return key.getPubKey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.security.*;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Handles a SecureChannel session with the card.
|
||||
*/
|
||||
public class SecureChannelSession {
|
||||
public static final short SC_SECRET_LENGTH = 32;
|
||||
public static final short SC_BLOCK_SIZE = 16;
|
||||
|
||||
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
|
||||
public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11;
|
||||
public static final byte INS_PAIR = 0x12;
|
||||
public static final byte INS_UNPAIR = 0x13;
|
||||
|
||||
public static final byte PAIR_P1_FIRST_STEP = 0x00;
|
||||
public static final byte PAIR_P1_LAST_STEP = 0x01;
|
||||
|
||||
public static final int PAYLOAD_MAX_SIZE = 223;
|
||||
|
||||
static final byte PAIRING_MAX_CLIENT_COUNT = 5;
|
||||
|
||||
|
||||
private byte[] secret;
|
||||
private byte[] publicKey;
|
||||
private byte[] iv;
|
||||
private Pairing pairing;
|
||||
private Cipher sessionCipher;
|
||||
private Cipher sessionMac;
|
||||
private SecretKeySpec sessionEncKey;
|
||||
private SecretKeySpec sessionMacKey;
|
||||
private SecureRandom random;
|
||||
private boolean open;
|
||||
|
||||
/**
|
||||
* Constructs a SecureChannel session on the client.
|
||||
*/
|
||||
public SecureChannelSession() {
|
||||
random = new SecureRandom();
|
||||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 keyData the public key returned by the applet as response to the SELECT command
|
||||
*/
|
||||
public void generateSecret(byte[] keyData) {
|
||||
try {
|
||||
ECKey key = new ECKey();
|
||||
SecretPoint secretPoint = new SecretPoint(key.getPrivKeyBytes(), keyData);
|
||||
publicKey = key.getPubKey(false);
|
||||
secret = secretPoint.ECDHSecretAsBytes();
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key
|
||||
*
|
||||
* @return the public key
|
||||
*/
|
||||
public byte[] getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the pairing information
|
||||
*
|
||||
* @return the pairing information
|
||||
*/
|
||||
public Pairing getPairing() {
|
||||
return pairing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets pairing information needed to open a secure channel.
|
||||
*
|
||||
* @param pairing the pairing information
|
||||
*/
|
||||
public void setPairing(Pairing pairing) {
|
||||
this.pairing = pairing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes a Secure Channel with the card. The command parameters are the public key generated in the first step.
|
||||
* Follows the specifications from the SECURE_CHANNEL.md document.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void autoOpenSecureChannel(CardChannel apduChannel) throws IOException, APDUException {
|
||||
APDUResponse response = openSecureChannel(apduChannel, pairing.getPairingIndex(), publicKey);
|
||||
response.checkOK("OPEN SECURE CHANNEL failed");
|
||||
processOpenSecureChannelResponse(response);
|
||||
|
||||
response = mutuallyAuthenticate(apduChannel);
|
||||
response.checkOK("MUTUALLY AUTHENTICATE failed");
|
||||
verifyMutuallyAuthenticateResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the response from OPEN SECURE CHANNEL. This initialize the session keys, Cipher and MAC internally.
|
||||
*
|
||||
* @param response the card response
|
||||
*/
|
||||
public void processOpenSecureChannelResponse(APDUResponse response) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA512");
|
||||
md.update(secret);
|
||||
md.update(pairing.getPairingKey());
|
||||
byte[] data = response.getData();
|
||||
byte[] keyData = md.digest(Arrays.copyOf(data, SC_SECRET_LENGTH));
|
||||
iv = Arrays.copyOfRange(data, SC_SECRET_LENGTH, data.length);
|
||||
|
||||
sessionEncKey = new SecretKeySpec(Arrays.copyOf(keyData, SC_SECRET_LENGTH), "AES");
|
||||
sessionMacKey = new SecretKeySpec(Arrays.copyOfRange(keyData, SC_SECRET_LENGTH, keyData.length), "AES");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", Drongo.getProvider());
|
||||
sessionMac = Cipher.getInstance("AES/CBC/NoPadding", Drongo.getProvider());
|
||||
open = true;
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that the response from MUTUALLY AUTHENTICATE is correct.
|
||||
*
|
||||
* @param response the card response
|
||||
* @return true if response is correct, false otherwise
|
||||
*/
|
||||
public void verifyMutuallyAuthenticateResponse(APDUResponse response) throws APDUException {
|
||||
if(response.getData().length != SC_SECRET_LENGTH) {
|
||||
throw new APDUException("Invalid authentication data from the card");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the entire pairing procedure in order to be able to use the secure channel
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void autoPair(CardChannel apduChannel, byte[] sharedSecret) throws IOException, APDUException {
|
||||
byte[] challenge = new byte[32];
|
||||
random.nextBytes(challenge);
|
||||
APDUResponse resp = pair(apduChannel, PAIR_P1_FIRST_STEP, challenge).checkOK("Pairing failed on step 1");
|
||||
|
||||
byte[] respData = resp.getData();
|
||||
byte[] cardCryptogram = Arrays.copyOf(respData, 32);
|
||||
byte[] cardChallenge = Arrays.copyOfRange(respData, 32, respData.length);
|
||||
byte[] checkCryptogram;
|
||||
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA256");
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
md.update(sharedSecret);
|
||||
checkCryptogram = md.digest(challenge);
|
||||
|
||||
if(!Arrays.equals(checkCryptogram, cardCryptogram)) {
|
||||
throw new APDUException("Invalid card cryptogram");
|
||||
}
|
||||
|
||||
md.update(sharedSecret);
|
||||
checkCryptogram = md.digest(cardChallenge);
|
||||
|
||||
resp = pair(apduChannel, PAIR_P1_LAST_STEP, checkCryptogram).checkOK("Pairing failed on step 2");
|
||||
respData = resp.getData();
|
||||
md.update(sharedSecret);
|
||||
pairing = new Pairing(md.digest(Arrays.copyOfRange(respData, 1, respData.length)), respData[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpairs the current paired key
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void autoUnpair(CardChannel apduChannel) throws IOException, APDUException {
|
||||
unpair(apduChannel, pairing.getPairingIndex()).checkOK("Unpairing failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a OPEN SECURE CHANNEL APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param index the P1 parameter
|
||||
* @param data the data
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws IOException {
|
||||
open = false;
|
||||
APDUCommand openSecureChannel = new APDUCommand(0x80, INS_OPEN_SECURE_CHANNEL, index, 0, data);
|
||||
return apduChannel.send(openSecureChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. The data is generated automatically
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel) throws IOException {
|
||||
byte[] data = new byte[SC_SECRET_LENGTH];
|
||||
random.nextBytes(data);
|
||||
|
||||
return mutuallyAuthenticate(apduChannel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param data the data
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws IOException {
|
||||
APDUCommand mutuallyAuthenticate = protectedCommand(0x80, INS_MUTUALLY_AUTHENTICATE, 0, 0, data);
|
||||
return transmit(apduChannel, mutuallyAuthenticate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PAIR APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param p1 the P1 parameter
|
||||
* @param data the data
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse pair(CardChannel apduChannel, byte p1, byte[] data) throws IOException {
|
||||
APDUCommand pair = new APDUCommand(0x80, INS_PAIR, p1, 0, data);
|
||||
return transmit(apduChannel, pair);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a UNPAIR APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param p1 the P1 parameter
|
||||
* @return the raw card response
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public APDUResponse unpair(CardChannel apduChannel, byte p1) throws IOException {
|
||||
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, p1, 0, new byte[0]);
|
||||
return transmit(apduChannel, unpair);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpair all other clients
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @throws IOException communication error
|
||||
*/
|
||||
public void unpairOthers(CardChannel apduChannel) throws IOException, APDUException {
|
||||
for(int i = 0; i < PAIRING_MAX_CLIENT_COUNT; i++) {
|
||||
if(i != pairing.getPairingIndex()) {
|
||||
APDUCommand unpair = protectedCommand(0x80, INS_UNPAIR, i, 0, new byte[0]);
|
||||
transmit(apduChannel, unpair).checkOK();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the plaintext data using the session key. The maximum plaintext size is 223 bytes. The returned ciphertext
|
||||
* already includes the IV and padding and can be sent as-is in the APDU payload. If the input is an empty byte array
|
||||
* the returned data will still contain the IV and padding.
|
||||
*
|
||||
* @param data the plaintext data
|
||||
* @return the encrypted data
|
||||
*/
|
||||
private byte[] encryptAPDU(byte[] data) {
|
||||
assert data.length <= PAYLOAD_MAX_SIZE;
|
||||
|
||||
try {
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
return sessionCipher.doFinal(data);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts the response from the card using the session key. The returned data is already stripped from IV and padding
|
||||
* and can be potentially empty.
|
||||
*
|
||||
* @param data the ciphetext
|
||||
* @return the plaintext
|
||||
*/
|
||||
private byte[] decryptAPDU(byte[] data) {
|
||||
try {
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionCipher.init(Cipher.DECRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
return sessionCipher.doFinal(data);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a command APDU with MAC and encrypted data.
|
||||
*
|
||||
* @param cla the CLA byte
|
||||
* @param ins the INS byte
|
||||
* @param p1 the P1 byte
|
||||
* @param p2 the P2 byte
|
||||
* @param data the data, can be an empty array but not null
|
||||
* @return the command APDU
|
||||
*/
|
||||
public APDUCommand protectedCommand(int cla, int ins, int p1, int p2, byte[] data) {
|
||||
byte[] finalData;
|
||||
|
||||
if(open) {
|
||||
data = encryptAPDU(data);
|
||||
byte[] meta = new byte[]{(byte) cla, (byte) ins, (byte) p1, (byte) p2, (byte) (data.length + SC_BLOCK_SIZE), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
updateIV(meta, data);
|
||||
|
||||
finalData = Arrays.copyOf(iv, iv.length + data.length);
|
||||
System.arraycopy(data, 0, finalData, iv.length, data.length);
|
||||
} else {
|
||||
finalData = data;
|
||||
}
|
||||
|
||||
return new APDUCommand(cla, ins, p1, p2, finalData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transmits a protected command APDU and unwraps the response data. The MAC is verified, the data decrypted and the
|
||||
* SW read from the payload.
|
||||
*
|
||||
* @param apduChannel the APDU channel
|
||||
* @param apdu the APDU to send
|
||||
* @return the unwrapped response APDU
|
||||
* @throws IOException transmission error
|
||||
*/
|
||||
public APDUResponse transmit(CardChannel apduChannel, APDUCommand apdu) throws IOException {
|
||||
APDUResponse resp = apduChannel.send(apdu);
|
||||
|
||||
if(resp.getSw() == 0x6982) {
|
||||
open = false;
|
||||
}
|
||||
|
||||
if(open) {
|
||||
byte[] data = resp.getData();
|
||||
byte[] meta = new byte[]{(byte) data.length, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
|
||||
byte[] mac = Arrays.copyOf(data, iv.length);
|
||||
data = Arrays.copyOfRange(data, iv.length, data.length);
|
||||
|
||||
byte[] plainData = decryptAPDU(data);
|
||||
|
||||
updateIV(meta, data);
|
||||
|
||||
if(!Arrays.equals(iv, mac)) {
|
||||
throw new IOException("Invalid MAC");
|
||||
}
|
||||
|
||||
return new APDUResponse(plainData);
|
||||
} else {
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the SecureChannel as closed
|
||||
*/
|
||||
public void reset() {
|
||||
open = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts the payload for the INIT command
|
||||
*
|
||||
* @param initData the payload for the INIT command
|
||||
* @return the encrypted buffer
|
||||
*/
|
||||
public byte[] oneShotEncrypt(byte[] initData) {
|
||||
try {
|
||||
iv = new byte[SC_BLOCK_SIZE];
|
||||
random.nextBytes(iv);
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
|
||||
sessionEncKey = new SecretKeySpec(secret, "AES");
|
||||
sessionCipher = Cipher.getInstance("AES/CBC/ISO7816-4Padding", Drongo.getProvider());
|
||||
sessionCipher.init(Cipher.ENCRYPT_MODE, sessionEncKey, ivParameterSpec);
|
||||
initData = sessionCipher.doFinal(initData);
|
||||
byte[] encrypted = new byte[1 + publicKey.length + iv.length + initData.length];
|
||||
encrypted[0] = (byte) publicKey.length;
|
||||
System.arraycopy(publicKey, 0, encrypted, 1, publicKey.length);
|
||||
System.arraycopy(iv, 0, encrypted, (1 + publicKey.length), iv.length);
|
||||
System.arraycopy(initData, 0, encrypted, (1 + publicKey.length + iv.length), initData.length);
|
||||
return encrypted;
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the SecureChannel as open. Only to be used when writing tests for the SecureChannel, in normal operation this
|
||||
* would only make things wrong.
|
||||
*
|
||||
*/
|
||||
protected void setOpen() {
|
||||
open = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a CMAC from the metadata and data provided and sets it as the IV for the next message.
|
||||
*
|
||||
* @param meta metadata
|
||||
* @param data data
|
||||
*/
|
||||
private void updateIV(byte[] meta, byte[] data) {
|
||||
try {
|
||||
IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[SC_BLOCK_SIZE]);
|
||||
sessionMac.init(Cipher.ENCRYPT_MODE, sessionMacKey, ivParameterSpec);
|
||||
sessionMac.update(meta);
|
||||
byte[] tmp = sessionMac.doFinal(data);
|
||||
iv = Arrays.copyOfRange(tmp, tmp.length - SC_BLOCK_SIZE, tmp.length);
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Tiny BER-TLV implementation. Not for general usage, but fast and easy to use for this project.
|
||||
*/
|
||||
public class TinyBERTLV {
|
||||
public static final byte TLV_BOOL = (byte) 0x01;
|
||||
public static final byte TLV_INT = (byte) 0x02;
|
||||
|
||||
public static final int END_OF_TLV = (int) 0xffffffff;
|
||||
|
||||
private byte[] buffer;
|
||||
private int pos;
|
||||
|
||||
public static int[] readNum(byte[] buf, int off) {
|
||||
int len = buf[off++] & 0xff;
|
||||
int lenlen = 0;
|
||||
|
||||
if((len & 0x80) == 0x80) {
|
||||
lenlen = len & 0x7f;
|
||||
len = readVal(buf, off, lenlen);
|
||||
}
|
||||
|
||||
return new int[]{len, off + lenlen};
|
||||
}
|
||||
|
||||
public static int readVal(byte[] val, int off, int len) {
|
||||
switch(len) {
|
||||
case 1:
|
||||
return val[off] & 0xff;
|
||||
case 2:
|
||||
return ((val[off] & 0xff) << 8) | (val[off + 1] & 0xff);
|
||||
case 3:
|
||||
return ((val[off] & 0xff) << 16) | ((val[off + 1] & 0xff) << 8) | (val[off + 2] & 0xff);
|
||||
case 4:
|
||||
return ((val[off] & 0xff) << 24) | ((val[off + 1] & 0xff) << 16) | ((val[off + 2] & 0xff) << 8) | (val[off + 3] & 0xff);
|
||||
default:
|
||||
throw new IllegalArgumentException("Integers of length " + len + " are unsupported");
|
||||
}
|
||||
}
|
||||
|
||||
public static void writeNum(ByteArrayOutputStream os, int len) {
|
||||
if((len & 0xff000000) != 0) {
|
||||
os.write(0x84);
|
||||
os.write((len & 0xff000000) >> 24);
|
||||
os.write((len & 0x00ff0000) >> 16);
|
||||
os.write((len & 0x0000ff00) >> 8);
|
||||
os.write(len & 0x000000ff);
|
||||
} else if((len & 0x00ff0000) != 0) {
|
||||
os.write(0x83);
|
||||
os.write((len & 0x00ff0000) >> 16);
|
||||
os.write((len & 0x0000ff00) >> 8);
|
||||
os.write(len & 0x000000ff);
|
||||
} else if((len & 0x0000ff00) != 0) {
|
||||
os.write(0x82);
|
||||
os.write((len & 0x0000ff00) >> 8);
|
||||
os.write(len & 0x000000ff);
|
||||
} else if((len & 0x00000080) != 0) {
|
||||
os.write(0x81);
|
||||
os.write(len & 0x000000ff);
|
||||
} else {
|
||||
os.write(len);
|
||||
}
|
||||
}
|
||||
|
||||
public TinyBERTLV(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enters a constructed TLV with the given tag
|
||||
*
|
||||
* @param tag the tag to enter
|
||||
* @return the length of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
*/
|
||||
public int enterConstructed(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
return readLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a primitive TLV with the given tag
|
||||
*
|
||||
* @param tag the tag to read
|
||||
* @return the body of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
*/
|
||||
public byte[] readPrimitive(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
int len = readLength();
|
||||
pos += len;
|
||||
return Arrays.copyOfRange(buffer, (pos - len), pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a boolean TLV.
|
||||
*
|
||||
* @return the boolean value of the TLV
|
||||
* @throws IllegalArgumentException if the next tag is not a boolean
|
||||
*/
|
||||
public boolean readBoolean() throws IllegalArgumentException {
|
||||
byte[] val = readPrimitive(TLV_BOOL);
|
||||
return ((val[0] & 0xff) == 0xff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an integer TLV.
|
||||
*
|
||||
* @return the integer value of the TLV
|
||||
* @throws IllegalArgumentException if the next tlv is not an integer or is of unsupported length
|
||||
*/
|
||||
public int readInt() throws IllegalArgumentException {
|
||||
byte[] val = readPrimitive(TLV_INT);
|
||||
return TinyBERTLV.readVal(val, 0, val.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all unread bytes in the TLV.
|
||||
*
|
||||
* @return all unread bytes
|
||||
*/
|
||||
byte[] peekUnread() {
|
||||
return Arrays.copyOfRange(buffer, pos, buffer.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Low-level method to unread the last read tag. Only valid if the previous call was readTag(). Does nothing if the
|
||||
* end of the TLV has been reached.
|
||||
*/
|
||||
public void unreadLastTag() {
|
||||
if(pos < buffer.length) {
|
||||
pos--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next tag. The current implementation only reads tags on one byte. Can be extended if needed.
|
||||
*
|
||||
* @return the tag
|
||||
*/
|
||||
public int readTag() {
|
||||
return (pos < buffer.length) ? buffer[pos++] : END_OF_TLV;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed.
|
||||
*
|
||||
* @return the tag
|
||||
*/
|
||||
public int readLength() {
|
||||
int[] len = TinyBERTLV.readNum(buffer, pos);
|
||||
pos = len[1];
|
||||
return len[0];
|
||||
}
|
||||
|
||||
private void checkTag(int expected, int actual) throws IllegalArgumentException {
|
||||
if(expected != actual) {
|
||||
unreadLastTag();
|
||||
throw new IllegalArgumentException("Expected tag: " + expected + ", received: " + actual);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.Satschip;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
|
||||
import com.sparrowwallet.sparrow.io.keycard.Keycard;
|
||||
import com.sparrowwallet.sparrow.io.satochip.Satochip;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Accordion;
|
||||
@@ -40,7 +41,7 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip(), new Satschip());
|
||||
List<KeystoreCardImport> cardImporters = List.of(new Tapsigner(), new Satochip(), new Satschip(), new Keycard());
|
||||
for(KeystoreCardImport importer : cardImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
CardImportPane importPane = new CardImportPane(getMasterController().getWallet(), importer, getMasterController().getDefaultDerivation(), getMasterController().getRequiredDerivation());
|
||||
|
||||
Reference in New Issue
Block a user