КАК ОБНАРУЖИТЬ КОНТАКТНЫЕ ИЛИ БЕСКОНТАКТНЫЕ СМАРТ-КАРТЫ С ПОМОЩЬЮ ПК / SC

Lord777

Professional
Messages
2,583
Reputation
15
Reaction score
1,254
Points
113
ВСТУПЛЕНИЕ
Это третья статья об использовании API смарт-карт PC / SC в Windows с кардридерами PC / SC.

Код в этих статьях будет написан на C ++ 11 с использованием набора символов Unicode и протестирован в сообществе Microsoft Visual Studio 2015. Также предполагалось, что читатель знаком с разработкой на C ++ 11 и Windows. В ходе этих статей мы будем разрабатывать простой класс для работы с API смарт-карт.

К сожалению, мы не можем публиковать подробности, относящиеся к технологии MIFARE DESFire, поскольку они защищены соглашением о неразглашении NXP ™ с партнерами.

ФОН
В статье показано, как использовать Windows API PC / SC для определения момента, когда карта была подана в устройство чтения карт, а затем для считывания уникального идентификатора (UID) с карты.

Этот код был протестирован со следующими устройствами: Считыватели: HID Omnikey 5021CL, ACS ACR122 и Identive CLOUD 3700 F
Карты: MIFARE Standard 1K, MIFARE Ultralight, MIFARE DESFire EV1

ОБНАРУЖЕНИЕ КАРТ
Шаги, необходимые для обнаружения наличия бесконтактной карты, требуют следующих шагов.
  1. Получить дескриптор контекста (SCardEstablishContext)
  2. Получить статус читателя, за которым ведется мониторинг (SCardGetStatusChange)
  3. Сравните элементы dwCurrentState и dwEventState в SCARD_READERSTATE, чтобы определить, произошло ли изменение статуса.
  4. Выполните необходимую операцию с картой, в нашем случае считайте UID.

Чтение изменения статуса для смарт-карт немного отличается от других операций Windows тем, что оно не управляется событиями, поэтому необходимо опросить подсистему смарт-карт. Следует отметить, что функция SCardStatusChange блокируется (т. е. она не возвращается, пока не произойдет изменение статуса или не истечет время ожидания). Чтобы предотвратить блокировку приложения, можно использовать один из следующих методов:
  • Используйте очень короткий тайм-аут - подходит, если в приложении есть цикл опроса.
  • Запустите опрос через WM_TIMER (OnTimer в приложении MFC) снова с очень коротким таймаутом.
  • Используйте отдельную нить.

Чтобы прочитать изменение статуса, вызывается функция SCardGetStatusChange, которая имеет следующие параметры:
Code:
LONG WINAPI SCardGetStatusChange(
    _In_    SCARDCONTEXT        hContext,
    _In_    DWORD               dwTimeout,
    _Inout_ LPSCARD_READERSTATE rgReaderStates,
    _In_    DWORD               cReaders
);

Параметр rgReaderStates представляет собой массив структур SCARD_READERSTATE. Эта структура имеет следующий синтаксис:
Code:
typedef struct {
    LPCTSTR szReader;
    LPVOID  pvUserData;
    DWORD   dwCurrentState;
    DWORD   dwEventState;
    DWORD   cbAtr;
    BYTE    rgbAtr[36];
} SCARD_READERSTATE, *PSCARD_READERSTATE, *LPSCARD_READERSTATE;

Переменные-члены используются следующим образом (дополнительные сведения см. в документации MSDN).
ПеременнаяИспользовать
szReaderИмя отслеживаемого читателя, обычно это имя обычно получается через вызов функции SCardListReaders .
pvUserDataНе используется.
dwCurrentStateБитовое поле последнего записанного состояния считывателя устанавливается приложением.
dwEventStateБитовое поле текущего состояния считывателя, известное подсистеме смарт-карт.
cbAtrКоличество байтов в rgbAtr.
rgbAtrATR вставленной карты.
Функция SCardGetStatusChange работает, сравнивая dwCurrentState с фактическим состоянием устройства чтения. Если есть несоответствие, то dwEventState обновляется с фактическим состоянием считывателя. SCARD_STATE_CHANGED флаг также будет установлен, чтобы указать, что существует разница между 2 членами государства.

Чтобы можно было изначально получить точное состояние считывателя, для dwCurrentState должно быть установлено значение SCARD_STATE_UNAWARE, а для dwEventState - 0x00. Следующий код показывает, как этого легко добиться.

Code:
SCARD_READERSTATE readerState = {};
readerState.szReader = _T("Identiv CLOUD 3700 F Contactless Reader 0");
readerState.dwCurrentState = SCARD_STATE_UNAWARE;

