
1. Введение
В данном цикле статей пойдет речь об использовании Java смарт-карт (более дешевых аналогов электронных ключей) для защиты программного обеспечения. Цикл разбит на несколько глав.
Для прочтения и осознания информации из статей вам понадобятся следующие навыки:
- Основы разработки ПО для Windows (достаточно умения программировать в любой визуальной среде, такой как Delphi или Visual Basic)
- Базовые знания из области криптографии (что такое шифр, симметричный, ассиметричный алгоритм, вектор инициализации, CBC и т.д. Рекомендую к обязательному прочтению Прикладную Криптографию Брюса Шнайера).
- Базовые навыки программирования на любом языке, хотя бы отдаленно напоминающем Java по синтаксису (Java, C++, C#, PHP и т.д.)
Цель цикла — познакомить читателя с Ява-картами (литературы на русском языке по их использованию крайне мало). Цикл не претендует на статус «Руководства по разработке защиты ПО на основе Ява-карт» или на звание «Справочника по Ява-картам».
Состав цикла:
- Глава 1. Ява-карта. Общие-сведения.
- Глава 2. Ява карта с точки зрения разработчика апплетов
- Глава 3. Установка и настройка IDE
- Глава 4. Пишем первый апплет
- Глава 5. Безопасность
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. Благодарность терпеливым читателям
Спасибо всем, кто дочитал до этого места. Благодарности и негодования принимаются прямо в карму 😉
Буду рад любым вопросам в комментариях и постараюсь обновлять статью так, чтобы она включала ответы.
ссылка на оригинал статьи http://habrahabr.ru/post/255535/
Добавить комментарий