Привет, Хабр!

Как вы уже наверняка помните, меня зовут Юрий Шабалин и я занимаюсь разработкой динамического анализатора мобильных приложений Стингрей. Сегодня мне хотелось бы затронуть тему интеграций с системами распространения (дистрибуции) мобильных приложений. В статье рассмотрим, что представляют собой такие системы, зачем они нужны и как могут использоваться для решения задач информационной безопасности. Материал будет полезен тем, кто хочет интегрировать проверки безопасности в свой процесс разработки, но не хочет менять процесс сборки. Расскажем также про инструмент, который может в этом помочь.

Поехали!

Оглавление

Введение

Как устроена разработка любого приложения? Специалист локально занимается решением какой-либо задачи, правит и тестирует (иногда) код и, когда он готов, отправляет его в систему хранения версий (VCS), чаще всего в Git-репозиторий. Того, как код попадает в VCS, автоматически запускается процесс сборки приложения, результат которого попадает в систему хранения версий (обычно это различные инструменты вроде Nexus, Artifactory и т.д.).

Такой процесс понятен, логичен и применяется при сборке разных приложений, вне зависимости от платформы (мобильные системы, Web, Desktop). Но дальше начинаются интересные различия. Они связаны, главным образом, с развертыванием приложения для тестирования. Когда мы имеем дело с Web, все достаточно просто: готовый артефакт автоматически разворачивается на нужных стендах, тестировщики могут открыть браузер и начать тестирование новой версии. По-другому все обстоит с мобильными приложениями, ведь стенд для тестирования представляет собой мобильное устройство и, чаще всего, не одно и даже не два. А проверять нужно на различных операционных системах, различных производителях и т.д. Для того, чтобы не переносить вручную на многочисленные устройства новые версии, были созданы системы дистрибуции мобильных приложений.

Системы дистрибуции

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

Все это работает так:

  • Новая версия после сборки попадает в систему дистрибуции.

  • Система рассылает уведомление на устройства, где установлено приложение дистрибуции.

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

  • Приложение обновляется и можно тестировать!

Схематично это выглядит так:

Схема процесса дистрибуции
Схема процесса дистрибуции

Таким образом, мы имеем аналог обычной раскатки новой версии приложения на стенды, роль которых в этом варианте выполняют мобильные устройства iOS и Android.

Конечно, только этим функционалом системы дистрибуции не ограничиваются, каждая из них имеет свои дополнительные «фишки», такие как подготовка и отправка приложений сразу в магазины или подключение различных проверок на безопасность и т.д. У каждой - свои достоинства и недостатки, мы не будем рассматривать их подробно в рамках данной статьи.

А вот о чем действительно хочется поговорить, так это о вариантах интеграции этих систем и способах загрузки из них собранных приложений. Зачем это может быть нужно? Как мы видим на практике, разработка мобильных приложений ведется вне внутреннего контура компании. Заниматься ей могут как отдельные команды внутри самой компании, так и вообще отдельные подрядчики, реализующие такие задачи “под ключ“. Причем во втором случае нередко разработчик отгружает новые релизные версии на проверку и публикацию в маркетплейсы, а у заказчика даже нет исходного кода. Передача версий может осуществляться различными способами, как через системы дистрибуции, так и просто в письме или через файловые обменники.

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

Тут начинается самое интересное, потому что каждая из этих систем имеет свои особенности. Некоторые из них дают скачивать приложения и имеют полноценный API для этого, другие же тщательно закрыты и стремятся максимально погрузить пользователя в свои сервисы. О некоторых из этих интеграций и о том, через что нам пришлось пройти, чтобы добавить загрузку приложений из этих источников, мы с @npina3v расскажем ниже.

AppCenter

Это, пожалуй, самая удобная из всех систем, и интеграция с ней не представляет сложности. Здесь есть описанный полноценный API, поиск по версии, скачивание последней доступной версии, нормальный логин по токену. AppCenter - первая интеграция, которую мы добавили, и самая удобная. Все, что нужно было сделать, это изучить документацию, составить несколько методов для получения информации о версии приложения по id:

