Операции с ACS-ключами для ридера бесконтактных карт RD-03AB

ACS-ключ (ключ доступа) представляет собой запись длиной 8 байт, идентифицирующую UID карты. Запись состоит из байта длины UID и 7ми байт UID. Возможны следующие длины UID: 4 байта, 7 байт и, теоретически, 10 байт (предусмотрено стандартом, но в реальных картах такой UID не встречается). Если длина UID недостаточна для заполнения записи ACS-ключа (4 байта), запись дополняется нулями. Если длина UID превышает длину ACS-ключа (10 байт), последние байты UID отбрасываются. ACS-ключ, у которого все байты равны 0xFF, считается пустым (нет записи о ключе, ключ удалён).

rd03ab_acsk

В зависимости от исполнения ридера, ACS-ключи могут храниться как во внутренней памяти контроллера — ROM (Read Only Memory), так и во внешней энергонезависимой памяти – NVM (Non-Volatile Memory). В ROM помещается около 700 ключей, количество ключей в NVM зависит от типа установленной микросхемы: в NVM может храниться от 511 (24C32) до 8191(24C512) ключей, включая мастер-ключ. Вообще-то, в NVM количество записей о ключах кратно 64, т.е. реальный объём микросхемы 24C512 в ключах равен 8192 записям, но последняя запись NVM содержит сигнатуру: по сигнатуре контроллер определяет, что NVM отформатирована и готова к использованию. Объём памяти ACS-ключей содержится во втором байте (смещение +1), возвращаемом командой CMG_VER. Мастер-ключ хранится по смещению 0 на странице 0.

Пример вычисления объёмов памяти, связанных с ACS-ключами.

// ACS-key size demo, V1.6, Windows
#include "rd0xAB.h"
 
