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