
Всем привет!
В прошлых статьях речь шла о возможностях смарт-карты, которые не получают должного освещения в публикациях, а именно: работа с ISD, создание своего SSD, сторонние библиотеки и криптография. Эти темы лишь часть огромного массива, который можно исследовать до самой китайской пасхи, а ведь хочется идти дальше, посмотреть, что еще интересного таит "Вселенная Java Card".
По этой причине я решил долго здесь не задерживаться и идти к новым горизонтам. Итак, встречайте - UICC HOTP!
В этой статье:
Демо: UI/UX;
UICC апплет;
Нюансы параметров инсталляции;
Гоняем тесты в консоли.
Исходники: апплет UICC HOTP, FunGP.
Демо: UI/UX
Дамы и господа, внимание на экран:

На видео следующий сюжет: в 12:04 по полудню пользователь дважды выбирает опцию "Сгенерировать HOTP" и получает один и тот же код "227806". Затем, в 12:05 направляет еще два запроса, но значение уже другое, "859878".
Жизнь пароля быстра и скоротечна: короткая минута но яркое мгновение, за которое он свершает свое предназначение, свой "Opus magnum". А затем забвение, тлен и небытие.
Мда, кажется, мне надо поменьше слушать ALI перед сном. Но отставить меланхолию, продолжаем повествование.
Вшивая HOTP в SIM-карту мы получаем такие преимущества, как доверенный источник, двух-факторая аутентификация "из коробки" и кросс-платформенность - от Нокиа 3310 до Айфона последней модели - UX везде будет одинаковым.
Так, с вводной частью покончили. Огромная благодарность всем, кто пришел за быстрым дофамином, надеюсь вам понравилось!

Остальных же я попрошу остаться. Еще на одну минуту...
UICC апплет
Для данного проекта понадобилась более навороченная железяка:

