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

Всем пока!