Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/io/CardApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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;
Expand Down Expand Up @@ -58,6 +59,13 @@ public static List<WalletModel> getConnectedCards() throws CardException {
//ignore
}

try {
KeycardApi keycardApi = new KeycardApi(null, null);
cards.add(keycardApi.getCardType());
} catch(Exception e) {
//ignore
}

return cards;
}

Expand All @@ -70,6 +78,10 @@ public static CardApi getCardApi(WalletModel walletModel, String pin) throws Car
return new SatoCardApi(walletModel, pin);
}

if(walletModel == WalletModel.KEYCARD) {
return new KeycardApi(walletModel, pin);
}

throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
}

Expand Down
125 changes: 125 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/io/keycard/APDUCommand.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
178 changes: 178 additions & 0 deletions src/main/java/com/sparrowwallet/sparrow/io/keycard/APDUResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package com.sparrowwallet.sparrow.io.keycard;

/**
* 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 WrongPINException wrong PIN
* @throws APDUException unexpected response
*/
public APDUResponse checkAuthOK() throws WrongPINException, APDUException {
if((this.sw & SW_WRONG_PIN_MASK) == SW_WRONG_PIN_MASK) {
throw new WrongPINException(sw2 & 0x0F);
} 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;
}
}
Loading