حماية مفاتيح API وأسرار البيئة
حماية مفاتيح API وأسرار البيئة
كل تطبيق Flutter يتواصل مع خدمات خارجية — الخرائط، التحليلات، بوابات الدفع، مزودي الإشعارات الفورية — يحتاج إلى نوع من بيانات الاعتماد. التعامل مع هذه البيانات باستهتار هو أحد أكثر الأخطاء الأمنية شيوعاً وخطورةً في تطوير تطبيقات الجوال. يشرح هذا الدرس سبب صعوبة حماية الأسرار في كود Dart المُجمَّع ويعرض الأسلوبين الرئيسيين للحفاظ عليها بعيداً عن نظام التحكم في الإصدارات وعن المهاجمين.
لماذا يمكن استخراج كود Dart والأصول
يقوم بناء Flutter للإصدار بتجميع Dart إلى كود أصلي ARM/x86 عبر مُجمِّع Dart AOT، لكن ذلك لا يجعل الأسرار آمنة. ثمة عدة مسارات تتيح للمهاجم استرداد السلاسل النصية التي تُضمِّنها مباشرةً في الكود:
- استخراج جدول السلاسل: سلاسل Dart النصية التي تظهر كثوابت حرفية تُخزَّن في اللقطة المُجمَّعة. أدوات مثل
stringsوradare2وjadxيمكنها تفريغ جدول السلاسل من أي APK أو IPA في ثوانٍ معدودة. - استخراج الأصول: أي ملف يُوضع في مجلد
assets/— بما في ذلك ملفات.envالمضافة إلىpubspec.yaml— يُحزَّم داخل أرشيف التطبيق دون تشفير. يكفي المهاجم أن يُعيد تسمية ملف APK إلى.zipليقرأ الملف مباشرةً. - فحص حركة الشبكة: حتى لو كان المفتاح مُخفَّى في الثنائي، فإنه سيظهر بنص واضح في طلبات الشبكة ما لم تُطبِّق أيضاً تثبيت الشهادات.
- تسريبات نظام التحكم بالإصدارات: إيداع ملف يحتوي على سر يكشفه لكل من لديه صلاحية قراءة المستودع، بمن فيهم مشغِّلو CI والتكاملات الخارجية والمساهمون في المستقبل.
الأسلوب الأول — --dart-define عند وقت البناء
تُمرِّر الراية --dart-define زوجاً من المفتاح والقيمة إلى عملية تجميع Dart. تُدمَج القيمة في الثنائي المُجمَّع لكنها لا تُكتب أبداً في أي ملف مصدري، لذا لا يمكن أن تُودَع في نظام التحكم بالإصدارات بطريق الخطأ. القيمة أيضاً أقل قابليةً للاكتشاف مقارنةً بثابت نصي مباشر، وإن كان مهاجم مُصِر لا يزال قادراً على العثور عليها بأدوات تحليل الثنائيات.
تمرير قيمة --dart-define وقراءتها
// أمر البناء (CI أو Fastlane أو الطرفية):
// flutter build apk --dart-define=MAPS_API_KEY=AIzaSyXXXXXXXXXX
// flutter build ios --dart-define=MAPS_API_KEY=AIzaSyXXXXXXXXXX
// قراءة القيمة في كود Dart:
class AppSecrets {
// fromEnvironment تُحلَّل في وقت الترجمة وليس وقت التشغيل.
// القيمة الافتراضية ('') تُستخدم حين لا تُوفَّر --dart-define،
// مثل عند تشغيل `flutter run` بدون الراية.
static const String mapsApiKey =
String.fromEnvironment('MAPS_API_KEY', defaultValue: '');
static const String stripePublishableKey =
String.fromEnvironment('STRIPE_PK', defaultValue: '');
}
// الاستخدام في أي مكان في التطبيق:
void initMaps() {
if (AppSecrets.mapsApiKey.isEmpty) {
throw StateError('MAPS_API_KEY لم يُوفَّر في وقت البناء');
}
GoogleMapsFlutter.init(AppSecrets.mapsApiKey);
}
--dart-define كأسرار مُشفَّرة في مزود CI/CD الخاص بك (أسرار GitHub Actions، أسرار Bitrise، متغيرات بيئة Codemagic). يحقنها خط الأنابيب في وقت البناء ولا يرى أي مطوِّر القيمة الحقيقية في ملف على القرص.للمشاريع ذات التعريفات الكثيرة، مرِّر ملف JSON باستخدام --dart-define-from-file=secrets.json (متاح منذ Flutter 3.7). أضف هذا الملف إلى .gitignore — أودِع فقط secrets.example.json يحتوي قيماً نموذجية.
استخدام --dart-define-from-file (Flutter 3.7 وما بعده)
// secrets.json (لا تودع هذا الملف أبداً — أضفه إلى .gitignore)
// {
// "MAPS_API_KEY": "AIzaSyXXXXXXXXXX",
// "STRIPE_PK": "pk_live_XXXXXXXXXX",
// "SENTRY_DSN": "https://abc@o123.ingest.sentry.io/456"
// }
// secrets.example.json (أودِع هذا كقالب)
// {
// "MAPS_API_KEY": "YOUR_MAPS_API_KEY_HERE",
// "STRIPE_PK": "YOUR_STRIPE_KEY_HERE",
// "SENTRY_DSN": "YOUR_SENTRY_DSN_HERE"
// }
// أمر البناء:
// flutter run --dart-define-from-file=secrets.json
// flutter build apk --dart-define-from-file=secrets.json
// القراءة مطابقة لـ --dart-define الفردي:
static const String sentryDsn =
String.fromEnvironment('SENTRY_DSN', defaultValue: '');
الأسلوب الثاني — flutter_dotenv في وقت التشغيل
تقرأ حزمة flutter_dotenv ملف .env قياسياً من حزمة أصول التطبيق في وقت التشغيل. هذا الأسلوب مألوف لمطوري الويب والواجهة الخلفية، لكنه ينطوي على تحفظ مهم: ملف .env هو أصل نصي واضح داخل APK/IPA. يبقي الأسرار خارج كود المصدر ونظام التحكم بالإصدارات، لكن المهاجم الذي يستخرج الأرشيف يمكنه قراءة الملف مباشرةً.
استخدم flutter_dotenv حين تحتاج قيماً مختلفة لكل بيئة (تطوير / اختبار / إنتاج) دون إعادة البناء، أو حين اعتاد فريقك على سير عمل dotenv. للأسرار الإنتاجية التي يجب ألا تُكشف، فضِّل --dart-define الذي يحقنه CI، أو انقل السر إلى خادمك الخلفي كلياً.
إعداد flutter_dotenv
// 1. أضف الاعتمادية: flutter pub add flutter_dotenv
// 2. أنشئ .env في جذر المشروع (أضفه إلى .gitignore!)
// MAPS_API_KEY=AIzaSyXXXXXXXXXX
// BASE_URL=https://api.myapp.com
// FEATURE_FLAG_CHAT=true
// 3. سجِّل الأصل في pubspec.yaml:
// flutter:
// assets:
// - .env
// 4. حمِّله في main.dart قبل runApp:
import 'package:flutter_dotenv/flutter_dotenv.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
runApp(const MyApp());
}
// 5. اقرأ القيم في أي مكان:
final apiKey = dotenv.env['MAPS_API_KEY'] ?? '';
final baseUrl = dotenv.env['BASE_URL'] ?? 'https://localhost';
final chatEnabled = dotenv.env['FEATURE_FLAG_CHAT'] == 'true';
.env دائماً إلى .gitignore قبل كتابة أي أسرار فيه. أنشئ ملف .env.example مُودَعاً يسرد كل مفتاح بقيمة فارغة أو نموذجية حتى يعرف المطورون الجدد ما يجب تعبئته.الاختيار بين الأسلوبين
- استخدم --dart-define حين يجب ألا تكون الأسرار في أي ملف على القرص — القيمة تعيش فقط في أسرار CI والثنائي المُجمَّع.
- استخدم flutter_dotenv حين تحتاج تبديلاً سهلاً بين البيئات دون إعادة البناء، أو للإعدادات غير الحساسة كأعلام الميزات وعناوين URL الأساسية ومستويات التسجيل.
- لا تستخدم أياً منهما للاعتمادات الحساسة حقاً كمفاتيح API من جانب الخادم، أو أسرار OAuth، أو مفاتيح معالجة الدفع — مرِّر هذه عبر خادمك الخلفي الخاص وصادِق التطبيق بتوكن قصير الأمد بدلاً من ذلك.
ملخص
لا يوفر تجميع Dart AOT حمايةً فعلية للثوابت النصية من الاستخراج. الدفاعان الرئيسيان هما --dart-define (حقن في وقت التجميع، يحتفظ بالأسرار في CI) وflutter_dotenv (تحميل أصل في وقت التشغيل، يُبقي الأسرار خارج كود المصدر لكن ليس خارج APK). للاعتمادات الحساسة حقاً، الأسلوب الآمن الوحيد هو وكيل الواجهة الخلفية. أودِع دائماً .env.example وsecrets.example.json حتى يتمكن زملاؤك من الانضمام دون رؤية الأسرار الحقيقية.