Базовый код для определения того, что читатель изменил свой статус, будет выглядеть так:
Code:
long ret = SCardGetStatusChange(hSC, 1, &readerState, 1);

if (ret == SCARD_SUCCESS)
{
    if (readerState.dwEventState & SCARD_STATE_CHANGED)
    {
        // the reader state has changed....
        OnStateChange(readerState);

        // clear the SCARD_STATE_CHANGED flag
        readerState.dwCurrentState = readerState.dwEventState & ~SCARD_STATE_CHANGED;
    }
}

После вызова SCardGetStatusChange (), dwEventState копируется в dwCurrentState но с SCARD_STATE_CHANGED бит очищен. Затем он готов к использованию для следующего вызова SCardGetStatusChange () для обнаружения следующего изменения состояния.

Другие флаги для dwCurrentState и dwEventState:
ЦенитьdwCurrentState ЗначениеdwEventState значение
SCARD_STATE_UNAWAREСостояние неизвестно dwEventState будет установлено в текущее состояние считывателя при следующем вызове SCardGetStatusChange ()не используется
SCARD_STATE_IGNOREНе интересует этот читательне используется
SCARD_STATE_CHANGEDне используетсяЕсть разница между ожидаемым состоянием и фактическим состоянием
SCARD_STATE_UNKNOWNне используетсяЧитатель не узнал
SCARD_STATE_UNAVALABLEСчитыватель недоступен для использованияСостояние для этого ридера недоступно
SCARD_STATE_EMPTYКарты в считывателе не ожидаетсяНет карты в считывателе
SCARD_STATE_PRESENTОжидается, что карта в считывателеВ считывателе есть карта
SCARD_STATE_ATRMATCHОжидается, что ATR карты в считывателе будет соответствовать одной из целевых картВ считывателе есть карта с ATR, которая соответствует одной из целевых карт.
SCARD_STATE_EXCLUSIVEОжидается, что карта в считывателе будет использоваться исключительно другой программой.Карта в считывателе используется исключительно другой программой
SCARD_STATE_INUSEОжидается, что карта в считывателе будет использоватьсяКарта в считывателе используется
SCARD_STATE_MUTEОжидается, что карта в считывателе не отвечаетВ считывателе не отвечает карта

Чтобы обнаружить наличие карты на считывателе, необходимо проверить флаг SCARD_STATUS_PRESENT. Чтобы проверить, что карта только что была представлена, этот флаг будет снят в dwCurrentState, но установлен в dwEventState. Следующий код показывает, как это достигается:

Code:
bool foundCard = false;

long ret = SCardGetStatusChange(m_hSC, timeout, &readerState, 1);

if (ret == SCARD_S_SUCCESS)
{
    if (readerState.dwEventState & SCARD_STATE_CHANGED)
    {
        if (((readerState.dwCurrentState & SCARD_STATE_PRESENT) == 0) &&
            (readerState.dwEventState & SCARD_STATE_PRESENT))
        {
            // have a card....
        }

        readerState.dwCurrentState = readerState.dwEventState & ~SCARD_STATE_CHANGED;
    }
}

Этот метод также можно использовать для обнаружения любых других состояний состояния, а также для определения того, была ли удалена карта.

Чтобы расширить класс CSmartcard из нашей предыдущей статьи (Как читать MIFARE UID с помощью PC / SC), чтобы он мог обнаруживать карты, мы добавляем новую переменную-член в заголовок, например:
Code:
protected:
    std::vector<SCARD_READERSTATE> m_readerState;

а затем инициализировать вектор в функции ListReaders:
Code:
// get a list of readers
const CReaderList& CSmartcard::ListReaders()
{
    m_readers.empty();
    m_readerState.clear();

    // initialise if not already done
    Init();

    // will auto allocate memory for the list of readers
    TCHAR *pszReaderList = nullptr;
    DWORD len = SCARD_AUTOALLOCATE;

    LONG ret = SCardListReaders(
        m_hSC,
        NULL,                       // groups, using null will list all readers in the system
        (LPWSTR)&pszReaderList,     // pointer where to store the readers
        &len);                      // will return the length of characters in the reader list buffer

    if (ret == SCARD_S_SUCCESS)
    {
        TCHAR *pszReader = pszReaderList;
        while (*pszReader)
        {
            m_readers.push_back(pszReader);
            pszReader += _tcslen(pszReader) + 1;
        }

        // free the memory
        ret = SCardFreeMemory(m_hSC, pszReaderList);

        // and set up the reader state list
        for (const auto &reader : m_readers)
        {
            SCARD_READERSTATE readerState = {};
            readerState.szReader = reader;
            readerState.dwCurrentState = SCARD_STATE_UNAWARE;

            m_readerState.push_back(readerState);
        }
    }
    else
    {
        throw CSmartcardException(ret);
    }

    return m_readers;
}

