Предыстория
Жило-было в Google Play Android приложение с несколькими тысячами пользователей. Через год понадобилось его обновить. Ок, запускаем Idea, выбираем «Build» — «Generate Signed APK». Вспоминаю что за это время успел пересесть в Linux, ничего страшного, выбираю файл с ключами, ввожу ранее заботливо записанный пароль… Не подходит. Хмм… Ввожу еще раз, еще… Перебор вариантов, переспрос коллег… Всё плохо.
В итоге потенциально три приложения зависли в Google play, ни один из вариантов не подходит. Вспоминаю, что Windows остался на dual-boot, перезагружаюсь туда, к счастью в этом экземпляре Idea остался сохраненный пароль.
Решение
Приложение успешно обновлено, но проблема с паролем осталась. Запрос в Jetbrains к сожалению не помог, поддержка ответила быстро, но ответ был в том духе что пароль восстановить не является возможным, дали ссылку на исходники и предложили сделать свой хак. Что в общем то логично.
Ну что же, надо думать. Так как Idea это обычное java-приложение, то возникла мысль подключить свой код к тому месту, где из хранилища считываются пароли. После прочтения топика про javaagent быстро набросал свой java agent который просто записывал в файл имена всех загружаемых классов. Все что нужно чтобы Idea запускалась с java agent, это прописать в файл idea.exe.vmoptions (или idea64.exe.vmoptions) строку вида
-javaagent:C:projectsagentoutartifactsagent_jaragent.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. А пароль оказался тем же самым, что и был записан, но вводился в русской раскладке. Всё было просто на самом деле…
Автор: bald2b