Search code examples
javasmartcardcontactless-smartcardemv

PSE of card not what it is supposed to be


So, I have a contactless Mastercard and a Visa paywave phone. I use a contactless HID Omnikey 5427 CK.

This is my code: `

static boolean cardReading = true;

public static void main(String[] args) throws CardException, UnsupportedEncodingException {

    while (cardReading == true) {

    try {
    TerminalFactory factory = TerminalFactory.getDefault();
    List<CardTerminal> terminals = factory.terminals().list();
    CardTerminal terminal = terminals.get(0);

    if (!terminal.isCardPresent()) {

        continue;

    }

    System.out.println("Terminals: " + terminals);
    System.out.println("Used terminal: " + terminal);

    Card card = terminal.connect("T=0");
    System.out.println("\n\nInserted card: " + card);
    CardChannel channel = card.getBasicChannel();

    String pse = "00A404000E325041592E5359532E444446303100";
    CommandAPDU apdu = new CommandAPDU(pse.getBytes());
    ResponseAPDU r = channel.transmit(apdu);

    System.out.println("Response: " + toHex(r.getData().toString()) + " " + r);
    System.out.println("ADPU: " + toHex(apdu.getBytes().toString()) + " " + r.getSW() + " " + r.getSW1() + " " + r.getSW2() + " " + r.getNr());

    apdu = new CommandAPDU((byte)0x00, (byte)0xB2, (byte)0x01, (byte)0x0C, (byte)0x00);
    r = channel.transmit(apdu);

    cardReading = false;
    Toolkit.getDefaultToolkit().beep();

    System.out.println("Terminals: " + terminals);
    System.out.println("Used terminal: " + terminal);
    System.out.println("\n\nInserted card: " + card);
    System.out.println("Response: " + toHex(r.getData().toString()) + " " + r);
    System.out.println("ADPU: " + toHex(apdu.getBytes().toString()) + " " + r.getSW() + " " + r.getSW1() + " " + r.getSW2() + " " + r.getNr());



    System.exit(1);

    } catch(Exception e) {

        continue;

    }

    }

}

public static byte[] hexStringToByteArray(String s) {
    int len = s.length();
    byte[] data = new byte[len / 2];
    for (int i = 0; i < len; i += 2) {
        data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                             + Character.digit(s.charAt(i+1), 16));
    }
    return data;
}

public static String toHex(String arg) {
    return String.format("%040x", new BigInteger(1, arg.getBytes()));
}

`

And so the output is:

Terminals: [PC/SC terminal HID OMNIKEY 5427 CK 0]
Used terminal: PC/SC terminal HID OMNIKEY 5427 CK 0


Inserted card: PC/SC card in HID OMNIKEY 5427 CK 0, protocol T=0, state OK
Response: 0000000000000000005b42403163343630306630 ResponseAPDU: 65 bytes, SW=9000
ADPU: 000000000000000000005b424035623665663230 36864 144 0 63
Terminals: [PC/SC terminal HID OMNIKEY 5427 CK 0]
Used terminal: PC/SC terminal HID OMNIKEY 5427 CK 0


Inserted card: PC/SC card in HID OMNIKEY 5427 CK 0, protocol T=0, state OK
Response: 0000000000000000005b42403565616235383039 ResponseAPDU: 14 bytes, SW=9000
ADPU: 0000000000000000005b42403433323065373664 36864 144 0 12

I do not understand why is the response 0000000000000000005b42403565616235383039... Please help.

Regards, Vlad.


Solution

  • As Michael Roland says -- your handling of hexadecimal strings is wrong.

    Meta-mistake 1: Using 'byte[].toString()'

    r.getData().toString()
    

    The byte[].toString uses the default Object.toString() implementation which returns the class name followed by @ and the value of Object.hashCode() (e.g. "[B@312b1dae") -- which is not what you want for further processing.

    You can use the Arrays.toString() method (which does not do the hexadecimal dump), or any other method.

    Meta-mistake 2: Using 'String.getBytes()'

    String pse = "00A404000E325041592E5359532E444446303100";
    CommandAPDU apdu = new CommandAPDU(pse.getBytes());
    

    Does not create the desired APDU object as the method String.getBytes() does not perform hexadecimal conversion, but a charset conversion in the platform's default charset, e.g.: "1234".getBytes() gives { 0x31, 0x32, 0x33, 0x34 } (and not { 0x12, 0x34 } as you might expect).

    Below is a simple code example which performs (almost) the same as your code:

    package test.java.so;
    
    import java.util.List;
    
    import javax.smartcardio.Card;
    import javax.smartcardio.CardChannel;
    import javax.smartcardio.CardTerminal;
    import javax.smartcardio.CommandAPDU;
    import javax.smartcardio.ResponseAPDU;
    import javax.smartcardio.TerminalFactory;
    
    import org.apache.commons.codec.binary.Hex;
    
    @SuppressWarnings("restriction")
    public class So39543402 {
    
        public static void main(String[] args) throws Exception {
                    TerminalFactory factory = TerminalFactory.getDefault();
                    List<CardTerminal> terminals = factory.terminals().list();
                    CardTerminal terminal = terminals.get(0);
    
                    Card card = terminal.connect("*");
                    CardChannel channel = card.getBasicChannel();
    
                    String pse = "00A404000E325041592E5359532E444446303100";
                    CommandAPDU apdu = new CommandAPDU(Hex.decodeHex(pse.toCharArray()));
                    exchangeApdu(channel, apdu);
    
                    apdu = new CommandAPDU(0x00, 0xB2, 0x01, 0x0C, 256);
                    exchangeApdu(channel, apdu);
        }
    
        private static ResponseAPDU exchangeApdu(CardChannel channel, CommandAPDU apdu) throws javax.smartcardio.CardException {
            System.out.println("APDU: " + Hex.encodeHexString(apdu.getBytes()));
            ResponseAPDU r = channel.transmit(apdu);
            System.out.println("Response: " + Hex.encodeHexString(r.getBytes()));
            return r;
        }
    
    }
    

    Please note some interesting parts:

    • this code uses Apache Commons Codec for the hexadecimal conversions

    • argument "*" is used for CardTerminal.connect() which is more versatile than always requesting the T=0 protocol

    • your READ RECORD APDU was modified to expect 256 bytes of response data -- this is how this particular constructor works (your code would produce an ISO case 1 Command-APDU which is probably not what you want)

    One additional note:

    • do not use the String.getBytes() method without parameters (even when you do want to convert single characters into bytes). Always specify the desired character set (e.g. "US-ASCII", "UTF-8")

    Good luck!