В России ее хрен достанешь, потому пришлось нехило так повозиться и потратиться, прежде чем мой комплект попал ко мне в руки. Несколько экземпляров готов выставить на продажу (дорого, бартер/интим не предлагать), так что если кому надо - пишите в личку.
Начнем разбирать наш апплет на части. Первым идет конструктор:
public UICC_HOTP(byte[] install_params, short offset, byte length) { length = install_params[offset++]; // instance AID length offset += length; length = install_params[offset++]; // Control info length offset += length; length = install_params[offset++]; // Grab the length of HMAC key secret_key = new byte[length]; Util.arrayCopyNonAtomic(install_params, offset, secret_key, _00, length); try { hmac_key = (HMACKey)KeyBuilder.buildKey(KeyBuilder.ALG_TYPE_HMAC, JCSystem.MEMORY_TYPE_TRANSIENT_RESET, (short)(length * 8), false); hmac_sign = Signature.getInstance(MessageDigest.ALG_SHA_256, Signature.SIG_CIPHER_HMAC, Cipher.PAD_NULL, true); } catch (CryptoException e) { ISOException.throwIt((short)(SW_CRYPTO_EXCEPTION + 1)); } }
Как и в предыдущих проектах, здесь мы сохраняем байты HMAC-ключа в массиве secret_key. Далее в блоке try-catch создаем экземпляры HMACKey и Signature с параметром SIG_CIPHER_HMAC. В целом схема действий такая:

Обратите внимание, что в конструкторе пропущен шаг №3.
Тут, конечно, у меня знатно подгорело. Дело в том, что изначально я не хотел никакого 'secret_key', а сразу же сохранить байты в ключе, однако после долгой отладки выяснилось, что на моих картах (как обстоят дела с другими - не знаю) механизм хранения ключа в постоянной памяти не работает. Может в общем механизм кривой, а может конкретно HMACKey косячит - не знаю, не ведаю, но чтобы воспроизвести проблему надо:
На этапе инсталляции создать ключ HMACKey с типом памяти JCSystem.MEMORY_TYPE_PERSISTENT;
Там же в конструкторе вызвать HMACKey.setKey( /* ключевой материал */ );
После инсталляции (в рабочем режиме) вызов Signature.init(HMACKey) упадет с CryptoException.UNINITIALIZED_key.
Обойти эту проблему пришлось посредством создания HMAC-ключа в переменной памяти, который инициализируется значением из буфера 'secret_key' каждый раз при вызове.
Также будьте внимательны с параметром "KeyLength" метода KeyBuilder.buildKey() т.к. в нем надо задавать длину ключа в битах, не байтах.
Далее идет install():
public static void install(byte[] install_params, short offset, byte length) { UICC_HOTP applet = new UICC_HOTP(install_params, offset, length); applet.register(); applet.reg = ToolkitRegistrySystem.getEntry(); applet.iid_sim_hotp = applet.reg.initMenuEntry(strings, STR_ALPHA_ID_OFF, STR_ALPHA_ID_LEN, PRO_CMD_SELECT_ITEM, false, (byte)_00, _00); }
Первое, что надо запомнить это последовательность вызовов: сначала Applet.register(), а потом ToolkitRegistrySystem.getEntry(). UICC API for Java Card REL-15 гласит, что в противном случае последний вернет null.
Метод ToolkitRegistry.initMenuEntry() - выполняет две важные задачи:
создает запись "SIM HOTP" в STK-меню телефона и возвращает ее идентификатор на сохранность;
подписывает апплет на событие "EVENT_MENU_SELECTION".
Вообще, тут требуется небольшое отступление с описанием архитектуры фреймворка UICC. В отличии от классической Java Card, SIM-апплет никогда не селектируется (потому-то метод process() остается в виде заглушки). На карте их может быть с десяток, подписанные на различные события (смена локации, поступление звонка/SMS и прочие).
Есть события точечные, например SMS-ка: даже если подписаны все 10 апплетов, операционка дернет тот апплет, чей TAR (Toolkit Application Reference, три байта) указан в метаданных сообщения.
А есть события массовые, например, таймер или запрос STK-меню, по наступлению которых ОС дернет всех подписантов.
Так вот, ToolkitRegistry.initMenuEntry(), подписывает нас на событие, связанное с входом пользователя в STK-меню. Обработка оного осуществляется в методе processToolkit():
public void processToolkit(short event) { EnvelopeHandler env_hdlr; byte item_id; try { switch (event) { case EVENT_MENU_SELECTION: { env_hdlr = EnvelopeHandlerSystem.getTheHandler(); item_id = env_hdlr.getItemIdentifier(); if (item_id == iid_sim_hotp) { hotp_submenu(); } } break; default: return; } } catch (ArrayIndexOutOfBoundsException exc) {error_logger(SW_ARRAY_BOUNDARY_EXCEPTION);} catch (NullPointerException exc) {error_logger(SW_NULL_PTR_EXCEPTION);} catch (ISOException exc) {error_logger(exc.getReason());} catch (ToolkitException exc) {error_logger((short)(SW_TOOLKIT_EXCEPTION | exc.getReason()));} catch (CryptoException exc) {error_logger((short)(SW_CRYPTO_EXCEPTION | exc.getReason()));} catch (SystemException exc) {error_logger((short)(SW_SYSTEM_EXCEPTION | exc.getReason()));} catch (Exception exc) {error_logger(SW_UNKNOWN_EXCEPTION);} }
Как было сказано ранее, EVENT_MENU_SELECTION - массовое событие, а потому апплет должен позаботиться о том, чтобы отреагировать только в том случае, если пользователь выбрал именно его. Для этого он сравнивает идентификатор выбранного пункта меню со своим, присвоенным на этапе вызова initMenuEntry() в методе install(). У нас-то простой случай, всего один пункт, а гляньте, что происходит в STK-меню Билайна:

Причем необязательно, чтобы под каждый пункт был свой апплет: метод initMenuEntry() можно вызвать несколько раз, тем самым создав вышепредставленную структуру.
Если пользователь выбрал наш пункт, то processToolkit() передает управление сюда:
protected void hotp_submenu() { byte tag = (byte)(TAG_ITEM | TAG_SET_CR); ProactiveHandler pro_hdlr = ProactiveHandlerSystem.getTheHandler(); ProactiveResponseHandler pro_resp_hdlr = ProactiveResponseHandlerSystem.getTheHandler(); pro_hdlr.init(PRO_CMD_SELECT_ITEM, (byte)_00, DEV_ID_TERMINAL); pro_hdlr.appendTLV(TAG_ALPHA_IDENTIFIER, strings, STR_ALPHA_ID_OFF, STR_ALPHA_ID_LEN); pro_hdlr.appendTLV(tag, ITEM_ID_HOTP, strings, STR_MENU_1_OFF, STR_MENU_1_LEN); pro_hdlr.appendTLV(tag, ITEM_ID_HELP, strings, STR_MENU_2_OFF, STR_MENU_2_LEN); if (RES_CMD_PERF != pro_hdlr.send()) { return; } switch (pro_resp_hdlr.getItemIdentifier()) { case ITEM_ID_HOTP: option_totp(pro_hdlr, pro_resp_hdlr); break; case ITEM_ID_HELP: option_help(pro_hdlr, pro_resp_hdlr); break; } }
где, формируется и посылается проактивная команда "SELECT ITEM" с последующим ожиданием ответа системы. Если команда выполнилась успешно, то пользователь увидит это:

Метод ProactiveResponseHandler.getItemIdentifier() является блокирующим и вернется только после того, как пользователь клацнет на одну из возможных опций. Кликнув на первую поток исполнения перейдет в целевой метод:
private void option_totp(ProactiveHandler pro_hdlr, ProactiveResponseHandler pro_resp_hdlr) { short len; int dbc = 0; int divisor = 1000000; byte[] global_buff; pro_hdlr.init(PRO_CMD_PROVIDE_LOCAL_INFORMATION, QLFR_DATE_TIME_INFO, DEV_ID_TERMINAL); if (pro_hdlr.send() != RES_CMD_PERF) { ISOException.throwIt((short)(SW_TOOLKIT_EXCEPTION | 0x13)); } if (pro_resp_hdlr.findTLV(TAG_DATE_TIME_AND_TIME_ZONE, (byte)_01) == TLV_NOT_FOUND) { ISOException.throwIt((short)(SW_TOOLKIT_EXCEPTION | 0x14)); } global_buff = UICCPlatform.getTheVolatileByteArray(); len = (short)(pro_resp_hdlr.getValueLength() - 2); // truncate seconds and time zone pro_resp_hdlr.copyValue(_00, global_buff, _00, len); hmac_key.setKey(secret_key, _00, (short)secret_key.length); hmac_sign.init(hmac_key, Signature.MODE_SIGN); len = hmac_sign.sign(global_buff, _00, len, global_buff, _00); if (len != _32) { ISOException.throwIt(ISO7816.SW_DATA_INVALID); } len = (short)((short)global_buff[31] & (short)0x000F); dbc = get_int(global_buff, len); // 4. Generate DBC (Dynamic Binary Code) dbc %= divisor; int_to_dec_str(dbc, global_buff, _00, _06); pro_hdlr.initDisplayText((byte)0x81, (byte)_04, global_buff, _00, _06); pro_hdlr.send(); }
Давайте разберем его на части.
Метод pro_hdlr.init(PRO_CMD_PROVIDE_LOCAL_INFORMATION) формирует запрос текущей даты и времени, затем вызывается pro_hdlr.send() который непосредственно шлет его. Если телефон поддерживает такую фичу, то апплету надо дернуть метод pro_resp_hdlr.findTLV(), чтобы получить приблизительно такую строку:
"26 07 620152212595 21" , где:
26 - тег DATE TIME параметра;
07 - длина данных;
62.01.52 21:25:95 - YY.MM.DD HH:MM:SS в формате BCD. Ниблы надо развернуть и тогда получим 26.10.25 12:52:59.
21 - кодировка часового пояса. Также надо развернуть и получим "12". Согласно ETSI 123 040, часовые пояса кодируются с интервалом 15 минут к UTC за единицу измерения. Таким образом: 12 единиц * 15 минут = 180 минут = +3 часа к UTC.
Обратите внимание на этот блок кода:
global_buff = UICCPlatform.getTheVolatileByteArray(); len = (short)(pro_resp_hdlr.getValueLength() - 2); // truncate seconds and time zone pro_resp_hdlr.copyValue(_00, global_buff, _00, len);
На первой строке мы берем указатель на глобальный буфер. Очень удобная штука, избавляет нас от необходимости создавать свой. В него мы кладем байты даты и времени но отсекаем секунды и часовой пояс. Секунды отсекаем, чтобы код обновлялся лишь раз в минуту, а не каждую секунду, а часовой пояс в данной реализации не требуется. В конце, в соответствии с RFC 4226 генерируем одноразовый пароль. Нюансы реализации я оставлю за скобками.
Завершает процедуру вызов pro_hdlr.initDisplayText(), который выведет на дисплей шесть цифр одноразового пароля.
Собрать .cap-файл необходимо командой ant:

Нюансы параметров инсталляции
Традиционно для тестирования воспользуемся библиотекой FunGP. Скачайте (или подтяните обновления), установите (README.md в помощь) и создайте в корневой директории папку 'resources' - в нее надо поместить апплет UICC_HOTP.cap и .json-файл с ключами от сим-карты. Если с апплетом все понятно, то .json-файл требует объяснения.

На скрине изображен рабочий проект. В папке 'resources' лежат .cap-файлы и sysmocom-cards.json, хранящий записи вида "IMSI, счетчик, ключи, идентификатор ключей, ICCID". Карта, вставленная в ридер будет однозначно идентифицирована по полю IMSI. Такое решение обусловлено наличием хреновой кучи тестовых карт и нежеланием вручную сопоставлять ICCID и ключи.
Переходим к первому скрипту: tests/UICC_HOTP/01_install.py
from fun_gp import Reader, SmartCard, CCM, UICC, InstallParams, \ SIMParams, UICCParams, SCP80, SCP80Params,\ RESOURCES_DIR from fun_gp import lv_hex, encode_bcd import time card_deck_path = RESOURCES_DIR / "sysmocom-cards.json" path_to_applet = RESOURCES_DIR / 'UICC_HOTP.cap' sys_params = UICCParams(msl='16', tar='46554E', text_length='14', menu_entries='010100') install_params = InstallParams( app_params = '01020304050607000010203040506070 01020304050607000010203040506070\ 01020304050607000010203040506070 01020304050607000010203040506070', sim_params=sys_params ) def main(): with Reader() as reader: scp80 = SCP80(card_deck_path, reader.plain_apdu) scp80.set_params(SCP80Params(spi='0621', tar='000000', kic=0x05, kid=0x05)) uicc = UICC(reader.plain_apdu, scp80) uicc.terminal_profile() startTime = time.time() uicc.install_app_scp80(path_to_applet, install_params) duration = time.time() - startTime print(f'duration: {str(round(duration, 2))}\n') main()
Надеюсь его лаконичный вид это не мое субъективное восприятие, а общепризнаваемый факт. Давайте по-порядку:
Объект UICCParams() реализует спецификацию ETSI 102 226, в частности, параметры команды install (п. 8.2.1.3). Учтите, что они разнятся для SIM и UICC:

Экземпляр UICCParams(), вместе с 64-байтовым ключом HMAC передается в InstallParams(), который, в свою очередь, будет передан командой install[for install & make selectable]. Но это в самом конце, сейчас же надо разобраться с SCP80Params().
class SCP80Params: def __init__(self, spi:str='1621', tar:str='000000', kic:int=0x00, kid:int=0x00, por:int=0x00): self.spi = hex_to_bytes(spi) self.kic = kic self.kid = kid self.tar = hex_to_bytes(tar) self.por = por def _build_list(self): return self.spi + [self.kic] + [self.kid] + self.tar + [self.por] def __getitem__(self, index): return self._build_list()[index] def __len__(self): return len(self._build_list())
Этот класс имплементирует параметры коммуникации по SCP80 в соответствии с ETSI 102 225:
SPI - определяет уровень накладываемой криптографии. По дефолту требуется инкремент счетчика, шифрование и имитовставка, PoR (Подтверждение получения SMS) только в случае возникновения ошибки.
TAR - по дефолту мы обращаемся к ISD. Вообще, стандартизированные TAR'ы можно посмотреть в ETSI 102 220, annex D.
KIc - параметры шифрования сообщений.
KID - параметры выработки имитовставки.
Конкретно в нашем случае применимы следующие значения:
SPI = 0621: счетчик не инкрементировать (не надо ушатывать flash).
KIc = 0x05: шифруем на 3DES-CBC с двумя ключами.
KID = 0x05: имитовставка на 3DES-CBC с двумя ключами.
Имейте в виду, что есть карты, где длина ключей 24 байта, тогда KIc/KID будут 0x09.
Ну и под конец вызывается install_app_scp80(), который в процессе работы выводит следующий лог:
Лог команды install_app_scp80
(.venv) PS C:\Users\tislamov\develop\FunGP\tests\UICC_HOTP> .\01_install.py Connecting to 'ACS ACR39U ICC Reader 0' reader Command: select MF >> 00A4 000C 02 3F00 << 9000 [OK ] duration: 0.0 Command: select DF GSM >> 00A4 000C 02 7F20 << 9000 [OK ] duration: 0.01 Command: select EF IMSI >> 00A4 000C 02 6F07 << 9000 [OK ] duration: 0.0 Command: read IMSI >> 00B0 0000 00 << 6c09 [Response bytes still available (CAT) (9 bytes remaining)] Repeat command with Le set to 09 >> 00B0 0000 09 << 089999070000818304 << 9000 [OK ] duration: 0.0 IMSI: 809999700000183840 Command: Terminal Profile >> 8010 0000 0A FFFFFFFFFFFFFFFFFFFF << 9000 [OK ] duration: 0.0 Command: Envelope [SCP80] INSTALL(for load) >> 80C2 0000 5C D15A8202838106038121438B4F600A8110325476987FF6622081410200213D0270000038150621151500000035A18D9948078ACEF8984857C41E4E19B7F749C22DC45034A7F8A116F67B57D96B28AE1E2AB1D5CDFA8653F31F724DA0 << 9134 [To be classified ] duration: 0.05 Command: Fetch >> 8012 0000 34 << D032810301130082028183050086038121438B2041000A81103254769800F614027100000F0A0000000000000005000002900000 << 9000 [OK ] duration: 0.0 command number: 01 command type : SEND SHORT MESSAGE qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : Network Alpha identifier: Address : 1234 SMS type : SMS-SUBMIT Message ref : 00 number length : 10 TON/NPI : 81 dest address : 0123456789 PID : 00 DCS : f6 TP-ValPeriod : 14 TP-UDL : 2 TP-UD : 7100000F0A0000000000000005000002900000 Command: Terminal response >> 8014 0000 0C 810301130082028281830100 << 9000 [OK ] duration: 0.0 Command: Envelope [SCP80] LOAD >> 80C2 0000 AD D181AA8202838106038121438B819E600A8110325476987FF6622081410200218C070003010201700000E8150621151500000054323ADCDD870EA6A3E2DB8513CC56E95EC17FEEC3CB08B9A27F5A416887CA11CDA378FEE97F401F5BF0244340FBB1D6AFF6D1ECC3DCF8C435776375FA92C66D9283939F9896B5C539538923A031FC0C6683AA259666213AB16608DB052A5545AAFD30C99F9105F4967E1AE659549D4B87903A1FB8E4E69F818A << 9000 [OK ] duration: 0.02 Command: Envelope [SCP80] LOAD >> 80C2 0000 8C D181898202838106038121438B7E600A8110325476987FF6622081410200216C0500030102022976BE30F472A38FAA979794B8CD2EE990B3A0B3D367AF2903E4A9A142F0AFFF2EBA45C4ECFDCD9D9CAAF011CCF89FDFEC4CC521595DC8C170054F52F9D72FD01FBDE2C9FEFAD8FD965507545522B9882EF59B286C10943B1EA4B5B78607AD7C5C64213AFB91 << 9134 [To be classified ] duration: 0.38 Command: Fetch >> 8012 0000 34 << D032810301130082028183050086038121438B2041000A81103254769800F614027100000F0A0000000000000005000002900000 << 9000 [OK ] duration: 0.01 command number: 01 command type : SEND SHORT MESSAGE qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : Network Alpha identifier: Address : 1234 SMS type : SMS-SUBMIT Message ref : 00 number length : 10 TON/NPI : 81 dest address : 0123456789 PID : 00 DCS : f6 TP-ValPeriod : 14 TP-UDL : 2 TP-UD : 7100000F0A0000000000000005000002900000 Command: Terminal response >> 8014 0000 0C 810301130082028281830100 << 9000 [OK ] duration: 0.0 ////////// ДЛИННАЯ ЦЕПОЧКА ИЗ КОМАНД LOAD ////////// Command: Envelope INSTALL[for install and make selectable] >> 80C2 0000 AD D181AA8202838106038121438B819E600A8110325476987FF6622081410200218C070003010201700000B01506211515000000AE74094D203F1349F0CF4075C2A1359072E95D4ED473A804D054BA3C6651ACEB18CD19289C295C7ED41435C7BD73487EB4F359EB86D8AF0514BC06AB35302D935DBCBC89BCDBFBBBED065A02E23BD9FA4336528CE167FF38DAA1ACF21F20669DAFF1A07EA75990070FDD23C7DB31C2733E37BCCAD45A54A03CDE << 9000 [OK ] duration: 0.02 Command: Envelope INSTALL[for install and make selectable] >> 80C2 0000 53 D1518202838106038121438B46600A8110325476987FF6622081410200213405000301020255B8A0704C82295D4917C58FDFE44E3F4E64FED95B179A02C34BF700338BA8276A062049F5A3F5AE2A82C0003C38 << 9134 [To be classified ] duration: 0.59 Command: Fetch >> 8012 0000 34 << D032810301130082028183050086038121438B2041000A81103254769800F614027100000F0A0000000000000005000002900000 << 9000 [OK ] duration: 0.02 command number: 01 command type : SEND SHORT MESSAGE qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : Network Alpha identifier: Address : 1234 SMS type : SMS-SUBMIT Message ref : 00 number length : 10 TON/NPI : 81 dest address : 0123456789 PID : 00 DCS : f6 TP-ValPeriod : 14 TP-UDL : 2 TP-UD : 7100000F0A0000000000000005000002900000 Command: Terminal response >> 8014 0000 0C 810301130082028281830100 << 912a [To be classified ] duration: 0.0 Command: Fetch >> 8012 0000 2A << D028810301250082028182850C53544B2053657276696365738F0C8081080853494D20484F5450180124 << 9000 [OK ] duration: 0.0 command number: 01 command type : SET UP MENU qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : ME Alpha identifier: TK Services item 80: SIM HOTP (plain hex: 8081080853494D20484F5450) Command: Terminal response >> 8014 0000 0C 810301250082028281830100 << 9000 [OK ] duration: 0.01 ***** CAP-file parameters ***** Package AID: A000000087 Applet AID: A0000000875549434320484F5450 Applet size: 1520 bytes. Total SMS sent: 9 duration: 3.42 Reader: context has been released. JSON file has been updated. (.venv) PS C:\Users\tislamov\develop\FunGP\tests\UICC_HOTP>
Я добавил в FunGP модуль "proactive_parser.py", который худо-бедно приводит байты в человеко-читаемые строки, но он требует доработки. Этот же парсер поможет нам отлаживаться в консоли. Поехали!
Гоняем тесты в консоли
Переключаемся на скрипт 03_display_hotp.py:
def main(): with Reader() as reader: scp80 = SCP80(card_deck_path, reader.plain_apdu) scp80.set_params(SCP80Params(spi='0621', tar='46554E', kic=0x05, kid=0x05)) uicc = UICC(reader.plain_apdu, scp80) uicc.terminal_profile() event_menu_selection(uicc, 0x80, 0x01)
В методе main() инициализируем ридер и карту точно такими же параметрами, что использовались на этапе инсталляции. затем вызываем event_menu_selection():
def event_menu_selection(uicc:UICC, menu_id:int, item_id:int): event = MenuSelection(menu_id) add_info_select = [0x10, 0x01, item_id] curr_time = datetime.now() curr_time = encode_bcd(curr_time.strftime("%y%m%d%H%M%S")) add_info_date_time = hex_to_bytes(f'26 07 {curr_time}21') resp, sw1, sw2 = uicc.envelope(event[0:], cmd_name=f'[EVENT DOWNLOAD] SELECT HOTP option') while sw1 == 0x91: uicc.gen_res.add_info = [] resp, _, _ = uicc.fetch(sw2) cmd_type = uicc.parser.parse(resp) if cmd_type[3] == 0x26: # DATE/TIME uicc.gen_res.add_info = add_info_date_time elif cmd_type[3] == 0x24: # SELECT ITEM uicc.gen_res.add_info = add_info_select _, sw1, sw2 = uicc.terminal_response(cmd_type, uicc.gen_res) print(f"\ncurrent date: {curr_time}\n")
Создаем экземпляр класса MenuSelection, реализующий требования п. 7.2 ETSI 102 223, далее запрашиваем текущую дату и время и формируем TLV-структуру "DATE TIME".
Теперь главное - симулируем телефон: шлем на карту команду ENVELOPE типа "пользователь вошел в STK-меню":
Command: Envelope [EVENT DOWNLOAD] SELECT HOTP option >> 80C2 0000 09 D30782020181900180 << 9142 [To be classified ] duration: 0.0
Как только ToolkitRegistry передаст его нашему апплету, тот сразу же обработает его и скажет, что у него есть '42 (66) байт проактивной команды':
Command: Fetch >> 8012 0000 42 << D040810301240082028182050B81080853494D20484F54508F1601811208A1B3B5BDB5C0B8C0BEB2B0C2CC20484F54508F1002810C089DC3B6BDB020BFBEBCBEC9CC << 9000 [OK ] duration: 0.0 command number: 01 command type : SELECT ITEM qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : ME Alpha ID : SIM HOTP item 01: Сгенерировать HOTP (plain hex: 01811208A1B3B5BDB5C0B8C0BEB2B0C2CC20484F5450) item 02: Нужна помощь (plain hex: 02810C089DC3B6BDB020BFBEBCBEC9CC)
В этот самый момент на экране высвечивается наше меню. На эту команду наш скрипт отвечает здесь:
while sw1 == 0x91: uicc.gen_res.add_info = [] resp, _, _ = uicc.fetch(sw2) cmd_type = uicc.parser.parse(resp) if cmd_type[3] == 0x26: # DATE/TIME uicc.gen_res.add_info = add_info_date_time elif cmd_type[3] == 0x24: # SELECT ITEM uicc.gen_res.add_info = add_info_select <<<==== вот тут отвечает
Command: Terminal response >> 8014 0000 0F 810301240082028281830100100101 << 910b [To be classified ] duration: 0.01
В ответе Терминал говорит: пользователь выбрал пункт №1. На что карта парирует: "тогда у меня еще "0B (11) байт проактивной команды":
Command: Fetch >> 8012 0000 0B << D009810301260382028182 << 9000 [OK ] duration: 0.0 command number: 01 command type : PROVIDE LOCAL INFORMATION qualifier : 03 (see ETSI 102 223, c. 8.6) from : UICC to : ME
Здесь карта непосредственно запрашивает текущую дату и время. На эту команду ответ формируется здесь:
while sw1 == 0x91: uicc.gen_res.add_info = [] resp, _, _ = uicc.fetch(sw2) cmd_type = uicc.parser.parse(resp) if cmd_type[3] == 0x26: # DATE/TIME uicc.gen_res.add_info = add_info_date_time <<<==== вот тут отвечает elif cmd_type[3] == 0x24: # SELECT ITEM uicc.gen_res.add_info = add_info_select
Command: Terminal response >> 8014 0000 15 810301260382028281830100260762016211916221 << 9114 [To be classified ] duration: 0.1
Получив запрашиваемые данные, наш апплет формирует команду DISPLAY TEXT:
Command: Fetch >> 8012 0000 14 << D0128103012181820281028D0704323137393734 << 9000 [OK ] duration: 0.0 command number: 01 command type : DISPLAY TEXT qualifier : 81 (see ETSI 102 223, c. 8.6) from : UICC to : Display DCS : 04 Message : 217974
В последней строке как раз получаем шесть цифр одноразового пароля.
Полный лог скрипта
(.venv) PS C:\Users\tislamov\develop\FunGP\tests\UICC_HOTP> .\03_display_hotp.py Connecting to 'ACS ACR39U ICC Reader 0' reader Command: select MF >> 00A4 000C 02 3F00 << 9000 [OK ] duration: 0.0 Command: select DF GSM >> 00A4 000C 02 7F20 << 9000 [OK ] duration: 0.01 Command: select EF IMSI >> 00A4 000C 02 6F07 << 9000 [OK ] duration: 0.0 Command: read IMSI >> 00B0 0000 00 << 6c09 [Response bytes still available (CAT) (9 bytes remaining)] Repeat command with Le set to 09 >> 00B0 0000 09 << 089999070000818304 << 9000 [OK ] duration: 0.0 IMSI: 809999700000183840 Command: Terminal Profile >> 8010 0000 0A FFFFFFFFFFFFFFFFFFFF << 912a [To be classified ] duration: 0.01 Command: Fetch >> 8012 0000 2A << D028810301250082028182850C53544B2053657276696365738F0C8081080853494D20484F5450180124 << 9000 [OK ] duration: 0.01 command number: 01 command type : SET UP MENU qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : ME Alpha ID : TK Services item 80: SIM HOTP (plain hex: 8081080853494D20484F5450) Command: Terminal response >> 8014 0000 0C 810301250082028281830100 << 9000 [OK ] duration: 0.01 Command: Envelope [EVENT DOWNLOAD] SELECT HOTP option >> 80C2 0000 09 D30782020181900180 << 9142 [To be classified ] duration: 0.01 Command: Fetch >> 8012 0000 42 << D040810301240082028182050B81080853494D20484F54508F1601811208A1B3B5BDB5C0B8C0BEB2B0C2CC20484F54508F1002810C089DC3B6BDB020BFBEBCBEC9CC << 9000 [OK ] duration: 0.0 command number: 01 command type : SELECT ITEM qualifier : 00 (see ETSI 102 223, c. 8.6) from : UICC to : ME Alpha ID : SIM HOTP item 01: Сгенерировать HOTP (plain hex: 01811208A1B3B5BDB5C0B8C0BEB2B0C2CC20484F5450) item 02: Нужна помощь (plain hex: 02810C089DC3B6BDB020BFBEBCBEC9CC) Command: Terminal response >> 8014 0000 0F 810301240082028281830100100101 << 910b [To be classified ] duration: 0.01 Command: Fetch >> 8012 0000 0B << D009810301260382028182 << 9000 [OK ] duration: 0.0 command number: 01 command type : PROVIDE LOCAL INFORMATION qualifier : 03 (see ETSI 102 223, c. 8.6) from : UICC to : ME Command: Terminal response >> 8014 0000 15 810301260382028281830100260762016211916221 << 9114 [To be classified ] duration: 0.1 Command: Fetch >> 8012 0000 14 << D0128103012181820281028D0704323137393734 << 9000 [OK ] duration: 0.0 command number: 01 command type : DISPLAY TEXT qualifier : 81 (see ETSI 102 223, c. 8.6) from : UICC to : Display DCS : 04 Message : 217974 Command: Terminal response >> 8014 0000 0C 810301218182028281830100 << 9000 [OK ] duration: 0.0
На этом все, дорогой читатель, надеюсь, этот материал кому-нибудь да подсобит в преодолении кривой обучаемости столько витиеватого направления, именуемого "Java Card".
Всем пока!