Можно добавить новую функцию, которая будет определять, была ли карта вставлена (в случае контактной карты) или помещена в считывающее устройство (если бесконтактно). Это выглядело бы так:
Code:
// waits for a card to be presented to a reader
bool CSmartcard::WaitForCard(CString & readerName, DWORD timeout)
{
    bool foundCard = false;

    long ret = SCardGetStatusChange(m_hSC, timeout, m_readerState.data(), m_readerState.size());

    if (ret == SCARD_S_SUCCESS)
    {
        // which reader had a card
        for (auto pos = m_readerState.begin(); (pos != m_readerState.end()) && !found; ++pos)
        {
            if (pos->dwEventState & SCARD_STATE_CHANGED)
            {
                if (((pos->dwCurrentState & SCARD_STATE_PRESENT) == 0) &&
                    (pos->dwEventState & SCARD_STATE_PRESENT))
                {
                    readerName = pos->szReader;
                    foundCard = true;
                }

                pos->dwCurrentState = pos->dwEventState & ~SCARD_STATE_CHANGED;
            }
        }
    }

    return foundCard;
}

Чтобы использовать эти обновления класса CSmartcard из приложения, можно использовать следующий метод.
Code:
CSmartcard smartcard;

try
{
    // initialise the smart-card class
    smartcard.Init();

    // get a list of attached readers
    smartcard.ListReaders();

    bool finished = false;

    while (!finished)
    {
        // test for key press to exit loop
        if (_kbhit())
        {
            finished = true;
        }

        // and wait for 1ms for a card to be presented
        CString readerName;
        if (smartcard.WaitForCard(readerName, 1))
        {
            // connect to this reader
            smartcard.Connect(readerName);

            _tprintf(_T("Card on reader %s - UID: %I64X\n"),
                readerName,
                smartcard.GetUID());
        }
    }
}
catch (const CSmartcardException &ex)
{
    _tprintf(_T("Error %s, 0x%08x\n"), ex.ErrorText(), ex.ErrorCode());
}

Обратите внимание, что цикл while завершится, если на клавиатуре будет нажата любая клавиша. Если карта обнаружена, она будет прочитана, и будет прочитан UID. например
Code:
Card on reader Identiv CLOUD 3700 F Contactless Reader 0 - UID: 42E18F2893180
Card on reader Identiv CLOUD 3700 F Contactless Reader 0 - UID: FE8C8C97
Card on reader Identiv CLOUD 3700 F Contactless Reader 0 - UID: 4B90DEA704880

ЧТО ТЕПЕРЬ?
После того, как состояние устройства чтения карт было обнаружено, оно может быть прочитано или записано, в этой статье мы только что прочитали UID карты, но в равной степени можно было бы читать или записывать на карту, если бы это была карта памяти. Чтение данных сектора с карты MIFARE будет рассмотрено в следующей статье.

ИСХОДНЫЙ КОД
NewCard.cpp
Code:
#include "stdafx.h"
#include "Smartcard.h"

int _tmain(int argc, _TCHAR* argv[])
{
    CSmartcard smartcard;

    try
    {
        // initialise the smart-card class
        smartcard.Init();

        // get a list of attached readers
        smartcard.ListReaders();

        bool finished = false;

        while (!finished)
        {
            // test for key press to exit loop
            if (_kbhit())
            {
                finished = true;
            }

            // and wait for 1ms for a card to be presented
            CString readerName;
            if (smartcard.WaitForCard(readerName, 1))
            {
                // connect to this reader
                smartcard.Connect(readerName);

                _tprintf(_T("Card on reader %s - UID: %I64X\n"),
                    readerName,
                    smartcard.GetUID());
            }
        }
    }
    catch (const CSmartcardException &ex)
    {
        _tprintf(_T("Error %s, 0x%08x\n"), ex.ErrorText(), ex.ErrorCode());
    }

    return 0;
}

Smartcard.h
Code:
// defines wrapper class for PC/SC smartcard API
#pragma once

#include <vector>
#include <comdef.h>
#include <stdint.h>
#include <winscard.h>

// also need to link in winscard.lib
#pragma comment(lib, "winscard.lib")

using CReaderList = std::vector<CString>;

// defines wrapper class for PC/SC smart card API
class CSmartcard
{
public:
    CSmartcard();
    ~CSmartcard();

    // initialise interface, throws CSmartcardException
    void Init();

