Мобильные
Ключи и API-адреса внутри APK и IPA: почему «спрятанный» ключ не секрет
Любой ключ и адрес в мобильном приложении находится через jadx или strings. Что можно хранить в приложении, что только на сервере и где предел обфускации.
· 10 мин. чтения

Частая ситуация в мобильной разработке: API-ключ SMS-шлюза, платёжной системы или картографического сервиса записывают в код приложения или в файл .env с мыслью «пользователь всё равно не увидит». Или адреса серверов, включая тестовые и административные, лежат в коде обычными строками. А эксперт начинает анализ приложения именно с этого: распаковывает APK или IPA и просматривает все строки внутри.
В экспертизе нашего собственного приложения тоже была находка по этой теме: API-адреса не должны храниться в коде открытыми строками. В этой статье разберём, что можно извлечь из приложения, почему «спрятать» недостаточно и где строится правильная защита.
В чём проблема
Приложение, установленное на устройство пользователя, — это файл в руках пользователя. Его можно скопировать, распаковать, декомпилировать и наблюдать во время работы. Значит, любое значение, помещённое в приложение, человек с достаточным временем обязательно найдёт. Это правило относится и к Android, и к iOS, и к нативному коду, и к Flutter, и к React Native.
Внутри приложения обычно находят:
- Секретные ключи сторонних сервисов — отправка SMS, платежи, email, облачное хранилище (например, access key S3), ключи AI API. Это самое опасное: получив ключ, человек отправляет SMS за ваш счёт, читает файлы или вызывает платёжный API.
- Публичные идентификаторы — конфигурация Firebase, ключ Google Maps, analytics ID. Они не создавались как секреты, но без правильных ограничений ими злоупотребляют.
- Адреса серверов — основной API, staging, тест, админ-панель, внутренние сервисы.
- Статический логин/пароль или «service»-токен — одинаковое для всех пользователей значение для доступа к серверному API.
- Ключи шифрования — записанный в код ключ AES или IV. Такое шифрование на практике ничего не защищает.
Почему это важно для экспертизы
На первом месте в OWASP Mobile Top 10 (2024) стоит M1 — Improper Credential Usage, сюда входят захардкоженные ключи и учётные данные. Ключи шифрования в коде относятся к M10 (Insufficient Cryptography), открытые строки и незащищённый бинарник — к M7 (Insufficient Binary Protections). В MASVS это требования групп STORAGE, CRYPTO и RESILIENCE.
Эксперт декомпилирует APK и ищет строки автоматически и вручную. Найденный секретный ключ обычно оценивается как высокий риск, потому что последствия выходят за пределы приложения — затрагивают весь сервис.
Что можно увидеть внутри приложения: проверьте сами
Android: jadx и apktool
# Открыть код Java/Kotlin в читаемом виде
jadx -d out app-release.apk
# Ресурсы и манифест
apktool d app-release.apk -o res_out
# Типичные поиски
grep -rniE "api[_-]?key|secret|token|password|bearer" out/ res_out/
grep -rnoE "https?://[a-zA-Z0-9./_-]+" out/ res_out/ | sort -u
Отдельно посмотрите res_out/res/values/strings.xml — многие ключи кладут именно туда.
Flutter-приложения
Во Flutter код на Dart компилируется в lib/arm64-v8a/libapp.so. jadx его не покажет, но строки всё равно лежат в бинарнике:
unzip -o app-release.apk -d apk_out
strings -n 8 apk_out/lib/arm64-v8a/libapp.so | grep -iE "https?://|key|secret"
ls apk_out/assets/flutter_assets/
Две распространённые ловушки:
- Файл
.envсflutter_dotenvдобавляется в приложение как asset и лежит обычным текстом по путиassets/flutter_assets/.env. Чтобы его открыть, никакие инструменты не нужны. - Значения
--dart-defineпри компиляции становятся константами и записываются в бинарник. В исходниках их не видно, ноstringsих находит.
iOS: IPA
unzip -o App.ipa -d ipa_out
plutil -p ipa_out/Payload/App.app/Info.plist
strings ipa_out/Payload/App.app/App | grep -iE "https?://|key|secret"
# В Flutter-приложении для iOS код Dart находится здесь:
strings ipa_out/Payload/App.app/Frameworks/App.framework/App | grep -iE "https?://"
Просмотрите также Info.plist и файлы конфигурации .plist, .json внутри приложения.
Автоматический поиск
Декомпилированную папку можно проверить инструментами поиска секретов:
gitleaks detect --no-git --source out/ -v
trufflehog filesystem out/
Поставьте эти инструменты и в CI на исходный код — ключ не должен попасть в репозиторий раньше, чем в приложение. Об этом мы писали в статье управление секретами и ключами.
Почему «спрятать» — не решение
Закодировать ключ в base64, разбить на две части или «зашифровать» через XOR — это усложняет статический поиск, но не защищает ключ. Чтобы использовать ключ, приложение должно получить его в открытом виде в памяти, и в этот момент его можно увидеть, перехватив функцию через Frida или наблюдая сетевой трафик. С адресами серверов то же самое: приложение отправляет на них запросы, значит они видны в трафике.
Нужно и правильное представление об обфускации:
- R8 / ProGuard сокращает имена классов и методов, но не шифрует строковые литералы.
- Dart
--obfuscateзаменяет имена символов, а строки остаются как есть. - Коммерческие средства защиты могут шифровать строки, но и они расшифровываются во время работы.
Правило простое: если раскрытие значения может навредить сервису, его не должно быть в приложении. В приложении остаются только значения, которые можно раскрыть и которые ограничены на стороне сервера.
Тем не менее уменьшать количество открытых строк полезно, и экспертиза этого ожидает: это повышает стоимость статического анализа, выводит приложение из категории «лёгкой цели» для автоматических сканеров и предотвращает случайное раскрытие внутренних адресов. Но это вспомогательный слой защиты, а не основной.
Как закрыть
1. Перенесите секретные ключи на сервер
Приложение не должно напрямую вызывать сторонний сервис. Оно обращается к вашему backend с токеном пользователя, а backend вызывает сервис с ключом. Пример на Laravel:
// routes/api.php
Route::middleware(['auth:sanctum', 'throttle:5,1'])
->post('/otp/send', [OtpController::class, 'send']);
// app/Http/Controllers/OtpController.php
public function send(Request $request, SmsGateway $sms)
{
$user = $request->user();
$sms->send($user->phone, $this->makeCode($user)); // ключ в config('services.sms.key')
return response()->noContent();
}
Пример на Node (Express):
app.post('/api/geocode', requireAuth, rateLimit({ windowMs: 60_000, max: 30 }),
async (req, res) => {
const r = await fetch(`https://maps.example.com/geocode?q=${encodeURIComponent(req.body.q)}`,
{ headers: { Authorization: `Bearer ${process.env.MAPS_SECRET}` } });
res.json(await r.json());
});
Так ключ живёт только на сервере, каждый запрос привязан к пользователю, а число запросов ограничено.
2. Ограничьте публичные идентификаторы
Ключи, которые должны остаться в приложении (например, для SDK карт), ограничьте в панели провайдера: для Android — имя пакета и SHA-1 сертификата подписи, для iOS — bundle ID, список разрешённых API и дневной лимит. Конфигурация Firebase — не секрет: её безопасность держится на Security Rules и App Check, поэтому правила не должны оставаться в состоянии «читать могут все».
3. Откажитесь от статического «service»-токена
Одинаковый токен для всех копий приложения — это общий пароль. Вместо него каждый пользователь при входе получает свой короткоживущий токен (Sanctum, OAuth2 и т. п.). Запросы до входа (регистрация, OTP) ограничиваются rate limiting и защитой от ботов.
4. Проверяйте подлинность приложения на сервере
Play Integrity API на Android, App Attest (DeviceCheck) на iOS или Firebase App Check дают серверу сигнал «запрос пришёл от неизменённого приложения с настоящего устройства». Это не заменяет аутентификацию, но усложняет злоупотребление API через скрипты.
5. В release-сборке — только release-адреса
Адреса staging, тестовых серверов и админки вообще не должны попадать в release-сборку. Во Flutter используйте flavor или отдельную точку входа:
flutter build apk --release --flavor prod -t lib/main_prod.dart \
--obfuscate --split-debug-info=build/symbols
Если админ-панель не нужна в мобильном приложении, её адреса в нём быть не должно, а на сервере она закрывается IP allowlist.
6. Сократите оставшиеся строки
Чтобы не хранить значения вроде API-адреса обычным литералом, есть подходы, шифрующие их при сборке. Например, в Dart пакет envied в режиме obfuscate: true не хранит значения открытой строкой:
@Envied(path: '.env.prod', obfuscate: true)
abstract class Env {
@EnviedField(varName: 'API_BASE')
static final String apiBase = _Env.apiBase;
}
Это намного лучше, чем добавлять .env как asset, но правило выше остаётся в силе: сюда кладутся только значения, которые можно раскрыть. Дополнительно защиту бинарника дополняют R8 (minifyEnabled true) на Android, --obfuscate в Dart и RASP — об этом в статье целостность приложения и обфускация.
7. Замените раскрытый ключ
Если ключ уже был в какой-либо выпущенной версии, удалить его недостаточно — старые APK остаются в интернете. Отзовите ключ (rotate) в панели провайдера и положите новый только на сервер.
Короткий чек-лист
- Release APK/IPA проверен через jadx, apktool и
strings; секретных ключей не найдено. -
.envне добавлен в приложение как asset; секреты не передаются через--dart-define. - Сторонние сервисы вызываются через backend; ключи только на сервере.
- Оставшиеся в приложении ключи ограничены в панели провайдера по пакету/bundle ID и подписи.
- Firebase Security Rules не открыты; рассмотрены App Check или Play Integrity / App Attest.
- Нет общего статического токена для всех пользователей.
- Нет ключа шифрования в коде.
- В release-сборке нет адресов staging, тестовых серверов и админки.
- API-адреса не хранятся открытыми литералами; R8 и
--obfuscateвключены. - Ранее раскрытые ключи заменены.
- В CI работает gitleaks или аналог.
Вывод
Хранить секрет внутри мобильного приложения невозможно — можно только повысить стоимость его извлечения. Поэтому правильный подход двухслойный: настоящие секреты — на сервере, а в приложении только ограниченные значения, раскрытие которых не причиняет вреда; поверх этого обфускация и сокращение открытых строк закрывают требования экспертизы. Такой подход помогает не только пройти экспертизу, но и уменьшить последствия, если ключ всё-таки утечёт.
Если хотите увидеть, что лежит внутри вашего release-приложения, раньше эксперта, оставьте заявку на сканирование. Claude AI агент быстро просмотрит APK/IPA и исходный код, а наш специалист оценит каждый найденный ключ и адрес и поможет перенести их на сервер.