https://api.appcenter.ms/v0.1/apps/{owner_name}/{application_identifier}/releases/{id}

или по версии:

https://api.appcenter.ms/v0.1/apps/{owner_name}/{application_identifier}/releases?scope=tester

И потом пройтись по версиям и выбрать нужную. Или можно просто получить последнюю, указав в качестве id слово latest.

Далее, получить информацию об URL, с которого необходимо скачивать, просто взяв его из информации о версии из параметра download_url.

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

Firebase

После покупки Гуглом в 2014 Firebase стал еще более популярным. Сейчас это один из самых используемых продуктов для разработки мобильных приложений. Функционал Firebase огромен, есть и аналитика, и различные инструменты для мониторинга, и удобные средства для тестирования и дистрибуции сборок для альфа и бета-тестеров.

Но единственный способ скачать новую сборку - использовать кнопку в Web-интерфейсе Firebase на странице Release and Monitor -> App Distribution. Может, это удобно вручную, но нам-то нужна автоматизация. Как? Смотрите.

Для начала необходимо получить доступ к самому Firebase, то есть пройти аутентификацию через Google SSO. К сожалению, нормального способа сделать это не нашлось, и мы стали экспериментировать и смотреть, какие Cookie влияют на доступ. По итогу, с пятью Cookie из всего списка можно полгода автоматизированно подключаться через Google SSO. Для того, чтобы получить эти токены, необходимо залогиниться в Firebase, открыть инструменты разработчика и сохранить значение 5 необходимых параметров:

  • SID

  • HSID

  • SSID

  • APISID

  • SAPISID

Cookie, получаемые в результате аутентификации в Firebase
Cookie, получаемые в результате аутентификации в Firebase

Однако, для успешного прохождения аутентификации необходим еще один параметр, который отправляется в заголовке Authorization, а именно некий SAPISIDHASH. Высчитать его несложно, достаточно воспользоваться функцией:

def calculate_sapisid_hash(self):
    """Calculates SAPISIDHASH based on cookies. Required in authorization to download app from firebase"""
    epoch = int(time())
    sha_str = ' '.join([str(int(epoch)), self.SAPISID, self.url])
    sha = sha1(sha_str.encode())
    return f'SAPISIDHASH {int(epoch)}_{sha.hexdigest()}'

Окей, доступ мы получили, но само приложение пока все равно недоступно - нужно осуществить запрос к внутренним API, которые как раз используют вычисленные нами куки для доступа. Изучив запросы на скачивание разных версий приложения, мы нашли закономерность: меняется только 4 параметра в запросе. И если вручную вытащить их, то можно автоматизировать дистрибуцию приложений из Firebase, обновляя только один параметр - код релиза, который меняется для каждой новой сборки.

Самый простой способ это сделать - зайти в Firebase на страницу Release & Monitor -> App Distribution, открыть инструменты разработчика и нажать на большую кнопку “Download“.

Единственная возможность скачать приложение из Firebase
Единственная возможность скачать приложение из Firebase

 После этого, на вкладке Network мы увидим запрос вида https://firebaseappdistribution-pa.clients6.google.com/v1/projects/{project_id}/apps/{app_id}/releases/{app_code}:getLatestBinary?alt=json&key={api_key}

Запрос на скачивание приложения
Запрос на скачивание приложения

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

Конечно, это далеко не самое удобное решение, ведь требуется целых 10 обязательных параметров для запуска, но, учитывая, что Firebase не имеет открытого API для скачивания и тестирования приложений, это пока единственный вариант. Мы написали им и спросили, не планируют ли они добавить API и получили отрицательный ответ.

Google Play

Казалось бы, раз уже реализован Google SSO, то и приложения из маркета скачивать можно без проблем. На самом деле, все гораздо сложнее и логика аутентификации и скачивания приложения из Google Play совсем другая.

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