// *** main ***
int __cdecl _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
    CCR         ccr;
    DWORD       err = LERR_SUCCESS;
    BYTE        buf[6];
 
    BYTE        romi;   // ROM info byte: b.7 – тип памяти, b.6...b.0 – последняя страница памяти
    UINT        memsz;  // объём рабочего буфера для считывания всех страниц, в байтах
    UINT        memszk; // объём рабочего буфера для считывания всех страниц, в ключах
    UINT        total;  // количество ключей, которое можно хранить в ридере (без мастер-ключа)
 
    _tprintf(TEXT("ACS-key size demo for CCR RD-0xAB, V1.6.\n--\n"));
 
    if ((link_hidopen(0, NULL, &ccr)) != LERR_SUCCESS) goto ferr_exit;
    // установка временного режима DIR с включенным бипером
    if ((err = link_packet(&ccr, CMS_MOD, 3 + 8, NULL, 0, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;
    // чтение информации о ридере
    if ((err = link_packet(&ccr, CMG_VER, 0, NULL, 0, buf, 6)) != LERR_SUCCESS) goto ferr_exit;
    romi = buf[1];  // запоминаем информационный байт
    _tprintf(TEXT("Info byte: %02X.\n"), romi);
 
    // выделяем биты 6..0 с номером последней доступной страницы,
    // затем инкремент кол-ва страниц, т.к отсчёт начинается с 0,
    // затем умножение на 512, т.к. это размер страницы с ACS-ключами;
    // в memsz получаем объём буфера, который который надо выделить в
    // памяти при считывании всех ACS-ключей из ридера
    memsz = ((UINT)(romi & 0x7F) + 1) * 512;
    _tprintf(TEXT("Size of memory buffer: %u bytes.\n"), memsz);
 
    // в memszk полный объём буфера памяти в ключах (длина ACS-ключа 8 байт)
    memszk = memsz / 8;
    _tprintf(TEXT("Size of memory buffer: %u keys.\n"), memszk);
 
    // вычисляем количество ключей которое может поместиться в памяти,
    // мастер-ключ за ключ не считается
    total = memszk - 1;
 
    // при хранении ACS-ключей во внешней памяти (NVM) последняя запись
    // используется для хранения сигнатуры, т.е. ключом не является
    if ((romi & 0x80) == 0) total--;    // учитываем, что в NVM на один ключ меньше
 
    _tprintf(TEXT("Total memory capacity: %u(%s) keys.\n"),
        total,
        (romi & 0x80) ? TEXT("ROM") : TEXT("NVM"));
ferr_exit:
    link_close(&ccr);
    if (err) _tprintf(TEXT("Error: code %u!\n"), err);
    return 0;
}

Результат работы программы для ридера с ROM-памятью для хранения ключей.
acsk_romsz

Результат работы программы для ридера с NVM-памятью для хранения ключей.rd03ab_acsk_nvmsz

Необходимость обслуживания ACS-ключей возникает достаточно редко, поэтому в API отсутствуют специальные функции для работы с ACS ключами: все потребности при операциях с ключами обычно удовлетворяются консолью для настройки ридеров ccr. В случае необходимости, реализация таких функций разбивается на две задачи: чтение ACS-ключей и запись ACS-ключей.

Читать и записывать ACS-ключи можно только постранично с помощью команд CMG_KM / CMS_KM, т.е. по 64 ключа (8байт * 64ключа = 512байт); операции только с одним ключом невозможны. Для того чтобы поменять один ключ, надо считать страницу с 64 ключами целиком, поменять значение требуемого ключа и записать страницу обратно. Чтобы не заниматься динамическим выделением памяти, можно сразу статически выделить буфер объёмом 64Кбайта (65536 байт) – это максимальный объём памяти ACS-ключей, который может быть в ридере.

Считывать ACS-ключи можно классическим способом, считывая все страницы (время чтения всех страниц из внешней памяти объёмом 64Кбайта по USB ~9с), и быстрым способом, считав все страницы до первой пустой: в ридере есть функция, которая возвращает последнюю занятую страницу (CMF_KME с параметром 0), незанятые страницы можно не считывать, т.к. они заполнены байтом 0xFF.

Пример считывания данных чтением всех страниц.

// ACS-key slow read demo, V1.6, Windows
#include "rd0xAB.h"
 
#pragma pack(1)
typedef struct {
BYTE        b[8]; } ACSK;   // структура для хранения ACS ключа
#pragma pack()
 
// *** main ***
int __cdecl _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
    CCR         ccr;
    DWORD       err = LERR_SUCCESS;
    BYTE        buf[6];     // буфер для приёма конфигурации ридера и его SN
    ACSK        acsk[8192]; // буфер для приёма ACS-ключей
    ACSK        emptyk;     // пустой ключ
    BYTE        romi;       // ROM info byte: b.7 – тип памяти, b.6...b.0 – последняя страница памяти
    UINT        cnt, memszk;
 
    _tprintf(TEXT("ACS-key read demo for CCR RD-0xAB, V1.6.\n--\n"));
 
    memset(&emptyk, 0xFF, sizeof(emptyk));  // инициализация пустого ключа
 
    if ((link_hidopen(0, NULL, &ccr)) != LERR_SUCCESS) goto ferr_exit;
    // установка временного режима DIR с включенным бипером
    if ((err = link_packet(&ccr, CMS_MOD, 3 + 8, NULL, 0, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;
 
    // чтение информации о ридере    if ((err = link_packet(&ccr, CMG_VER, 0, NULL, 0, buf, 6)) != LERR_SUCCESS) goto ferr_exit;     romi = buf[1];  // запоминаем информационный байт    for (cnt = 0; cnt < = (UINT)(romi & 0x7F); cnt++) // цикл чтения страниц с ACS-ключами        if ((err = link_packet(&ccr, CMG_KM, (BYTE)cnt,            NULL, 0, acsk[cnt * 64].b, 512)) != LERR_SUCCESS) goto ferr_exit; 
    // вычисляем номер последнего ключа (это также объём памяти в ключах) для вывода ключей на экран
    memszk = (romi & 0x80) ? (UINT)(romi & 0x7F) * 64 - 1 : (UINT)(romi & 0x7F) * 64 - 2;
    for (cnt = 0; cnt <= memszk; cnt++)    
    {
        UINT    i;
        // выводим на экран мастер-ключ и непустые ключи
        if ((cnt == 0) || (memcmp(acsk[cnt].b, emptyk.b, 8)))
        {
            if (cnt == 0) _tprintf(TEXT("MAST: "));
            else _tprintf(TEXT("%4u: "), cnt);
            for( i = 0; i != 8; i++)    // вывод значения ACS-ключа
                _tprintf(TEXT("%02X"), acsk[cnt].b[i]);
            _tprintf(TEXT("\n"));
        }
    }
ferr_exit:
    link_close(&ccr);
    if (err) _tprintf(TEXT("Error: code %u!\n"), err);
    return 0;
}

Пример считывания данных быстрым способом.

// ACS-key fast read demo, V1.6, Windows
#include "rd0xAB.h"
 
#pragma pack(1)
typedef struct {
BYTE        b[8]; } ACSK;   // структура для хранения ACS ключа
#pragma pack()
 
// *** main ***
int __cdecl _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
    CCR         ccr;
    DWORD       err = LERR_SUCCESS;
    BYTE        buf[6];     // буфер для приёма конфигурации ридера и его SN
    ACSK        acsk[8192]; // буфер для приёма ACS-ключей
    ACSK        emptyk;     // пустой ключ
    BYTE        romi;       // ROM info byte: b.7 – тип памяти, b.6...b.0 – последняя страница памяти
    BYTE        lstpg;      // последняя занятая ключами страница памяти
    UINT        cnt, memszk;
 
    _tprintf(TEXT("ACS-key read demo for CCR RD-0xAB, V1.6.\n--\n"));
 
    memset(&emptyk, 0xFF, sizeof(emptyk));  // инициализация пустого ключа
 
    if ((link_hidopen(0, NULL, &ccr)) != LERR_SUCCESS) goto ferr_exit;
    // установка временного режима DIR с включенным бипером
    if ((err = link_packet(&ccr, CMS_MOD, 3 + 8, NULL, 0, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;
 
    // чтение информации о ридере    if ((err = link_packet(&ccr, CMG_VER, 0, NULL, 0, buf, 6)) != LERR_SUCCESS) goto ferr_exit;    romi = buf[1];  // запоминаем информационный байт     // чтение последней занятой ключами страницы (не путать с последней страницей памяти!)    if ((err = link_packet(&ccr, CMF_KME, 0, NULL, 0, &lstpg, 1)) != LERR_SUCCESS) goto ferr_exit;    // обязательная(!) инициализация массива ключей, т.к. при неполном чтении заполняется не весь массив    memset(&acsk, 0xFF, sizeof(acsk));               for (cnt = 0; cnt < = (UINT)lstpg; cnt++)    // цикл чтения страниц с ACS-ключами        if ((err = link_packet(&ccr, CMG_KM, (BYTE)cnt,            NULL, 0, acsk[cnt * 64].b, 512)) != LERR_SUCCESS) goto ferr_exit; 
    // вычисляем номер последнего ключа (это также объём памяти в ключах) для вывода ключей на экран
    memszk = (romi & 0x80) ? (UINT)(romi & 0x7F) * 64 - 1 : (UINT)(romi & 0x7F) * 64 - 2;
    for (cnt = 0; cnt <= memszk; cnt++)
    {
        UINT    i;
        // выводим на экран мастер-ключ и непустые ключи
        if ((cnt == 0) || (memcmp(acsk[cnt].b, emptyk.b, 8)))
        {
            if (cnt == 0) _tprintf(TEXT("MAST: "));
            else _tprintf(TEXT("%4u: "), cnt);
            for( i = 0; i != 8; i++)    // вывод значения ACS-ключа
                _tprintf(TEXT("%02X"), acsk[cnt].b[i]);
            _tprintf(TEXT("\n"));
        }
    }
ferr_exit:
    link_close(&ccr);
    if (err) _tprintf(TEXT("Error: code %u!\n"), err);
    return 0;
}

Цветом в обоих примерах выделена часть программы, отвечающая за чтение страниц с ACS-ключами. Видно, что при быстром чтении, чтение ведётся до последней занятой страницы, а не до конца памяти, что значительно сокращает время чтения.

Записывать ACS-ключи можно классическим способом, записывая все страницы подряд, и быстрым способом, выполнив полное стирание памяти, а затем записав только занятые страницы. Следует учитывать, что при полном стирании памяти мастер-ключ не стирается, т.е. при желании удалить мастер-ключ, чтобы запись новых ключей была возможна только с помощью компьютера, дать команду стирания или сброса настроек недостаточно: необходимо записать страницу 0 с пустым или новым мастер-ключом. При записи последней страницы памяти NVM следует сохранить сигнатуру NVM, иначе при следующем включении ридер отформатирует энергонезависимую память.

Стирание встроенной памяти ROM и NVM небольших объёмов можно производить, просто подав команду стирания CMF_KME. При стирании NVM больших объёмов, команда будет возвращать ошибки потери связи, т.к. полное стирание NVM может занимать до 10с. Поэтому универсальный программный код должен подавать команду стирания и, в случае возникновения ошибки, пинговать ридер, ожидая установления связи с ним. После установления связи, программа может продолжить работу.

Пример универсального кода стирания всех страниц ACS-ключей.

// посылка команды стирания памяти ACS-ключей
if ((err = link_packet(&ccr, CMF_KME, 0xA5, NULL, 0, NULL, 0)) == LERR_HARDWARE) goto ferr_exit;
if (err != LERR_SUCCESS)        // если была ошибка, то значит идёт "длинное" стирание
{
    // производим 32 попытки пинга ридера
    for (cnt = 0; cnt != 32; cnt++)
    {
        // цикл пинга: посылаем символ пинга IO_CHPING, ждём эха, затем меняем сивол, и так 8 раз;
        // пинг считается успешным, когда все 8 символов прошли без ошибок
        for (i = 0; i != PINGLEN_CN; i++)
        {
            if ((err = link_echobyte(&ccr, (BYTE)(IO_CHPING + i))) == LERR_HARDWARE) goto ferr_exit;
            if (err != LERR_SUCCESS) break;
        }
        if (err == LERR_SUCCESS) break;
        Sleep(150);
    }
}
if (err != LERR_SUCCESS) goto ferr_exit;
// здесь продолжаем выполнение кода программы
// ..........................................

Сигнатуру NVM можно узнать при чтении ACS-ключей – это последняя запись на последней странице: “AnyRAM\x00\x0N”, где N – число, указывающее объём памяти (тип) микросхемы. Допустимы пять значений N: 1 – тип NVM 24C32, объём 4Кб; 2 – тип NVM 24C64, объём 8Кб; 3 – тип NVM 24C128, объём 16Кб; 4 – тип NVM 24C256, объём 32Кб; 5 – тип NVM 24C512, объём 64Кб.

Если программа обладает ограниченным количеством ресурсов, например, на платформе Arduino, где в контроллере каждый байт памяти на счету, сформировать сигнатуру можно “вручную”, зная последнюю страницу NVM.

Пример формирования сигнатуры для записи последней страницы NVM.

BYTE        nvmsign[8] = { 'A', 'n', 'y', 'R', 'A', 'M', 0, 0 };
BYTE        nsz, tb = romi & 0x7F; // romi – последняя страница памяти (смещение +1 из команды CMG_VER)
// сдвигаем значение последней страницы, пока в бите 3 не появится 0
for (nsz = 1; nsz != 5; nsz++, tb >>= 1)
    if ((tb & 8) == 0) break;
nvmsign[7] = nsz;
memcpy(acsk[(romi & 0x7F) < < 6) | 63].b, nvmsign, 8);

Для ускорения работы ридера и процесса записи ключей, рекомендуется уплотнять ключи: удалять повторяющиеся ключи, убирать чередование рабочих ключей с пустыми ключами, смещать рабочие ключи к началу памяти, т.к. проверка на наличие ключа в памяти производится перебором ключей, начиная с младших адресов.

Пример записи данных путём записи всех страниц.

// ACS-key slow write demo, V1.6, Windows
#include "rd0xAB.h"
 
#pragma pack(1)
typedef struct {
BYTE        b[8]; } ACSK;   // структура для хранения ACS ключа
#pragma pack()
 
// *** main ***
int __cdecl _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
    CCR         ccr;
    DWORD       err = LERR_SUCCESS;
    BYTE        buf[6];     // буфер для приёма конфигурации ридера и его SN
    ACSK        acsk[8192]; // буфер ACS-ключей
    BYTE        romi;       // ROM info byte: b.7 – тип памяти, b.6...b.0 – последняя страница памяти
    BYTE        lstpg;      // последняя страница памяти
    UINT        cnt, memszk;
 
    _tprintf(TEXT("ACS-key slow write demo for CCR RD-0xAB, V1.6.\n--\n"));
 
    if ((link_hidopen(0, NULL, &ccr)) != LERR_SUCCESS) goto ferr_exit;
    // установка временного режима DIR с включенным бипером
    if ((err = link_packet(&ccr, CMS_MOD, 3 + 8, NULL, 0, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;
    // чтение информации о ридере
    if ((err = link_packet(&ccr, CMG_VER, 0, NULL, 0, buf, 6)) != LERR_SUCCESS) goto ferr_exit;
    romi = buf[1];  // запоминаем информационный байт
    // стираем все ключи в памяти
    memset(acsk, 0xFF, sizeof(acsk));
    // устанавливаем новый мастер-ключ: UID=11223344, ACSK=0411223344000000
    memcpy(acsk, "\x04\x11\x22\x33\x44\x00\x00\x00", 8);
    // вычисляем номер последнего ключа
    memszk = ((romi & 0x7F) + 1) * 64 - 1;
    if ((romi & 0x80) == 0) memszk--;       // - 1 для внешней NVM
    // вычисляем номер последней страницы    lstpg = (BYTE)(memszk >> 6);    // запись всех страниц памяти    for (cnt = 0; cnt < = lstpg; cnt++)    {        // проверка записи последней страницы NVM: формируем сигнатуру, если условие выполнено        if (((romi & 0x80) == 0) && (cnt == (UINT)(romi & 0x7F)))        {            BYTE        nvmsign[8] = { 'A', 'n', 'y', 'R', 'A', 'M', 0, 0 };            BYTE        nsz, tb = romi & 0x7F;            // сдвигаем значение последней страницы, пока в бите 3 не появится 0            for (nsz = 1; nsz != 5; nsz++, tb >>= 1) if ((tb & 8) == 0) break;            nvmsign[7] = nsz;            memcpy(acsk[(cnt < < 6) | 63].b, nvmsign, 8);        }        _tprintf(TEXT("Page %u writing... "), cnt);        // write ACS page        if ((err = link_packet(&ccr, CMS_KM, (BYTE)cnt,            acsk[cnt << 6].b, 512, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;        _tprintf(TEXT("Ok.\n"));    }ferr_exit:
    link_close(&ccr);
    if (err) _tprintf(TEXT("Error: code %u!\n"), err);
    return 0;
}

Пример записи данных быстрым способом.

// ACS-key fast write demo, V1.6, Windows
#include "rd0xAB.h"
#pragma pack(1)
typedef struct {
BYTE        b[8]; } ACSK;   // структура для хранения ACS ключа
#pragma pack()
 
// *** main ***
int __cdecl _tmain(int argc, TCHAR *argv[], TCHAR *envp[])
{
    CCR         ccr;
    DWORD       err = LERR_SUCCESS;
    BYTE        buf[6];     // буфер для приёма конфигурации ридера и его SN
    ACSK        acsk[8192]; // буфер для приёма ACS-ключей
    ACSK        emptyk;     // пустой ключ
    BYTE        romi;       // ROM info byte: b.7 – тип памяти, b.6...b.0 – последняя страница памяти
    BYTE        lstpg;      // последняя занятая ключами страница памяти
    UINT        cnt, memszk;
 
    _tprintf(TEXT("ACS-key fast write demo for CCR RD-0xAB, V1.6.\n--\n"));
    memset(&emptyk, 0xFF, sizeof(emptyk));  // инициализация пустого ключа
 
    if ((link_hidopen(0, NULL, &ccr)) != LERR_SUCCESS) goto ferr_exit;
    // установка временного режима DIR с включенным бипером
    if ((err = link_packet(&ccr, CMS_MOD, 3 + 8, NULL, 0, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;
    // чтение информации о ридере
    if ((err = link_packet(&ccr, CMG_VER, 0, NULL, 0, buf, 6)) != LERR_SUCCESS) goto ferr_exit;
    romi = buf[1];  // запоминаем информационный байт
    // стираем все ключи в памяти
    memset(acsk, 0xFF, sizeof(acsk));
    // устанавливаем новый мастер-ключ: UID=11223344, ACSK=0411223344000000
    memcpy(acsk, "\x04\x11\x22\x33\x44\x00\x00\x00", 8);
 
    // вычисляем номер последнего ключа в памяти ридера (объём памяти в ключах)    memszk = ((romi & 0x7F) + 1) * 64 - 1;    if ((romi & 0x80) == 0) memszk--;       // - 1 для внешней NVM    // ищем последнюю занятую страницу по первому непустому ключу, не включая мастер-ключ    for (cnt = memszk; cnt != 0; cnt--)     // обратный поиск непустого ключа        if (memcmp(acsk[cnt].b, emptyk.b, 8)) break;    lstpg = (BYTE)(cnt >> 6);               // вычисление страницы, на которой хранится ключ    // * стирание памяти ACS-ключей *    if ((err = link_packet(&ccr, CMF_KME, 0xA5, NULL, 0, NULL, 0)) == LERR_HARDWARE) goto ferr_exit;    if (err != LERR_SUCCESS)    // если была ошибка, то значит идёт "длинное" стирание    {        // производим 32 попытки пинга ридера        for (cnt = 0; cnt != 32; cnt++)        {            UINT        i;            for (i = 0; i != PINGLEN_CN; i++)            {                if ((err = link_echobyte(&ccr, (BYTE)(IO_CHPING + i))) == LERR_HARDWARE) goto ferr_exit;                if (err != LERR_SUCCESS) break;            }            if (err == LERR_SUCCESS) break;            Sleep(150);        }    }    if (err != LERR_SUCCESS) goto ferr_exit;    // * запись только занятых страниц памяти до первой свободной *    for (cnt = 0; cnt < = lstpg; cnt++)    {        if (((romi & 0x80) == 0) && (cnt == (UINT)(romi & 0x7F)))        {            BYTE        nvmsign[8] = { 'A', 'n', 'y', 'R', 'A', 'M', 0, 0 };            BYTE        nsz, tb = romi & 0x7F;            // сдвигаем значение последней страницы, пока в бите 3 не появится 0            for (nsz = 1; nsz != 5; nsz++, tb >>= 1) if ((tb & 8) == 0) break;            nvmsign[7] = nsz;            memcpy(acsk[(cnt < < 6) | 63].b, nvmsign, 8);        }        _tprintf(TEXT("Page %u writing... "), cnt);        // write ACS page        if ((err = link_packet(&ccr, CMS_KM, (BYTE)cnt,            acsk[cnt << 6].b, 512, NULL, 0)) != LERR_SUCCESS) goto ferr_exit;        _tprintf(TEXT("Ok.\n"));    }ferr_exit:
    link_close(&ccr);
    if (err) _tprintf(TEXT("Error: code %u!\n"), err);
    return 0;
}

Цветом в обоих примерах выделена часть программы, отвечающая за запись страниц с ACS-ключами. Видно, что при быстрой записи, запись ведётся до первой свободной страницы, а не до конца памяти, что значительно сокращает время. Несмотря на то, что размер кода для записи ACS-ключей быстрым способом больше, запись в большинстве случаев происходит значительно быстрее. В приведённых примерах запись классическим способом 128 страниц занимает около 3х минут, запись тех же данных быстрым способом занимает 16с, т.е. преимущество быстрого алгоритма записи налицо.

Разумеется, когда память ридера близка к заполнению, быстрые алгоритмы не будут давать преимущества в скорости работы с памятью, однако, при неполной занятости памяти, т.е. в большинстве случаев, быстрые алгоритмы очень эффективны.