strings app.apk | grep -i "key\|secret\|token" — ברוב האפליקציות הלא מוגנות, פקודה זו חושפת מספר סודות. מפתח Google Maps API ב-AndroidManifest.xml, מפתח Firebase API ב-google-services.json, מפתח Stripe Publishable בקוד המקור — את כל אלה ניתן לחלץ מקובץ APK ללא הנדסה הפוכה. נתקלנו בפרויקטים שבהם מפתחות היו מקודדים בצורה קשיחה, גלויים לכל מי שהוריד את האפליקציה.
למה אי אפשר לאחסן מפתחות בקוד
כל מפתח המוטמע במשאבים או בקבועים הוא ציבורי. קבצי APK ו-IPA ניתנים לדקומפילציה; ערפול (obfuscation) רק מקשה על החילוץ אך אינו מונע אותו. לדוגמה, מפתח ב-local.properties עדיין מגיע לבנייה וניתן לקריאה מהמניפסט. ניתן להגביל מפתח Google Maps API לפי שם חבילה ו-SHA-1, אך עבור סודות אמיתיים (כמו מפתח שער תשלום) זו לא פתרון.
לעתים קרובות שומעים: "מפתח Firebase API הוא ציבורי, זה בסדר לחשוף אותו." טכנית זה נכון עבור apiKey — הוא מזהה את הפרויקט, והגישה נשלטת על ידי Firebase Rules. אבל מפתח Maps, מפתח Stripe Secret, ומפתחות backend הם שונים. מפתח Maps שדלף יכול לגרום לבקשות לא מורשות שיחויבו לחשבונך. אנו מבטיחים שאחרי העבודה שלנו, מפתחות לעולם לא יעזבו את המכשיר שלא לצורך, וסודות השרת נשארים על השרת.
איך לאחסן מפתחות בצורה מאובטחת על המכשיר
אם מפתח חייב להיות על המכשיר (לדוגמה, טוקן לאחר אימות), השתמש ב-Android Keystore או ב-Keychain Services. אחסון מקורי ב-Keystore אמין פי 100 מ-Shared Preferences.
אנדרואיד:
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
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 в EncryptedSharedPreferencesval keyStore = KeyStore.getInstance("AndroidKeyStore") keyStore.load(null) 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 הוא wrapper נוח שמבצע אוטומציה של תהליך זה.
iOS: Keychain Services דרך androidx.security:security-crypto/SecItemAdd. ב-Swift, השתמש ב-SecItemCopyMatching או ב-KeychainAccess. הגדר את התכונה SwiftKeychainWrapper כדי למנוע העברת נתונים עם גיבויי iCloud.
למה אחסון בצד השרת הוא האפשרות המאובטחת היחידה
מפתחות API לשירותים חיצוניים (שערי תשלום, ספקי SMS, APIs של AI) חייבים להיות מאוחסנים על השרת. הלקוח שולח בקשה ל-backend שלך, וה-backend שלך שולח בקשה ל-Stripe/Twilio/OpenAI באמצעות המפתח שלו. הלקוח לעולם לא מקבל את המפתח הזה. זה בטוח פי עשרות עבור נתונים רגישים.
תבנית למפתחות עם גישה מוגבלת: הלקוח מאמת, השרת מנפיק טוקן קצר מועד (JWT או nonce חתום ב-HMAC) עם הרשאות ספציפיות. לדוגמה, להעלאות קבצים ישירות ל-S3, השתמש ב-presigned URLs — המפתח הראשי לעולם לא עוזב את השרת.
מתי NDK וערפול מוצדקים
אם מחרוזת חייבת להיות באפליקציה ולא ניתן להביא אותה מהשרת, השתמש בקוד native. פונקציית 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 расшифровка
}זוהי אבטחה דרך ערפול, אבל היא מעלה את רף ההתקפה: קוד native קשה יותר לחיבור עם כלים אוטומטיים.
הגנה בזמן בנייה: מניעת דליפות במאגרי קוד
השתמש ב-kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly (שאינו נכלל ב-git) עבור משתני בנייה. דוגמה:
MAPS_API_KEY=AIzaSy... ב-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 расшифровка } :
manifestPlaceholders = [mapsApiKey: properties["MAPS_API_KEY"] ?: ""] ב-AndroidManifest:
<meta-data android:name="com.google.android.geo.API_KEY" android:value="${mapsApiKey}"/> המפתח לא מגיע למאגר הקוד, אבל הוא עדיין מגיע ל-APK וניתן לקריאה מהמניפסט. עבור מפתח Maps זה מקובל בשילוב עם הגבלות שם חבילה ו-SHA-1, אבל לא עבור סודות אמיתיים.
השוואת שיטות הגנה
| שיטה | רמת אבטחה | מורכבות יישום | המלצה |
|---|---|---|---|
| אחסון בקוד (string/resource) | נמוכה | נמוכה | לעולם לא להשתמש |
| ערפול (ProGuard/R8) | נמוכה–בינונית | בינונית | לא מספיק עבור סודות |
| NDK + הצפנה | בינונית | גבוהה | עבור מפתחות שלא ניתן להעביר |
| Keychain/Keystore | גבוהה | בינונית | עבור טוקנים ונתונים לאחר אימות |
| פרוקסי שרת | גבוהה מאוד | בינונית | עבור כל מפתחות ה-API החיצוניים |
טעויות נפוצות בהגנה על מפתחות
- אחסון מפתחות ב-
local.propertiesאו במשאבים — סימן ראשון לדליפה. - שימוש באותם מפתחות לפיתוח וייצור — סיכון במהלך הפיתוח.
- חוסר הגבלות בקונסולות הספקים — מפתח Maps ללא הגבלה יכול להיגנב על ידי כל אחד.
- הזנחת רוטציית מפתחות — החלף מפתחות מעת לעת, במיוחד אם יש חשד לדליפה.
מה העבודה שלנו כוללת
- ביקורת על כל מפתחות ה-API והסודות בקוד, בתצורות ובסקריפטים של בנייה.
- העברת מפתחות קריטיים לשרת עם הקמת שירות פרוקסי.
- יישום Keychain/Keystore עבור טוקנים וסודות בצד הלקוח.
- הגדרת הגבלות ב-Google Cloud Console, Stripe, Firebase ושירותים אחרים.
- תיעוד המתאר את תוכנית האחסון החדשה.
- תמיכה לאחר השקה — חודש אחד של ייעוץ חינם.
תהליך העבודה: מביקורת ועד פריסה
- ניתוח — מצא את כל המקומות שבהם משתמשים במפתחות.
- עיצוב — החלט אילו מפתחות עוברים לשרת ואילו נשארים על המכשיר.
- יישום — כתוב קוד עבור Keychain/Keystore, שרת פרוקסי, עדכן את תהליך הבנייה.
- בדיקות — ודא שמפתחות לא דולפים גם תחת ניתוח תעבורה או דקומפילציה.
- פריסה וניטור — פרסם עדכון, הגדר התראות לפעילות חריגה.
תוכנית הגנה מלאה אורכת 2 עד 5 ימים בהתאם למספר המפתחות והארכיטקטורה. העלות מחושבת באופן אישי לאחר ביקורת. אבטחנו מעל 20 פרויקטים ניידים. צור קשר להערכת הפרויקט שלך. קבל ייעוץ — כתוב לנו.