Поэтому мы реализовали регистрацию фейкового устройства и автоматическое получение нужных параметров. То есть, при первом запуске с логином и паролем мы получаем нужные данные и скачиваем приложение. В ходе работы сначала будет произведен логин в Google Play, который создаст для нас фейковый девайс (сейчас это Pixel 2, Android 9, api 28) и получим необходимую информацию в виде gsfid зарегистрированного телефона и токен для аутентификации. Этой комбинации будет достаточно для повторного переиспользования без регистрации бесконечного количества новых девайсов.

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

Пример регистрации устройства
Пример регистрации устройства

Теперь для загрузки кроме gsfid и токена понадобится лишь один параметр - package name, удобно! Также, для загрузки приложения оно должно быть «куплено» или, проще говоря, подтверждена его загрузка в аккаунт. Это мы тоже добавили и при скачивании "покупка" она будет выполнена автоматически (работает только для бесплатных приложений).

Теперь можно скачивать официальные, не перепакованные сборки apk напрямую из Google Play, меняя лишь имя пакета в параметрах запуска. На данный момент это работает только для apk файлов. В последнее время все больше компаний выкладывают свои приложения в виде android app bundle (aab), а в этом случае возможна некорректная установка скачанного файла. В ближайшем будущем мы планируем реализовать возможность скачивания aab или split-apk.

Все, дело сделано. Как только мы отметили успешную интеграцию с Play Store, Google обновил пользовательское соглашение и изменил правила логина, сильно усложнив авторизацию по паролю приложения (app password). Раньше при ошибке авторизации можно было подтвердить доступ через браузер и ошибка исчезала, но с недавних пор такой трюк недоступен. Пришлось долго искать новое решение и смотреть, что поменялось в процессе аутентификации. Честно говоря, до сих пор не понимаю, какой магией это заработало, но для решения проблемы пришлось добавить ssl verify в запросы и использовать строго определенные версии библиотек. Спасибо за это добрым людям, которые поддерживают googleplay-api репозиторий.

В коде это выглядит примерно следующим образом:

class SSLContext(ssl.SSLContext):
    def set_alpn_protocols(self, protocols):
        """
        ALPN headers cause Google to return 403 Bad Authentication.
        """
        pass

