Реалізація захисту 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 дні.







