The Jerusalem PostSheffi Paz, prominent Tel Aviv anti-illegal immigration activist, dies at 74PunchWorld leaders celebrate Lula’s re-election in BrazilEl ComercioEN VIVO | Lula gana las elecciones presidenciales en Brasil tras una remontada inédita ante Flávio BolsonaroDaily MaverickVenezuela opposition leader Machado returns home after fraught year awayBollywood HungamaAnupam Kher to play Sunil Munshi’s father in Swedish-Belgian film Out of Fashion, directed by Jens JonssonZDF heuteAktuelle Pressemitteilungen des ZDFSCMP ChinaWhy China’s new estimate on Greenland ice loss speed is 55% lower than America’sTagesschauMarktbericht: Gesunkene Kurse locken Schnäppchenjäger anSRF NewsEbola-Virus – An Ebola erkrankte Schweizerin wird in die Schweiz gebrachtMyJoyOnlineGhana is restoring nature, but is nature recovering?Wirtualna Polska"Nie będę z siebie robił anioła". Nawrocki o konflikcie z TuskiemESPNTransfer rumors, news: Man United eye Branthwaite amid defensive rebuild
The Daily Newsstand · Free, Always
Monday, October 26, 2026

UICC HOTP: генерируем одноразовые пароли на SIM-карте

Translate

Всем привет!
В прошлых статьях речь шла о возможностях смарт-карты, которые не получают должного освещения в публикациях, а именно: работа с 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. В целом схема действий такая:

Порядок создания экземпляра HMAC

Порядок создания экземпляра HMAC

Обратите внимание, что в конструкторе пропущен шаг №3.
Тут, конечно, у меня знатно подгорело. Дело в том, что изначально я не хотел никакого 'secret_key', а сразу же сохранить байты в ключе, однако после долгой отладки выяснилось, что на моих картах (как обстоят дела с другими - не знаю) механизм хранения ключа в постоянной памяти не работает. Может в общем механизм кривой, а может конкретно HMACKey косячит - не знаю, не ведаю, но чтобы воспроизвести проблему надо:

  1. На этапе инсталляции создать ключ HMACKey с типом памяти JCSystem.MEMORY_TYPE_PERSISTENT;

  2. Там же в конструкторе вызвать HMACKey.setKey( /* ключевой материал */ );

  3. После инсталляции (в рабочем режиме) вызов 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() - выполняет две важные задачи:

  1. создает запись "SIM HOTP" в STK-меню телефона и возвращает ее идентификатор на сохранность;

  2. подписывает апплет на событие "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:

Более подробную инфу найдете в ETSI 102 225 и 102 226

Более подробную инфу найдете в ETSI 102 225 и 102 226

Экземпляр 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".

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.