class AuthHTTPAdapter(requests.adapters.HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        """
        Secure settings from ssl.create_default_context(), but without
        ssl.OP_NO_TICKET which causes Google to return 403 Bad
        Authentication.
        """
        context = SSLContext()
        context.set_ciphers(ssl_.DEFAULT_CIPHERS)
        context.verify_mode = ssl.CERT_REQUIRED
        context.options &= ~0x4000
        self.poolmanager = PoolManager(*args, ssl_context=context, **kwargs)

Apple App Store

AppStore имеет более закрытый API, чем Google Play, но функционал двух этих систем дистрибуции очень похож. Также происходит скачивание архива с файлами приложения, только в случае AppStore есть выбор, какой параметр задать, - либо bundle id, либо id приложения в магазине (его можно получить, банально скопировав id из адреса страницы).

Пример получения id приложения из AppStore
Пример получения id приложения из AppStore

Тут нам очень сильно упростила жизнь уже готовая реализация загрузки приложений ipatool и ее интерпретация на Python ipatool-py. Поэтому мы просто включили ее к себе, объединив тем самым несколько систем дистрибуции.

Однако есть некоторые нюансы для логина и последующего запуска приложения. Уже достаточно давно Apple запретила создавать учетки без двухфакторной аутентификации, и при первом логине обязательно будет необходимо устройство с работающим Apple-аккаунтом для получения кода двухфакторной аутентификации (или же можно получить код по смс/почте).

Пример получение 2FA при первом логине
Пример получение 2FA при первом логине

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

По аналогии с Play Market от Google, приложение также должно быть «куплено», а точнее подтверждено намерение скачать и установить его. На данный момент это происходит автоматически, то есть можно скачивать любые приложения, вне зависимости от того, были они установлены ранее или нет.

Ну и последний момент. Скачанные таким способом приложения будут запускаться в неизменном виде только на устройствах с тем же самым Apple Id, из-под которого они были загружены. Для того, чтобы снять это ограничение, необходимо получить расшифрованный ipa-файл из памяти на устройстве с Jailbreak и только потом переносить его и устанавливать на любом другом Apple Id (или даже без него).

Nexus Repository

Как ни странно, достаточно часто при разработке приложений в закрытом контуре в качестве системы для хранений версий используется корпоративный Nexus. Также это бывает удобно, если приложения приходят от подрядчиков и, чтобы не выкладывать их на различные ресурсы или файловые шары, можно достаточно легко настроить настроить проксирование или зеркалирование необходимых пакетов через Nexus. Схема простая: подрядчик на своей стороне заливает приложение в Nexus, а Nexus на стороне заказчика “перекладывает” его к себе. Никаких внешних файловых обменников, все строго, прозрачно и удобно. Но, так как Nexus из коробки не поддерживает загрузку android и ios дистрибутивов, приходится идти на небольшие хитрости и загружать их в mvn-репозиторий. Если у кого-то также возникнет необходимость в загрузке приложений в Nexus, то для удобства можно воспользоваться небольшим снипетом для Android:

deploy_to_nexus() {
    echo "deploy_to_nexus"
    packaging="apk"
    file_name="app-prod-debug.apk"
    groupId="com.appsec.mobile"
    artifactId="app-prod-debug"
    war_version="1.0"
    mvn deploy:deploy-file -DgeneratePom=true -DrepositoryId=maven-dev -Durl=https://nexus.dynamicmobilesecurity.com/repository/maven-releases/ \
        -Dpackaging=$packaging \
        -Dfile=$file_name \
        -DgroupId=$groupId \
        -DartifactId=$artifactId \
        -Dversion=$war_version \
        || exit 1
}
deploy_to_nexus

Или аналогичным для iOS:

deploy_to_nexus() {
    echo "deploy_to_nexus"
    packaging="ipa"
    file_name="RickAndMorty.ipa"
    groupId="example.RickAndMorty"
    artifactId="RickAndMorty"
    war_version="1.0"
    mvn deploy:deploy-file -DgeneratePom=true -DrepositoryId=maven-dev -Durl=https://nexus.dynamicmobilesecurity.com/repository/maven-releases/ \
        -Dpackaging=$packaging \
        -Dfile=$file_name \
        -DgroupId=$groupId \
        -DartifactId=$artifactId \
        -Dversion=$war_version \
        || exit 1
}

deploy_to_nexus

Загрузка из Nexus тоже не является чем-то особенным. Можно скачать по прямой ссылке или провести поиск и выбрать интересующую или последнюю версию. Спасибо полноценному API за то, что оба эти пути достаточно просто реализуемы. Мы пошли по второму, и при загрузке ищем в указанном репозитории либо последнюю, либо указанную версию.

Заключение

В результате наших трудов и стараний появился единый репозиторий для загрузки приложений из AppCenter, Firebase, Google Play, Apple AppStore и Nexus. Разрабатывали мы его для того, чтобы скачанные приложения можно было загружать в наш анализатор мобильных приложений. Но те, кому нужна просто загрузка файла, могут просто указать параметр запуска --download_onlyи тогда скрипт завершит свою работу сразу после загрузки приложения. Поставляется в виде исходного кода, Python-пакета и docker-образа, кому как удобнее использовать.

Надеюсь, что вам было интересно прочитать о сложностях в процессе интеграции с различными системами. После такой работы очень сильно начинаешь ценить продукты, у которых есть полноценный, задокументированный и понятный API. Работать с ними - одно удовольствие, и никакого ночного дебаггинга API какого-нибудь Firebase!

Мы продолжим развивать этот небольшой инструмент, добавлять новые интеграции и поддерживать существующие. В ближайших планах у нас реализация интеграций с Huawei AppGallery и рядом Российских магазинов приложений - NashStore, Ru Store и др. Очень надеюсь, что их API будет удобным и не принесет новых сюрпризов.

Спасибо за внимание и до скорых встреч!

Комментарии (0)