Закрывает блок 12 чеклиста, в банке вопросов — блок 11 (вопросы 143–150).
Что уже разобрано в других материалах и здесь не дублируется:
- экспортируемые компоненты,
android:exported, deep links и App Links как механика —10-android-sdk-deep.md, разделы 1 и 5.4; - разрешения и хранилища — там же, разделы 9 и 10;
- R8 как инструмент сборки и его правила —
13-di-build-deep.md, раздел 10.
Здесь — модель угроз и то, что из неё следует: где проходит граница доверия, как хранить секреты, что делать с сетью, и почему половина «защитных» мер на клиенте не защищает.
Вопрос 143 банка проверяет базовое понимание, без которого остальные ответы не имеют смысла. Формулировка, которую стоит выучить:
Приложение целиком находится в руках потенциального злоумышленника. Он может декомпилировать APK, подменить его, запустить на рутованном устройстве, подцепить отладчик, перехватить и изменить трафик. Значит, любая проверка на клиенте — это UX, а не безопасность. Единственная граница доверия — сервер.
Из этого следует конкретика, которую стоит привести примером, а не абстракцией:
- Проверка «пользователь премиум» на клиенте определяет, показать ли кнопку. Право выполнить платную операцию проверяет сервер, при каждом запросе.
- Валидация формы на клиенте существует, чтобы не гонять сеть зря. Сервер валидирует заново и никогда не доверяет присланному.
- Скидка, цена, лимит, роль — вычисляются на сервере. Клиент их только отображает.
Раз клиент не граница доверия, зачем пиннинг, обфускация и root-детект? Правильный ответ — повышение стоимости атаки и сокращение массовости:
- Массовая атака (скрипт, который работает у тысяч людей) становится дороже и требует квалификации. Это отсекает большинство.
- Целевая атака на конкретного мотивированного исследователя не отсекается ничем. Исходить надо из того, что она удастся.
Поэтому здоровая позиция: клиентские меры — это про экономику атаки и телеметрию, а не про гарантии. Кандидат, который обещает «защитить приложение от реверса», сигнализирует о непонимании; кандидат, который говорит «сделаю невыгодным и узнаю, что пытались», — о понимании.
TLS обязателен везде; открытый HTTP запрещён (с Android 9 это и так дефолт — cleartext выключен, и включать его исключениями стоит только для локальной разработки). Конфигурация задаётся через network security config, а не кодом: это декларативно, видно на ревью и не размазано.
Что стоит держать в конфиге:
- запрет cleartext для продовых доменов;
- отдельный
debug-overridesдля дебажных сборок — там можно доверять пользовательским сертификатам, чтобы работал прокси-снифер; в релизе этого быть не должно; - пины, если они используются.
Отдельно: по умолчанию приложения не доверяют пользовательским CA начиная с Android 7. Это и есть
причина, по которой для отладки трафика нужен debug-overrides — и хороший ответ на вопрос
«почему Charles не видит мой трафик».
Что пинить. Не сам сертификат, а SPKI-хеш публичного ключа (Subject Public Key Info). Причина: сертификат перевыпускается регулярно (у Let's Encrypt — каждые 90 дней), и пин по сертификату сломает приложение при первом же перевыпуске. Публичный ключ при перевыпуске может сохраняться.
Почему не leaf, а промежуточный. Пин на конечный сертификат означает, что любая ротация ключа ломает всех, кто не обновился. Пин на промежуточный сертификат CA переживает ротацию листа и при этом всё ещё сильно сужает множество допустимых сертификатов. Это стандартный компромисс между безопасностью и эксплуатационным риском.
Backup-пин обязателен. Всегда пинится минимум два ключа: текущий и запасной, который ещё не используется. Без запасного любая внеплановая смена ключа (в том числе компрометация) означает, что все установленные приложения перестают работать до релиза — а релиз через Play идёт днями.
Срок годности. В network security config у пинов есть expiration. После этой даты пиннинг
перестаёт применяться. Это выглядит контринтуитивно («защита сама отключается»), но это защита
от кирпича: лучше приложение без пиннинга, чем неработающее приложение у пользователей, которые
не обновлялись год.
Follow-up: «пиннинг обошли через Frida, что дальше». Ожидаемый ответ — не «усилим пиннинг». Правильно:
- Признать, что на скомпрометированном устройстве пиннинг не спасает by design: Frida работает внутри процесса и просто подменяет проверку.
- Перенести защиту туда, где она работает: подпись запросов, привязка сессии к устройству, аттестация через Play Integrity, серверные лимиты и аномалия-детекция.
- Использовать факт обхода как сигнал: аномальный трафик, невалидные вердикты целостности, всплеск операций с одного аккаунта.
- Не логировать тела запросов в релизе. Логирующий интерцептор OkHttp с уровнем
BODYв проде — это токены и персональные данные в logcat, доступные при отладке и в баг-репортах. Уровень логирования должен зависеть от типа сборки. - Не класть секреты в приложение. Ключ API, зашитый в код, извлекается из APK за минуты,
и
BuildConfig, NDK или обфускация этого не меняют — они только меняют время. Если ключ нужен для стороннего сервиса, правильная схема — проксировать через свой бэкенд.
Прежде чем выбирать API, надо назвать, от чего защищаемся, — иначе ответ будет про инструменты, а не про риск.
| Угроза | Насколько реальна | Что помогает |
|---|---|---|
| Другое приложение читает наши файлы | низкая: sandbox изолирует | ничего дополнительного не нужно |
| Физический доступ к разблокированному устройству | средняя | биометрия на вход, FLAG_SECURE |
| Физический доступ к выключенному устройству | низкая: FBE шифрует диск | шифрование ОС уже работает |
| Рутованное устройство / вредонос с root | высокая для целевых атак | ничего не помогает полностью |
| Утечка через бэкап | средняя | исключить секреты из бэкапа |
| Утечка через логи и крэш-репорты | высокая, самая частая | дисциплина логирования |
Главный вывод, который стоит произнести: на современном Android весь пользовательский раздел уже зашифрован (file-based encryption), и приложения изолированы. Поэтому дополнительное шифрование защищает в основном от сценария «root» — где оно, строго говоря, тоже пробивается. Это не значит «не шифровать»; это значит «не считать шифрование на устройстве главной мерой».
Факт, который в 2026 году отличает актуальные знания от устаревших: библиотека
androidx.security:security-crypto, из которой брали EncryptedSharedPreferences и
EncryptedFile, объявлена устаревшей целиком. Причём необычным образом: депрекация
приехала ещё в альфа-версиях, а затем вышел стабильный релиз, в котором все API уже помечены
устаревшими. То есть это не «скоро уберут» — это официальный отказ от подхода.
Официальные замены выглядят обескураживающе прямолинейно: вместо EncryptedSharedPreferences
предлагается обычный SharedPreferences, вместо EncryptedFile — обычный File, а вместо
MasterKey — самостоятельная работа с KeyGenerator и Android Keystore. Логика Google в том,
что диск и так зашифрован средствами ОС (см. модель угроз выше), а тем, кому нужно именно
прикладное шифрование, следует делать его явно через Keystore, а не через обёртку.
Практичный современный ответ на «где хранить токен»:
- DataStore плюс собственное шифрование через Keystore, если шифрование действительно нужно. Появился и первый официальный кирпич для этого — сериализатор DataStore, шифрующий содержимое (пока в альфа-версии), так что схема перестаёт быть полностью самодельной.
- Механизм платформы для токенов, переживающих переустановку и перенос устройства, если задача именно в этом: Google предоставляет отдельное хранилище для небольших секретов с сквозным шифрованием, специально предназначенное для токенов аутентификации.
- Ничего не хранить там, где это возможно, — см. 3.4.
Если на интервью вы упомянете EncryptedSharedPreferences как рекомендуемый подход, это
считывается как знание образца 2023 года. Достаточно добавить полфразы: «раньше брали
EncryptedSharedPreferences, но библиотека задепрекейчена, и сейчас это Keystore напрямую».
Единственный по-настоящему сильный механизм на устройстве. Ключевое свойство: приватный ключ не покидает защищённое хранилище и в идеале живёт в отдельном аппаратном модуле. Приложение работает с ключом по ссылке — просит подписать или расшифровать, но не может извлечь материал.
Что это даёт и чего не даёт:
- Даёт: ключ нельзя скопировать с устройства даже с root. Украсть зашифрованные данные можно, расшифровать их на другом устройстве — нет.
- Не даёт: на скомпрометированном устройстве атакующий может воспользоваться ключом, просто попросив Keystore о том же, о чём просит приложение.
Практические свойства, о которых стоит знать:
- Аппаратная привязка. Ключи могут храниться в TEE или в отдельном чипе (StrongBox). Наличие StrongBox проверяется, и при его отсутствии нужен фолбэк — иначе приложение упадёт на устройствах без него.
setUserAuthenticationRequired(true)делает ключ доступным только после аутентификации пользователя. Можно задать окно валидности в секундах либо потребовать аутентификацию на каждое использование (тогда ключ используется черезCryptoObjectвBiometricPrompt). Это то, что превращает биометрию из «проверки в UI» в реальную криптографическую гарантию: без успешной аутентификации данные не расшифровать в принципе.- Инвалидация при смене биометрии. Если задать соответствующий флаг, ключ автоматически становится непригодным при добавлении нового отпечатка. Это защита от сценария «злоумышленник добавил свой палец». Обратная сторона — легальная смена биометрии тоже ломает ключ, и это надо обработать (перевыпуск ключа и повторный вход), иначе получите поток жалоб.
- Key attestation — способ доказать серверу, что ключ действительно порождён в аппаратном хранилище, а не подсунут. Устройство выдаёт цепочку сертификатов, которую проверяет бэкенд (на клиенте это бессмысленно). Деталь, актуальная именно сейчас: корней доверия два — исторический и новый, — и проверяющая сторона обязана принимать оба, потому что старые устройства на новый корень никогда не перейдут. Отдельно существует механизм удалённой выдачи ключей, обязательный на устройствах, вышедших с последними версиями Android.
Самая сильная стратегия — не хранить:
- Пароль пользователя не хранится никогда, ни в каком виде.
- Долгоживущий refresh-токен — минимально необходимый срок, с ротацией при каждом использовании, и с возможностью отзыва на сервере.
- Access-токен короткоживущий, и его утечка ограничена по времени по построению.
- Данные карт не хранятся вообще (раздел 6).
Что нужно исключить из бэкапа: токены, ключи, всё, что привязано к устройству. Иначе секрет
уезжает в облако и приезжает на другое устройство — а autoBackup включён по умолчанию. Для
этого есть правила извлечения данных; их надо писать явно, а не полагаться на дефолт.
Механизм, который отвечает на вопрос «это действительно наше приложение на настоящем Android?». Приложение запрашивает токен, отправляет его на свой бэкенд, бэкенд обращается к Google за расшифровкой и получает вердикты по нескольким осям: соответствует ли приложение тому, что опубликовано в Play (не пересобрано и не подменено), похоже ли устройство на настоящее и прошедшее сертификацию, лицензирован ли аккаунт.
Три вещи, за которые ставят плюс:
- Проверка обязана быть на сервере. Если приложение само разбирает вердикт и решает, что делать, механизм бесполезен — эту проверку патчат первой. Токен идёт на бэкенд, решение принимает бэкенд.
requestHashпривязывает токен к конкретному действию. В запрос кладётся хеш содержимого операции (например, суммы перевода). Без этого токен можно переиспользовать для другой операции — классическая replay-атака.- Вердикт не булев. Это набор сигналов, и реакция должна быть градуированной, а не «пускать / не пускать» (раздел 4.3).
Ограничения, которые стоит назвать честно: есть квоты на запросы, поэтому дёргать на каждое действие нельзя; сервис требует наличия Play Services, поэтому на устройствах без Google (в том числе значимая часть рынка) он просто не работает, и нужен запасной путь.
Ключевой тезис: это телеметрия, а не гейт.
Почему не гейт:
- Надёжного детекта не существует. Любая проверка — это поиск известных признаков, а сокрытие root развивается быстрее детекта.
- Ложные срабатывания бьют по легальным пользователям: кастомные прошивки, разработчики, корпоративные устройства.
- Блокировка рутованных устройств отсекает лояльных технарей и не отсекает атакующего.
Как правильно: собирать сигналы (признаки root, эмулятора, отладчика, инструментирования, подозрительных модулей) и повышать требования пропорционально риску операции:
- обычный просмотр — ничего не делаем;
- вход в аккаунт — дополнительная проверка;
- изменение платёжных реквизитов — обязательная повторная аутентификация, задержка, уведомление на почту;
- крупный перевод — ограничение суммы или ручная проверка.
Такая формулировка показывает продуктовое мышление: безопасность соразмерна риску, а не одинакова везде.
От чего защищает: от быстрого чтения кода. Имена классов и методов теряются, мёртвый код удаляется, часть логики инлайнится. Это заметно повышает время, нужное чтобы разобраться — особенно в большом приложении.
От чего не защищает:
- Строки остаются строками. URL, ключи и сообщения видны в дампе ресурсов.
- Логика остаётся логикой. Тот, кто ищет конкретную проверку, найдёт её по строкам, по вызовам системных API или динамически, под отладчиком.
- Публичные точки входа не переименовываются: имена Activity, сервисов, всё, что упомянуто в манифесте и в рефлексии, остаётся.
- От модификации не защищает вообще: приложение пересобирается и подписывается заново.
Практическая связка, которую стоит добавить: обфускация ломает стектрейсы, поэтому обязателен
mapping.txt и его загрузка в систему крэш-репортинга. Команда, которая включила R8 и потеряла
читаемость крэшей, — типичная и болезненная история.
BiometricPrompt — единственный правильный способ: он показывает системный диалог, единообразно
работает с отпечатком, лицом и фолбэком на PIN/пароль устройства, и учитывает различия вендоров.
Самому рисовать экран биометрии нельзя.
Что отличает поверхностный ответ от глубокого:
- Классы стойкости. Биометрия делится на сильную и слабую; криптографические операции
(
CryptoObject) доступны только для сильной. Распознавание лица на многих устройствах относится к слабой — значит, «войти по лицу» может быть доступно, а «расшифровать ключ по лицу» — нет. - Биометрия без криптографии — это UI. Если результат проверки — просто
trueв колбэке, её обходят патчем. Реальная гарантия появляется, когда успешная аутентификация разблокирует ключ в Keystore, которым расшифровываются данные. - Фолбэк обязателен. Биометрия может быть не настроена, временно заблокирована после неудачных попыток или недоступна на устройстве. Нужен путь через код-пароль.
Схема, которая считается правильной по умолчанию: короткоживущий access-токен, долгоживущий refresh-токен с ротацией, отзыв на сервере, привязка сессии к устройству.
Отдельно про OAuth в мобильном приложении: авторизация должна идти через системный браузер
(Custom Tabs), а не через встроенный WebView. Причины: WebView контролируется приложением,
то есть оно может прочитать введённые логин и пароль; пользователь не видит адресную строку и
не может проверить домен; не работает существующая сессия браузера. Плюс PKCE обязателен,
потому что мобильное приложение не может хранить client secret.
Главный принцип: не прикасаться к данным карты. Всё остальное — следствие.
- Поля номера карты, срока и CVV отображает компонент платёжного провайдера (его SDK, его форма или его вебвью). Данные уходят напрямую провайдеру, минуя ваш код и ваш бэкенд. Приложение получает обратно токен и работает уже с ним.
- Причина не в удобстве, а в сужении PCI DSS-скоупа: как только данные карты проходят через ваш код, в область сертификации попадает приложение, бэкенд, логи, инфраструктура и процессы команды. Это на порядок дороже, чем интеграция SDK. Именно эту связку и проверяет вопрос.
- Что нельзя логировать: номер карты, CVV, полный трек, любые данные аутентификации. Никогда, ни на каком уровне, ни в аналитике, ни в крэш-репортах. CVV не хранится даже в зашифрованном виде — это прямо запрещено стандартом.
- На таких экранах ставится
FLAG_SECURE: он запрещает скриншоты и запись экрана и убирает содержимое из превью в списке недавних задач. Второе часто важнее первого. - Автозаполнение и клавиатурные подсказки на полях с чувствительными данными отключаются, чтобы значение не попало в системные словари и в сервисы автозаполнения.
Хороший финальный штрих в ответе: даже при полном использовании SDK провайдера остаётся ваша зона ответственности — целостность приложения (подмена SDK), защита от наложений поверх окна и корректная обработка результата платежа, включая идемпотентность (повтор не должен приводить ко второму списанию).
Короткий чеклист, который закрывает 🟡-пункт чеклиста и вопрос 150:
- Экспортируемые компоненты. Всё, что не предназначено для внешнего вызова, должно быть
exported="false". Экспортированный компонент — это публичный API вашего приложения, который может вызвать любое приложение с любыми параметрами. - Валидация deep links. Приходящие ссылки — недоверенный ввод. Закрываемый класс уязвимостей:
открытый редирект и подстановка чужого адреса (приложение получает ссылку с параметром
?next=https://evil.example, доверчиво по нему переходит и утекает токен либо показывает фишинговый экран внутри доверенного интерфейса). Лечение — allowlist доменов и схем, а не попытка отфильтровать плохое. - Проверка прав на входе, а не в UI. Компонент, который можно вызвать снаружи, обязан сам проверить, что вызывающий имеет право, — нельзя полагаться на то, что до него «дошли по правильному сценарию».
FLAG_SECUREна экранах с чувствительными данными.- Логи без персональных данных. Самая частая реальная утечка в мобильной разработке — не
взлом, а логи, крэш-репорты и аналитика, куда положили лишнее. Инструмент — не только
дисциплина, но и проверка:
StrictMode, ревью схемы аналитики, автоматический поиск подозрительных полей. - Экраны поверх приложения. Наложение чужого окна поверх вашего (tapjacking) — реальный вектор на чувствительных подтверждениях; для критичных действий стоит игнорировать касания, когда окно перекрыто.
Отличается от безопасности: здесь речь не о защите от злоумышленника, а об обязательствах перед пользователем и регулятором.
- Data Safety в Play. Декларация о том, какие данные собираются и куда уходят, обязательна, и она должна соответствовать действительности, включая сторонние SDK. Рекламный или аналитический SDK, который собирает больше заявленного, — это ответственность приложения.
- Минимизация. Не собирать то, что не нужно продукту. Это одновременно самый дешёвый способ снизить риск: данных, которых нет, не бывает в утечке.
- Локальное регулирование. GDPR в ЕС, 152-ФЗ в России (в том числе требование локализации персональных данных), отраслевые требования для финансов и здоровья. Знать детали наизусть не нужно, но нужно знать, что эти требования влияют на архитектуру (где физически лежат данные) и что решение принимается не разработчиком в одиночку.
- Идентификаторы. Рекламный идентификатор сбрасывается пользователем и может быть удалён; привязывать к нему что-либо долгоживущее нельзя. Постоянных аппаратных идентификаторов на современном Android для приложений нет — и попытки собрать «стабильный отпечаток устройства» из доступных сигналов нарушают политику Play.
- Почему клиент не является границей доверия и что из этого следует практически?
- Если клиентская защита ничего не гарантирует, зачем она нужна?
- Что настраивается через network security config и зачем нужен
debug-overrides? - Что именно пинят при certificate pinning и почему не листовой сертификат?
- Зачем backup-пин и почему у пиннинга есть срок годности?
- Пиннинг обошли через Frida. Что дальше?
- Модель угроз для данных на устройстве: от чего вы реально защищаетесь?
- Что случилось с
EncryptedSharedPreferencesи чем это заменяют сегодня? - Что даёт Keystore и чего он не даёт на скомпрометированном устройстве?
- Что делает
setUserAuthenticationRequiredи в чём подвох инвалидации по биометрии? - Что такое key attestation, кто её проверяет и почему корней доверия два?
- Play Integrity: где обязана происходить проверка и зачем
requestHash? - Почему root-детект — телеметрия, а не гейт? Как выглядит градуированная реакция?
- От чего защищает и от чего не защищает обфускация R8?
- Почему биометрия без криптографии — это UI, а не защита?
- Почему OAuth должен идти через системный браузер, а не через WebView?
- Как сузить PCI-скоуп при вводе карты и что нельзя логировать никогда?
- Какой класс уязвимостей закрывает allowlist для deep links?
- Где происходит самая частая реальная утечка данных в мобильной разработке?
- Что такое Data Safety и почему сторонние SDK — ваша ответственность?