Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Хешируем строки на этапе компиляции с поддержкой annotation

Anna | 4.06.2014 | нет комментариев
Недавно я начал разрабатывать приложение под Android и передо мною появилась задача защитить его от реверса. Беглый просмотр гугла дозволил предположить, что ProGuard, входящий в состав Android Studio, с задачей совладает. Итог меня подлинно устроил за исключением одной крошечной детали — строки.
Программа обменивается с сервисом информацией с поддержкой Intent. Ключевой частью которых является строка действия. И если для взаимодействия с системой либо другими приложениями строка должна иметь определённый формат, то для обменов внутри приложения довольно её уникальности. Для комфорта рекомендуется составлять данную строку из имени пакета и наименования действия. Скажем:

public final class HandlerConst {
    public static final String ACTION_LOGIN = "com.example.app.ACTION_LOGIN";
}

Это комфортно для отладки, но крепко снижает качество обсфускации кода. Хочется, чтоб в релизе программы взамен данной строки оказался, скажем, её MD5 хеш.

public final class HandlerConst {
    public static final String ACTION_LOGIN = "7f315954193d1fd99b017081ef8acdc3";
}

Под катом рассказано, как добиться такого поведения с поддержкой подручного велосипеда.

Немножко лирики

Я был крепко удивлён, узнав, что ProGuard не работает со строками. Из документации на официальном сайте удалось узнать, что со строками может трудиться продвинутая платная версия. Вот только она шифрует строки с целью их расшифровки в изначальный вариант во время работы программы. Решения, разрешающего превратить строку в её MD5 значение, мне обнаружить не удалось.
Попытки обнаружить решение этой задачи вывели меня на статью, демонстрирующую чудеса оптимизирующих компиляторов C : Вычисление CRC32 строк в compile-time. Но в Java подобный способ не взмыл. ProGuard довольно крепко свернул способы, но споткнулся на приобретении массива байт из строки.
Позже этого я решил не тратить силы на попытки автоматизации и легко решить задачу руками:

public final class HandlerConst {
    public static final String ACTION_LOGIN;
    static {
        if (BuildConfig.DEBUG) ACTION_LOGIN = "com.example.app.ACTION_LOGIN";
        else ACTION_LOGIN = "7f315954193d1fd99b017081ef8acdc3";
    }
}

Но когда я увидел на прогре статью Custom Annotation Preprocessor — создание на базе Android-приложения и конфигурация в IntelliJ IDEA, я осознал, что это решение моей задачи.

Реализация аннотации

Постижение аннотаций по обычии началось с отсутствия требуемой информации на русском языке. Множество статей рассматривает использование Runtime аннотаций. Однако, подходящая статья нашлась на прогре:Подсчёт времени выполнения способа через аннотацию.
Для создания аннотации времени компиляции нам нужно:

  1. Описать аннотацию;
  2. Реализовать преемника класса AbstractProcessor, тот, что будет обрабатывать нашу аннотацию;
  3. Осведомить компилятору где искать наш процессор.

Изложение аннотации может выглядеть так:

package com.example.annotation;

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Hashed {
    String method() default "MD5";
}

Target — определяет объекты, для которых применима аннотация. В данном случае аннотацию дозволено применить к объявлениям переменных в классе. К сожалению, к любым, но об этом позднее.
Retention — время жизни аннотации. Мы указываем, что она существует только в начальном коде.
В самой аннотации мы заводим поле, определяющее способ хеширования. По умолчанию MD5.
Этого довольно чтоб применять в коде аннотацию, но от неё не будет никакого толку, пока мы не напишем обработчик аннотации.

Обработчик аннотации наследуется от javax.annotation.processing.AbstractProcessor. Наименьший класс обработчика выглядит так:

package com.example.annotation;
@SupportedAnnotationTypes(value = {"com.example.annotation.Hashed"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class HashedAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }
}

SupportedAnnotationTypes — определяет имена классов аннотаций, которые будут обрабатываться нашим процессором.
SupportedSourceVersion — поддерживаемая версия исходников. Толк в том, чтоб процессор не сломал при обработке аннотаций языковые конструкции, которые возникли в больше новых версиях языка.
Взамен данных аннотаций дозволено переопределить способы getSupportedAnnotationTypes иgetSupportedSourceVersion.
Способ process получает список необработанных поддерживаемых аннотаций и объект взаимодействия с компилятором. Если способ возвращает false — компилятор передаёт аннотацию на обработку дальнейшему процессору, тот, что поддерживает данный тип аннотаций. Если же способ вернул истину — аннотация считается обработанной и огромнее никуда не попадёт. Это необходимо рассматривать, чтоб нечаянно не прибить чужие аннотации.
Если в процессе работы всякого процессора изменились либо добавились начальные коды — компилятор пойдёт на дальнейший проход.

Для метаморфозы начального кода нам будет неудовлетворительно RoundEnvironment следственно мы переопределяем способ init и получаем из него JavacProcessingEnvironment. Данный класс разрешает получить доступ к начальным кодам, системе выброса предупреждений и ошибок компиляции и многое другое. Там же получим TreeMaker — вспомогательный инструмент для метаморфозы начальных кодов.

    private JavacProcessingEnvironment javacProcessingEnv;
    private TreeMaker maker;

    @Override
    public void init(ProcessingEnvironment procEnv) {
        super.init(procEnv);
        this.javacProcessingEnv = (JavacProcessingEnvironment) procEnv;
        this.maker = TreeMaker.instance(javacProcessingEnv.getContext());
    }

