Pull to refresh

Как я потерял пароль от Android keystore, но потом смог восстановить с помощью Jetbrains Idea

Reading time 3 min
Views 32K
Предыстория

Жило-было в Google Play Android приложение с несколькими тысячами пользователей. Через год понадобилось его обновить. Ок, запускаем Idea, выбираем «Build» — «Generate Signed APK». Вспоминаю что за это время успел пересесть в Linux, ничего страшного, выбираю файл с ключами, ввожу ранее заботливо записанный пароль… Не подходит. Хмм… Ввожу еще раз, еще… Перебор вариантов, переспрос коллег… Всё плохо.

В итоге потенциально три приложения зависли в Google play, ни один из вариантов не подходит. Вспоминаю, что Windows остался на dual-boot, перезагружаюсь туда, к счастью в этом экземпляре Idea остался сохраненный пароль.

signed apk

Решение

Приложение успешно обновлено, но проблема с паролем осталась. Запрос в Jetbrains к сожалению не помог, поддержка ответила быстро, но ответ был в том духе что пароль восстановить не является возможным, дали ссылку на исходники и предложили сделать свой хак. Что в общем то логично.

Ну что же, надо думать. Так как Idea это обычное java-приложение, то возникла мысль подключить свой код к тому месту, где из хранилища считываются пароли. После прочтения топика про javaagent быстро набросал свой java agent который просто записывал в файл имена всех загружаемых классов. Все что нужно чтобы Idea запускалась с java agent, это прописать в файл idea.exe.vmoptions (или idea64.exe.vmoptions) строку вида
-javaagent:C:\projects\agent\out\artifacts\agent_jar\agent.jar 


После запуска с агентом текстовый файл быстро наполнился строками вида
com/intellij/openapi/ui/impl/DialogWrapperPeerImpl$MyDialog
com/intellij/openapi/ui/impl/DialogWrapperPeerImpl$MyDialog$1
com/intellij/openapi/ui/impl/DialogWrapperPeerImpl$MyDialog$DialogRootPane
com/intellij/openapi/ui/impl/DialogWrapperPeerImpl$MyDialog$MyWindowListener
com/intellij/openapi/ui/DialogWrapper$19
com/intellij/openapi/ui/DialogWrapper$ErrorPaintingType
com/intellij/ide/wizard/AbstractWizard$1


Затем жму на «Generate Signed APK» и смотрю на вывод в файле:
org/jetbrains/android/exportSignedPackage/KeystoreStep
org/jetbrains/android/compiler/artifact/ApkSigningSettingsForm
org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep


Кажется, все нужное нам лежит в exportSignedPackage

Небольшое гугление, и находим исходники 2012 г.

Здесь нас привлекает кусочек кода:
        String password = passwordSafe.getPassword(project, KeystoreStep.class, KEY_STORE_PASSWORD_KEY);
        if (password != null) {
          myKeyStorePasswordField.setText(password);
        }
        password = passwordSafe.getPassword(project, KeystoreStep.class, KEY_PASSWORD_KEY);
        if (password != null) {
          myKeyPasswordField.setText(password);
        }

Здесь видно, что пароли вытаскивается из защищенного хранилища и сохраняются в JPasswordField (стандартный контрол Swing для ввода паролей).

Осталось всего ничего — вытащить данные из текстовых полей. В этом нам поможет Javassist — библиотека для манипулирования байт-кодом «на лету». Пишем в нашем java-agent следующий кусочек кода:
    public byte[] transform(final ClassLoader loader, String className,
                            final Class classBeingRedefined, final ProtectionDomain protectionDomain,
                            final byte[] classfileBuffer) throws IllegalClassFormatException {
        if ("javax/swing/JPasswordField".equals(className)) {
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("javax.swing.JPasswordField");
                CtMethod m = cc.getDeclaredMethod("getPassword");
                m.insertAfter("{System.out.println(\"password is: \" + $_);}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return classfileBuffer;
    }

Что он делает? Перехватываем момент загрузки класса JPasswordField, находим в нем метод getPassword() и добавляем в конец метода наш фрагмент кода, который печатает в консоль искомый пароль ($_ это служебная переменная javassist, где лежит значение возвращаемое методом).

Таким нехитрым способом пароли были восстановлены и спасены.

P. S. А пароль оказался тем же самым, что и был записан, но вводился в русской раскладке. Всё было просто на самом деле…
Tags:
Hubs:
+49
Comments 29
Comments Comments 29

Articles