Реализация защиты API-ключей в мобильном приложении
strings app.apk | grep -i "key\|secret\|token" — первые 10 строк вывода уже дают что-то интересное в большинстве приложений без защиты. Google Maps API Key в AndroidManifest.xml, Firebase API Key в google-services.json, Stripe Publishable Key в коде — всё это читается из распакованного APK без единой строчки реверс-инжиниринга.
Почему нельзя хранить ключи в коде
Любой ключ в строковых ресурсах, константах классов или конфигурационных файлах приложения — публичный ключ. APK и IPA декомпилируются. Даже обфускация только усложняет поиск, но не делает его невозможным.
Частый аргумент: «Firebase API Key публичный, его можно засветить». Технически верно для apiKey в Firebase конфиге — он нужен только для идентификации проекта, а доступ контролируется Firebase Rules. Но Google Maps API Key, Stripe Secret Key, ключи к собственному бэкенду — другая история. Утечка Maps Key означает чужие запросы за ваш счёт.
Правильное хранение секретов на устройстве
Если ключ всё же должен быть на устройстве (например, после аутентификации сервер выдаёт токен) — только Keychain (iOS) или Android Keystore.
На Android:
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
// создаём ключ шифрования привязанный к KeyStore
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGen.init(
KeyGenParameterSpec.Builder("my_key_alias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
// шифруем токен, сохраняем зашифрованный blob в EncryptedSharedPreferences
EncryptedSharedPreferences из androidx.security:security-crypto — удобная обёртка, которая делает это под капотом. Под неё не нужно писать руками KeyStore код.
На iOS Keychain Services через SecItemAdd / SecItemCopyMatching. В Swift удобно через KeychainAccess или SwiftKeychainWrapper. Атрибут kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly — данные доступны только когда устройство разблокировано, не мигрируют при резервном копировании iCloud.
Ключи, которых не должно быть на клиенте
API ключи к внешним сервисам (платёжные шлюзы, SMS-провайдеры, AI API) — на сервере. Точка. Клиент делает запрос к своему бэкенду, бэкенд делает запрос к Stripe/Twilio/OpenAI со своим ключом. Клиент никогда не узнаёт этот ключ.
Паттерн для ключей с ограниченным использованием: клиент аутентифицируется, сервер выдаёт короткоживущий токен (JWT или HMAC-подписанный nonce) с конкретными permissions. Клиент использует этот токен для прямых запросов к сервису (например, загрузки файла напрямую в S3 через presigned URL). Основной ключ — никогда не покидает сервер.
NDK и нативное хранение
Если строка должна быть в приложении и нельзя запрашивать её с сервера — нативный код. JNI функция возвращает ключ, собранный из нескольких частей:
JNIEXPORT jstring JNICALL
Java_com_example_NativeKeys_getApiKey(JNIEnv *env, jobject obj) {
// ключ разбит, части в разных местах
const char part1[] = {0x41, 0x42, 0x43, 0x00};
const char part2[] = {0x44, 0x45, 0x46, 0x00};
// собираем + XOR расшифровка
// ...
}
Это security through obscurity, не настоящая защита. Но нативный код сложнее хукать автоматическими инструментами, и порог атаки растёт.
Build-time защита
local.properties — файл вне репозитория, хранит переменные для сборки:
MAPS_API_KEY=AIzaSy...
В build.gradle:
manifestPlaceholders = [mapsApiKey: properties["MAPS_API_KEY"] ?: ""]
В AndroidManifest:
<meta-data android:name="com.google.android.geo.API_KEY" android:value="${mapsApiKey}"/>
Ключ не попадает в репозиторий, но попадает в APK — всё равно читается из манифеста. Для Maps Key это приемлемо с правильными ограничениями в Google Cloud Console (restrict by Android app package name + SHA-1). Для Secret Keys — нет.
Срок реализации полной схемы: аудит текущего положения ключей, миграция в Keystore/Keychain, вынос серверных ключей на бэкенд, настройка ограничений в Google Cloud/App Store Connect — 2–3 дня.