Сейчас нам остаётся перебрать наши аннотированные поля и заменить значения строковых констант. Код привожу в сокращении. Ссылка на GitHub в конце статьи.

 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if ( annotations == null || annotations.isEmpty()) {
            return false;
        }
        for (TypeElement annotation : annotations)
        {
            // Выбираем все элементы, у которых стоит наша аннотация
            final Set<? extends Element> fields = roundEnv.getElementsAnnotatedWith(annotation);
            JavacElements utils = javacProcessingEnv.getElementUtils();
            for (final Element field : fields) {
                //Получаем аннотацию, потом возьмём из неё способ хеширования.
                Hashed hashed = field.getAnnotation(Hashed.class);
                //преобразовываем аннотированный элемент в дерево
                JCTree blockNode = utils.getTree(field);
                if (blockNode instanceof JCTree.JCVariableDecl) {
                    //Помним, что поле может оказаться не только строковым.
                    JCTree.JCVariableDecl var = (JCTree.JCVariableDecl) blockNode;    
                    //получаем инициализатор (то что позже знака = )
                    JCTree.JCExpression initializer = var.getInitializer();
                    //Проверка отсечёт поля с инициализацией в конструкторе, а так же конструкции вида:
                    // ""   1
                    // new String("new string")
                    if ((initializer != null) && (initializer  instanceof JCTree.JCLiteral)){
                        JCTree.JCLiteral lit = (JCTree.JCLiteral) initializer;
                        //получаем строку
                        String value = lit.getValue().toString();
                        try {
                            MessageDigest md = MessageDigest.getInstance(hashed.method());
                            //Для однообразия на различных платформах задаём локаль.
                            md.update(value.getBytes("UTF-8"));
                            byte[] hash = md.digest();
                            StringBuilder str = new StringBuilder(hash.length * 2);
                            for (byte val : hash) {
                                str.append(String.format("%02X", val & 0xFF));
                            }
                            value = str.toString();
                            lit = maker.Literal(value);
                            var.init = lit;
                        } catch (NoSuchAlgorithmException e) {
                            //ошибка компиляции: неверный алгорифм хеширования
                        } catch (UnsupportedEncodingException e) {
                            //ошибка компиляции: такое вообще допустимо??
                        }
                    }else{
                        //Ошибка компиляции: неверное использование аннотации.
                    }
                }
            }
        }
    }

В способе мы бежим по списку аннотаций (мы чай помним, что в всеобщем случае процессор обрабатывает огромнее чем одну аннотацию?), для всякой аннотации выбираем список элементов. Позже этого начинается магия. Мы используем инструменты из поставки com.sun.tools.javac чтоб преобразовать элементы в дерево начального кода, у которого большое число вероятностей и по обычии полное неимение русскоязычной докуava”>@SupportedOptions({“Hashed”}) public class HashedAnnotationProcessor extends AbstractProcessor { private boolean enable = true; @Override public void init(ProcessingEnvironment procEnv) { //Добавленный код java.util.Map<java.lang.String,java.lang.String> opt = javacProcessingEnv.getOptions(); if (opt.containsKey(ENABLE_OPTIONS_NAME) && opt.get(ENABLE_OPTIONS_NAME).equals(“disable”)){ enable = false; } } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!enable){ javacProcessingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, “Annotation Hashed is disable”); return false; } //… } }
Мы объявили, что ждем опцию «Hashed» и если она «disable», то ничего не делаем и выводим информацию пользователю. Сообщения типа Diagnostic.Kind.NOTE являются информационными и при настройках по умолчанию многие среды разработки эти сообщения вообще не покажут.
При этом мы информируем компилятору, что обрабатывать аннотацию не стали. Если в системе есть ещё процессоры, которые обрабатывают аннотации такого типа, либо вообще не разбирают тип — они могут получить нашу аннотацию. Правда, я абсолютно ничего не могу сказать о том, в каком порядке компилятор будет пытаться распорядиться аннотацией. Пока у нас только наша библиотека и ровно одна аннотация — это не актуально, но при применении нескольких библиотек аннотаций будьте готовы к всплытию подводных камней.
Осталось передать эту опцию компилятору. Опции для процессоров передаются компилятору ключом “-A”. В нашем случае “-AHashed=disable”.
Остаётся только застваить Gradle передать эту опцию в необходимый момент. И вновь костыли:

tasks.withType(JavaCompile) {
      if (name == "compileDebug"){
          options.compilerArgs << "-AHashed=disable"
      }
}

Это для нынешней версии Android Studio. Для больше ранних tasks.withType(Compile).
Костыль, потому что данный блок вызывается для всякого типа сборки самостоятельно от задачи. По идее должно быть что-то схожее buildTypes из блока android, но у меня теснее не было никаких сил искать прекрасное решение. Все чай теснее додумались, что документации на русском обычно нет?
В коде аннотации могут выглядеть так:

    @Hashed 
    public static final String demo1 = "habr";
    @Hashed (method="SHA-1")
    public static final String demo2 = "habrahabr";
    @Hashed(method="SHA-256")
    public static final String demo3 = "habracadabra";

Способ может быть любым из поддерживаемых MessageDigest.

Вывод

Задача решена. Безусловно же, только для одного дюже определенного метода объявления констант, безусловно, не самым результативным методом, а у многих и сама постановка задачи вызовет огромнее вопросов, чем материал статьи. А я легко верю, что кто-нибудь потратит поменьше времени и нервов, если на его пути встретится схожая задача.
Но ещё огромнее я верю, что кто-нибудь заинтересуется данной темой и прогр увидит статьи, в которых будет рассказано, отчего каждая эта магия работает.
И, безусловно же, обещанный код: GitHub::DemoAnnotation

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB