Introduction
This is the second article on using the PC/SC Smart Card API in Windows with PC/SC card readers. The code in these articles will be in C++11 using the Unicode character set and has been tested with Microsoft Visual Studio 2015 Community. It also assumed that the reader has a grasp of C++11 and Windows development. During the course of these articles we shall be developing a simple class for handling the smart card API.
Background
The article shows how to use the PC/SC Windows API to read the unique identifier (UID) from a contactless storage card. Each card contains an integrated chip with a permanent identification number, or UID. This number is created during the manufacturing process, it is sometimes referred to as the card serial number. The UID can be 4 bytes (32bit), 7 bytes (56Bit) or 10 bytes (80bit). It is also possible for a card to produce a random UID.
This code has been tested with the following :
Readers: HID Omnikey 5021CL, ACS ACR122 & Identive CLOUD 3700 F
Cards: MIFARE Classic 1K, MIFARE Ultralight, MIFARE DESFire EV1
Reading the UID
The steps required in reading the UID from a contactless card requires the following steps.
1. Get context handle (SCardEstablishContext)
2. Connect to the card on the reader (SCardConnect)
3. Send the Get Data Command using SCardTransmit. (See section 3.3.5.1.3 in Part 3 of the PC/SC specification for more details on this command. Link in References).
The challenge and potential sticking point is with SCardConnect, as one of its parameters requires the name of the card reader and also for it to work; a card needs to be on the card reader. It is possible to wait for a card to be presented but we shall discuss that in a future article. So we shall use the ListReaders function from our previous article (How to list smart card readers using PC/SC) to get our list of readers and connect to the first reader in the list. Like this:
CSmartcard smartcard; try { // initialise the smart-card class smartcard.Init(); // get a list of attached readers auto readerList = smartcard.ListReaders(); // and connect to the first one and read the UID from the card on it. if (readerList.size() >= 1) { // connect to the card reader smartcard.Connect(readerList[0]); } } catch (const CSmartcardException &ex) { _tprintf(_T("Error %s, 0x%08x\n"), ex.ErrorText(), ex.ErrorCode()); }
The new Connect member function is implemented as follows:
// connect to card on specified reader, throws CSmartcardException void CSmartcard::Connect(CString &reader) { DWORD protocols(SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1); m_activeProtocol = 0; LONG ret = SCardConnect(m_hSC, reader, SCARD_SHARE_SHARED, protocols, &m_hCard, &m_activeProtocol); if (ret != SCARD_S_SUCCESS) { throw CSmartcardException(ret); } }
If a card is not present then a CSmartcardException with SCARD_W_REMOVED_CARD (0x80100069) error code will be thrown. If the Init() function has not been called prior to calling this function then a CSmartcardException with SCARD_E_NO_READERS_AVAILABLE (0x8010002E) error code will be thrown.
Here we have a new handle (m_hCard, initialised to NULL in the constructor) of type HCARDHANDLE and this handle is used with functions that communicate with the card, which we acquire using SCardConnect. SCardConnect has the following syntax:
LONG WINAPI SCardConnect( _In_ SCARDCONTEXT hContext, _In_ LPCTSTR szReader, _In_ DWORD dwShareMode, _In_ DWORD dwPreferredProtocols, _Out_ LPSCARDHANDLE phCard, _Out_ LPDWORD pdwActiveProtocol );
Most of these parameters are fairly straightforward, you will notice that I used SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1 for the preferred protocols as specifying both protocol options allows the PC/SC reader driver to determine which of the two possible ISO7816-3 contact smartcard protocols is appropriate. For contactless cards neither protocol is actually applicable as the commands are pseudo APDUs communicated to the reader (or IFD). The communication between reader and card is handled automatically using the appropriate contactless protocols. But to keep the PC/SC interface happy, we’ll just allow both protocols here!
An APDU (Application Protocol Data Unit) is the communication unit between a smart card reader and a smart card. The structure of the APDU is defined by ISO/IEC 7816-4.
So to read the UID we need to send a GET DATA command APDU using the SCardTransmit function. The GET DATA command APDU has the following format:
Command | Class | INS | P1 | P2 | Lc | Data In | Le |
Get Data | 0xFF | 0xCA | 0x00 0x01 |
0x00 | – | – | xx |
The options for P1 & P2 are:
P1 | P2 | |
0x00 | 0x00 | UID is returned |
0x01 | 0x00 | all historical bytes from the ATS of a ISO 14443 A card without CRC are returned |
An ATS (Answer To Select) can be used to identify the manufacturer, card type and application.
The length of the UID returned will depend on the card used. To handle this we can create a member function to our CSmartcard class as follows:
// gets the UID from the current card, throws CSmardcardException // returns as unsigned 64 bit unsigned int uint64_t CSmartcard::GetUID() { uint64_t uid(0); // check that have a card handle if (m_hCard == NULL) { throw CSmartcardException(SCARD_E_INVALID_HANDLE); } // create the get data APDU UCHAR sendBuffer[] = { 0xff, // CLA - the instruction class 0xCA, // INS - the instruction code 0x00, // P1 - 1st parameter to the instruction 0x00, // P2 - 2nd parameter to the instruction 0x00 // Le - size of the transfer }; UCHAR receiveBuffer[32]; DWORD sendLength(); DWORD receiveLength(_countof(receiveBuffer)); // set up the io request SCARD_IO_REQUEST ioRequest; ioRequest.dwProtocol = m_activeProtocol; ioRequest.cbPciLength = sizeof(ioRequest); LONG ret = SCardTransmit(m_hCard, &ioRequest, sendBuffer, // the send buffer _countof(sendBuffer), // the send buffer length NULL, receiveBuffer, // the receive buffer &receiveLength); // the receive buffer length if (ret == SCARD_S_SUCCESS) { // have received a response. Check that did not get an error if (receiveLength >= 2) { // do we have an error if ((receiveBuffer[receiveLength - 2] != 0x90) || (receiveBuffer[receiveLength - 1] != 0x00)) { throw CAPDUException( &receiveBuffer[receiveLength - 2]); } else if (receiveLength > 2) { for (DWORD i = 0; i != receiveLength - 2; i++) { uid <<= 8; uid |= static_cast<uint64_t>(receiveBuffer[i]); } } } else { // didn't get a recognisable response, // so throw a generic read error throw CSmartcardException(ERROR_READ_FAULT); } } else { throw CSmartcardException(ret); } return uid; }
This function can also throw a new exception type, its definition & implementation is below:
// exception class for APDU errors class CAPDUException { public: CAPDUException(const UCHAR *error) : CAPDUException(error[0], error[1]) { } CAPDUException(UCHAR e1, UCHAR e2) { m_errorCode = (static_cast<USHORT>(e1) << 8) | static_cast<USHORT>(e2); } // get the error code inline USHORT GetErrorCode() const { return m_errorCode; } inline CString GetErrorCodeText() const { CString str; str.Format(_T("%04x"), m_errorCode); return str; } protected: USHORT m_errorCode; };
To use our GetUID function we can do the following:
int _tmain(int argc, _TCHAR* argv[]) { CSmartcard smartcard; try { // initialise the smart-card class //smartcard.Init(); // get a list of attached readers auto readerList = smartcard.ListReaders(); // and connect to the first one and read the UID from the card on it. if (readerList.size() >= 1) { // connect to the card reader smartcard.Connect(readerList[0]); uint64_t uid = smartcard.GetUID(); _tprintf(_T("UID: %I64X\n"), uid); } } catch (const CSmartcardException &ex) { _tprintf(_T("Error %s, 0x%08x\n"), ex.ErrorText(), ex.ErrorCode()); } return 0; }
When I ran the program with my test MIFARE cards & readers I got the following…
Card | Readers | ||
HID Omnikey 5021CL | ACS ACR122 | Identive CLOUD 3700 F | |
MIFARE Standard 1K | UID: FE8C8C97 | UID: FE8C8C97 | UID: FE8C8C97 |
MIFARE Ultra light | UID: 4891692BF2380 | UID: 4891692BF2380 | UID: 4891692BF2380 |
MIFARE DESFire EV1 | UID: 437146AB73780 | UID: 437146AB73780 | UID: 437146AB73780 |
The MIFARE Ultra light & MIFARE DESFire UIDs are 56bit long and the MIFARE Standard UID is 32bits long.
Now what
As this sample stands it is not very useful as the GetUID function only works when a card is present on the card reader at that moment in time. What would be more useful would be to be able to wait for a card to be presented at a card reader and then read the UID or other data, which will be the subject of a future article on the use of PC/SC.
References
PC/SC specifications: Part 3 http://www.pcscworkgroup.com/specifications/html/pcsc3_v2.01.09/