    // get a list of readers throws CSmartcardException
    const CReaderList& ListReaders();

    // connect to card on specified reader, throws CSmartcardException
    void Connect(const CString &reader);

    // gets the UID from the current card
    // returns as unsigned 64 bit int
    // throws CSmardcardException or CAPDUException
    uint64_t GetUID();

    // wait for card
    bool WaitForCard(CString &readerName, DWORD timeout);

protected:
    SCARDCONTEXT m_hSC;
    SCARDHANDLE m_hCard;
    DWORD m_activeProtocol;
    CReaderList m_readers;
    std::vector<SCARD_READERSTATE> m_readerState;
};

// the definition of the exception class
class CSmartcardException
{
public:
    CSmartcardException(LONG errorCode)
        : m_errorCode(errorCode)
    {
    }

    // get error code
    inline LONG ErrorCode() const
    {
        return m_errorCode;
    }

    // get text for error code
    inline CString ErrorText() const
    {
        return CString(_com_error(m_errorCode).ErrorMessage());
    }

protected:
    LONG m_errorCode;
};

// 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;
};

Smartcard.cpp
#include "stdafx.h"
#include "Smartcard.h"

// the constructor
CSmartcard::CSmartcard() :m_hSC(NULL), m_hCard(NULL)
{
}

// and the destructor
CSmartcard::~CSmartcard()
{
    if (m_hCard)
    {
        SCardDisconnect(m_hSC, m_hCard);
    }

    if (m_hSC)
    {
        SCardReleaseContext(m_hSC);
    }
}

// initialise interface
void CSmartcard::Init()
{
    if (m_hSC == NULL)
    {
        LONG ret = SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, &m_hSC);

        if (ret != SCARD_S_SUCCESS)
        {
            throw CSmartcardException(ret);
        }
    }
}

// get a list of readers
const CReaderList& CSmartcard::ListReaders()
{
    m_readers.empty();
    m_readerState.clear();

    // initialise if not already done
    Init();

    // will auto allocate memory for the list of readers
    TCHAR *pszReaderList = nullptr;
    DWORD len = SCARD_AUTOALLOCATE;

    LONG ret = SCardListReaders(
        m_hSC,
        NULL,                       // groups, using null will list all readers in the system
        (LPWSTR)&pszReaderList,     // pointer where to store the readers
        &len);                      // will return the length of characters in the reader list buffer

    if (ret == SCARD_S_SUCCESS)
    {
        TCHAR *pszReader = pszReaderList;
        while (*pszReader)
        {
            m_readers.push_back(pszReader);
            pszReader += _tcslen(pszReader) + 1;
        }

        // free the memory
        ret = SCardFreeMemory(m_hSC, pszReaderList);

        // and set up the reader state list
        for (const auto &reader : m_readers)
        {
            SCARD_READERSTATE readerState = {0};
            readerState.szReader = reader;
            readerState.dwCurrentState = SCARD_STATE_UNAWARE;

            m_readerState.push_back(readerState);
        }
    }
    else
    {
        throw CSmartcardException(ret);
    }

    return m_readers;
}


// connect to card on specified reader, throws CSmartcardException
void CSmartcard::Connect(const 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);
    }
}

// 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, _countof(sendBuffer),       // the send buffer & length
        NULL,
        receiveBuffer, &receiveLength);         // the receive buffer and 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;
}


// waits for a card to be presented to a reader
bool CSmartcard::WaitForCard(CString & readerName, DWORD timeout)
{
    bool foundCard = false;

    long ret = SCardGetStatusChange(m_hSC, timeout, m_readerState.data(), m_readerState.size());

    if (ret == SCARD_S_SUCCESS)
    {
        // which reader had a card
        for (auto pos = m_readerState.begin(); pos != m_readerState.end(); ++pos)
        {
            _tprintf(_T(" -- reader: %s - state: 0x%x\n"), pos->szReader, pos->dwEventState);

            if (pos->dwEventState & SCARD_STATE_CHANGED)
            {
                if (((pos->dwCurrentState & SCARD_STATE_PRESENT) == 0) &&
                    (pos->dwEventState & SCARD_STATE_PRESENT))
                {
                    readerName = pos->szReader;
                    foundCard = true;
                }

                pos->dwCurrentState = pos->dwEventState & ~SCARD_STATE_CHANGED;
            }
        }
    }

    return foundCard;
}

STDAFX.H
Code:
#pragma once

#include "targetver.h"

#include <stdio.h>
#include <tchar.h>
#include <conio.h>

#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS      // some CString constructors will be explicit

#include <atlbase.h>
#include <atlstr.h>
 
Top