Использование Java смарт-карт для защиты ПО. Глава 4. Пишем первый апплет

    image

    1. Введение


    В данном цикле статей пойдет речь об использовании Java смарт-карт (более дешевых аналогов электронных ключей) для защиты программного обеспечения. Цикл разбит на несколько глав.

    Для прочтения и осознания информации из статей вам понадобятся следующие навыки:
    • Основы разработки ПО для Windows (достаточно умения программировать в любой визуальной среде, такой как Delphi или Visual Basic)
    • Базовые знания из области криптографии (что такое шифр, симметричный, ассиметричный алгоритм, вектор инициализации, CBC и т.д. Рекомендую к обязательному прочтению Прикладную Криптографию Брюса Шнайера).
    • Базовые навыки программирования на любом языке, хотя бы отдаленно напоминающем Java по синтаксису (Java, C++, C#, PHP и т.д.)

    Цель цикла — познакомить читателя с Ява-картами (литературы на русском языке по их использованию крайне мало). Цикл не претендует на статус «Руководства по разработке защиты ПО на основе Ява-карт» или на звание «Справочника по Ява-картам».

    Состав цикла:




    1. Пример апплета


    На самом деле писать мы ничего не будем. Я просто приведу здесь жутко прокомментированный исходник тестового апплета для карты. А потом расскажу о некоторых тонкостях реализации.

    package test;
    
    //Импортируем необходимые библиотеки
    import javacard.framework.APDU;
    import javacard.framework.Applet;
    import javacard.framework.ISO7816;
    import javacard.framework.ISOException;
    import javacard.framework.Util;
    import javacard.security.*;
    import javacardx.crypto.*;
    
    //Определяем апплет
    public class Test extends Applet {
    
        // Класс команд (CLA) для нашего апплета
        private static final byte CLA_TEST = (byte) 0x80;
    
        // Номер команды для тестирования скорости
        private static final byte INS_TESTSPEED = (byte) 0x20;
    
        // Номер команды для тестирования шифрования
        private static final byte INS_TEST3DES = (byte) 0x30;
    
        // Выделяем себе буфер для операций шифрования
        private static byte[] enсryptBuffer = new byte[120];
    
        // Создаем экземпляр класса, ведающего шифрованием. Говорим, что используем
        // DES в CBC режиме
        private static Cipher cipher = Cipher.getInstance(Cipher.ALG_DES_CBC_NOPAD,
                false);
    
        // Создаем экземпляр класса, который будет хранить ключ шифрования 3DES3.
        private static DESKey key = (DESKey) KeyBuilder.buildKey(
                KeyBuilder.TYPE_DES, KeyBuilder.LENGTH_DES3_3KEY, false);
    
        // Выделяем память для ключа ширования 3DES3
        private static byte[] keyarr = new byte[24];
    
        // Конструктор апплета. Вызывается один раз при установке апплета в карту
        protected Test() {
            // Заполняем буфер для шифрования байтами 0xAA
            Util.arrayFillNonAtomic(enсryptBuffer, (short) 0,
                    (short) enсryptBuffer.length, (byte) 0xAA);
            // а ключ байтами 0xBB
            Util.arrayFillNonAtomic(keyarr, (short) 0, (short) keyarr.length,
                    (byte) 0xBB);
    
            // Устанавливаем использование данных из массива keyarr в качестве ключа
            // шифрования
            key.setKey(keyarr, (short) 0);
        }
    
        // Метод, выполняющий установку апплета.
        public static void install(byte bArray[], short bOffset, byte bLength)
                throws ISOException {
            new Test().register();
        }
    
        // Метод, который вызывается при отправке команды нашему апплету
        public void process(APDU apdu) throws ISOException {
            // Получаем ссылку на буфер с данными команды
            byte buffer[] = apdu.getBuffer();
    
            // Если переданная апплету команда - SELECT, возвращаем управление
            // CardManager
            if ((buffer[ISO7816.OFFSET_CLA] == 0x00)
                    && (buffer[ISO7816.OFFSET_INS] == (byte) (0xA4)))
                return;
    
            // Если CLA отличается от ожидаемого - возвращаем ошибку
            if (buffer[ISO7816.OFFSET_CLA] != CLA_TEST) {
                ISOException.throwIt(ISO7816.SW_CLA_NOT_SUPPORTED);
            }
    
            // Смотрим, что за команда нам отправлена и вызываем соответствующий
            // метод обработки
            switch (buffer[ISO7816.OFFSET_INS]) {
            case INS_TEST3DES:
                processTest3DES(apdu);
            case INS_TESTSPEED:
                processTestSpeed(apdu);
            default: // Для всех других значений INS возвращаем код ошибки
                ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
            }
        }
    
        // Метод, обрабатывающий команду тестирования скорости отправки данных
        private void processTestSpeed(APDU apdu) {
            // Получаем ссылку на буфер
            byte[] buffer = apdu.getBuffer();
            // Заполняем буфер произвольными байтами
            for (byte i = 0; i < 120; i++) {
                buffer[i] = i;
            }
            // Соощаем Java машине, что при выходе из метода process нужно передать
            // компу в качестве
            // ответа содержимое буфера начиная с байта с индексом 0 и длиной 120
            // байт.
            // Не забываем, что этот метод вызван именно методом process.
            // Немедленной отправки не происходит
            apdu.setOutgoingAndSend((short) 0, (short) 120);
        }
    
        private void processTest3DES(APDU apdu) {
            // Получаем ссылку на буфер
            byte[] buffer = apdu.getBuffer();
    
            // Шифруем содержимое enсryptBuffer, помещая результат в buffer
            cipher.doFinal(this.enсryptBuffer, (short) 0,
                    (short) this.enсryptBuffer.length, buffer, (short) 0);
    
            // Инструктируем об отправке данных
            apdu.setOutgoingAndSend((short) 0, (short) this.enсryptBuffer.length);
        }
    }
    


    2. Некоторые тонкости


    Конструктор класса апплета вызывается только один раз — при загрузке апплета в карту и его установке.

    Первое, о чем нужно узнать, при разработке вашего апплета — поддерживает ли реализация Java Card API на карте сборщик мусора (garbage collector). Если сборщик мусора не поддерживается, то любой объект, созданный с помощью оператора new, останется висеть в памяти карты навечно. Уничтожить его и освободить занимаемую память будет нельзя (кроме как удалив апплет с карты).

    Из этого следует один простой вывод: если ваша карта не поддерживает сбор мусора, все операторы new должны располагаться внутри конструктора класса апплета (внутри метода, с именем как у самого класса). Вне этого метода операторы new использовать будет нельзя. Если вы нарушите это правило, через какое-то время при выполнении команд апплет начнет «плеваться» ошибками и его придется перезаписать.

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

    Второй момент — фрагментация памяти карты при управлении апплетами. Апплет в памяти карты — что файл на жестком диске. Если вы удалите апплет, записанный перед каким-нибудь другим, в памяти карты останется «дырка» свободного пространства размером с удаленный апплет. Поэтому нужно придерживаться простых правил:

    • Если некоторые апплеты после записи в карту удалять не предполагается, записывайте их первыми.
    • Если какие-то апплеты предполагается обновлять и после них в карту записаны другие — перед обновлением придется удалить их все и перезаписать.

    То же самое касается динамического выделения памяти на картах, поддерживающих сборку мусора. Хоть сборщик и уничтожает неиспользуемые объекты, он может не дефрагментировать память, поскольку в случае с EEPROM карты это очень долгий процесс.

    Реализация языка Java для карт не поддерживает как минимум:
    • типы char, double, float, long, int, string;
    • квалификатор transient;
    • перечисления
    • многомерные массивы

    Причем, отсутствие поддержки типа int приводит к довольно смешному формату вызова методов: apdu.setOutgoingAndSend((short) 0, (short) 120) поскольку в Java числовые литералы — int по умолчанию.

    Ошибки в написании апплета могут проявляться на каждом из трех уровней: компиляция (*.java -> *.class), конвертация (*.class -> *.cap), линковка апплета в карте (*.cap -> карта). Будьте внимательны. Можно написать огромный апплет, скомпилировать и сконвертировать, а потом обнаружить, что в карту он не заливается по какой-то причине. Поскольку с отладкой апплетов все сложно в этом случае можно посоветовать комментировать части апплета и заливать его в карту, пока не наткнетесь на часть, которая вызывала ошибку.

    На некоторых картах присутствует верификатор (например, CodeShield на последних партиях Schlumberger/Gemalto egate). Этот верификатор дополнительно проверяет код апплета уже после загрузки его в карту на этапе установки (характерный признак его присутствия — долгая пауза перед получением ответа на последнюю команду загрузки — может достигать целой минуты), создавая дополнительный геморрой. Например, этот верификатор может требовать обязательного присутствия break в каждом case оператора switch, не давая сгруппировать два case в один.

    Все члены класса апплета хранятся в EEPROM карты. Количество циклов перезаписи этой памяти хоть и велико (несколько сот тысяч циклов), но все же ограничено. В картах есть некоторое количество оперативной памяти, которую можно выделить вызовом Util.makeTransientArray (как видно из названия, память выделяется в виде массива и такое выделение доступно и на картах без сборщика мусора. Память, выделенная makeTransientArray во всех случаях освобождается корректно (см. документацию)). Обычно около 1Кб. Разумеется, работа с этой памятью быстрее, чем с EEPROM.

    Чтобы представить себе возможности, которые доступные апплету, предлагаю просто пролистать документацию, входящую в состав Javacard SDK.

    3. Благодарность терпеливым читателям


    Спасибо всем, кто дочитал до этого места. Благодарности и негодования принимаются.

    Буду рад любым вопросам в комментариях и постараюсь обновлять статью так, чтобы она включала ответы.
    Поделиться публикацией

    Комментарии 14

      +1
      Мой товарищ-нехабраюзер, возмущённый качеством представленных здесь материалов по JavaCard, отрекомендовал читать книжки и не читать русскую википедию:

      А всем интересующимся вот книжка на русском:
      Java Card Technology for Smart Cards: Architecture and Programmer's Guide
      Автор: Жикун Чен
      Издательство: Техносфера
      Серия: Мир программирования
      ISBN 978-5-94836-143-7, 0-201-70329-7; 2008 г.


        +1
        Вы не могли бы попросить его прокомментировать его возмущение? Можно мне на почту, если у него нет доступа на Хабр.

        Книга, на которую вы ссылаетесь, замечательная. Однако, на Хабре книги размещать смысла не имеет. И отсылать читателей Хабра к книгам тоже (иначе вообще зачем статьи писать?).

        Я буду признателен за любую критику размещенных мной публикаций.
        +3
        Спасибо за статью. Вы пишите, что int не поддерживается, на самом деле это не совсем так. Данный тип является опциональным по спецификации, и в конкретных реализациях JCRE он может быть, и может не быть.

        По примеру:

                // Заполняем буфер произвольными байтами
                for (byte i = 0; i < 120; i++) {
                    buffer[i] = i;
                }
        
        


        Произвольные байты это не 1…120, как минимум рекомендуется сделать так:
        RandomData.getInstance(RandomData.ALG_SECURE_RANDOM).generateData(buffer, (short)0, (short) 120);
        

        будет надежнее и быстрее чем цикл.

        Второй момент — фрагментация памяти карты при управлении апплетами. Апплет в памяти карты — что файл на жестком диске. Если вы удалите апплет, записанный перед каким-нибудь другим, в памяти карты останется «дырка» свободного пространства размером с удаленный апплет.

        Здесь есть некоторая путаница,  следует различать код апплета и созданный экземпляр класса. Код апплета может храниться как в EEPROM, так и в ROM (маска). Экземпляр класса хранится в EEPROM.
        Соотвественно, можно создать несколько экземпляров апплета с разными AID, и при этом будет выдяляться память необходимая именно на хранение экземляра класса апплета.
        Поскольку с отладкой апплетов все сложно в этом случае можно посоветовать комментировать части апплета и заливать его в карту, пока не наткнетесь на часть, которая вызывала ошибку.

        Тут все как раз просто. Есть встроенный в NetBeans отладчик правда для JC 3.0.4, есть JCOP Tools, а если хотите под все платформы и бесплатно, и еще писать unit-тесты — вот наш проект jCardSim.
          +1
          Пардон, спросоня.
          «Произвольные байты это не 1…120», имел в виду, конечно, 0...119.
            +1
            Да, я понял. Спасибо за комментарий.

            По поводу int наверное, вы правы. Я не помню, чтобы в спецификации был какой-то запрет на поддержку этого типа. Однако в моей практике карты с его поддержкой не встречались. Кроме того, официальный конвертер из JC SDK 2.2.x не конвертит апплеты, в которых используется int. Говорит, неизвестный тип, если не ошибаюсь.

            Что касается апплетов — все верно. Однако апплеты, хранящиеся в ROM я вообще не рассматривал. Понятно, что их загружать в карту не нужно. Достаточно создать экземпляр.

            Про Netbeans я, по-моему, упоминал в статьях. Карт, поддерживающих 3.x версию SDK мне не попадалось. Поэтому и Netbeans я не использовал. Мне кажется, их и сейчас на рынке немного.
        +1
        … конвертер из JC SDK 2.2.x не конвертит апплеты, в которых используется int...

        -i опция конвертора (-i
        Instructs the Converter to support the 32-bit integer type.)

        Не совсем справедливо делать выводы о том как работает менеджер памяти и gc в общем случае, основываясь на поведении конкретных реализаций Java Card.

        Карт, поддерживающих 3.x версию SDK мне не попадалось.

        Карт, кстати, достаточно,  в том числе и российских www.esmart.ru/_products/_token-gost.  Опять таки, нужно разделять 3.0.x Classic  и Connected Edtion. Хоть Вы и говорите в начале о том, что речь пойдет о 2.2.1, но было бы здорово добавить, как дела обстоят сейчас.
          0
          Разумеется, добавлю. Спасибо.
          0
          Как данная технология защищена от кардшаринга (наподобие активно используемой в системах спутникового ТВ) в комплекте со средствами отладки ПО и MITM?
            0
            Ява-карта — это просто черный ящик для вашего секретного апплета, который что-то там такое секретное делает. Все остальное зависит от вас как от разработчика. Сама карта за вас ничего не сделает.

            • От отладки нужно защищаться средствами вроде Themida или StarForce (я об этом писал).
            • По поводу Man-in-the-middle — не понял, что за ситуацию вы имеете ввиду. Эмуляцию карты?


            От кардшаринга можно попробовать защититься так
            • При начале работы софт устанавливает с картой защищенную сессию. При завершении работы — завершает ее, производя некий handshake с картой. Если осуществляется попытка начать новую сессию без завершения предыдущей, некий счетчик внутри карты увеличивается. При достижении X таких попыток карта блокируется. Но это сильно зависит от вашей специфики. Возможно, от таких мер просто взвоют пользователи.
            • Софт, защищенный навесным протектором, может измерять скорость получения ответа от карты. Карта, расположенная локально, отвечать будет довольно быстро. Карта, расположенная в сети, будет отвечать заметно медленнее. В зависимости от качества соединения, расшаренная по сети карта скорее всего будет иметь непредсказуемое время ответа. Это можно замерять и блокировать карту при достижении определенных критериев.
            • Карта сама может хранить несколько последних меток времени, полученных через протокол общения с ПО и действовать самостоятельно с учетом этой информации


            Как вы понимаете, карта сама никак не может установить, расшарили ее или нет. В стандартных условиях канал связи с картой приложение обычно открывает эксклюзивно, поэтому без вторжения в ОС расшарить карту будет сложно. Но, конечно, можно пустить общение с картой через свой канал, сняв это ограничение.
            0
            Да, я имел ввиду общение между софтом и картой через свой канал. Защиту от отладки при очень большом желании гипотетически можно снять. Блокировать карту при достижении счетчика определенных значений — тоже не лучший вариант, учитывая закон о защите прав потребителей (сначала нужно доказать, что это не какой-либо сбой), тем более, как правило, защищающие права потребителей стоят на стороне потребителей, плюс этим конкуренты могут воспользоваться и заполнить интернет негативом. По поводу измерения скорости получения ответа от карты — хороший вариант, но никто не запрещает поставить ПО на сервер, завести туда 1000 учетных записей с софтом и продавать решение в «облаке». Вопрос в том, насколько популярен софт и какую потенциальную прибыль он может принести разработчику. Я бы предложил разработчикам, пользующихся данным решением, добавить в подозрительных случаях СМС верификацию (например при переполнении счетчика карты), так как приплетать дополнительно смс шлюз с сим-картой для взлома — это уже перебор, хотя… В целом годная схема, спасибо
              0
              Когда вы работаете с ява-картами, единственная вещь, которую сделать действительно невозможно — это извлечь ваш апплет из карты и проанализировать его. Поэтому, если в вашем ПО нет никакой логики, которую можно было бы поместить в апплет, защита не будет абсолютно надежной.

              Мои клиенты — в основном авторы ПО для разблокировки сотовых телефонов. Как правило, там много чего можно в карту поместить (алгоритмы расчета кодов, ключи авторизации и прочее). Если у вас, скажем, бухгалтерское ПО, вряд ли использование ява-карты даст вам преимущество по сравнению с использованием какого-нибудь HASP или Guardant ключа.
              0

              Если кто-либо захочет использовать этот пример (я бы не советовал), не забудте вызвать в конструкторе после инициализации ключа еще и инициализацию чипера:


              cipher.init(key, Cipher.MODE_ENCRYPT);

              Ведь если ключ создавался, значит он для чего-нибудь нужен =)

                0

                Этот апплет — "пример", как и указано в его заголовке. Поэтому, конечно, в прод его пускать не нужно.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое