
تطبيق مفتوح المصدر لجانب الهاتف من Android Auto مع هندسة عكسية للبروتوكول، والمصادقة المتبادلة عبر TLS، وإسقاط الفيديو H.264، وإدخال اللمس، وبث بيانات الاستشعار عبر USB AOA.
تطبيق مفتوح المصدر لجانب الهاتف في نظام Android Auto. يعمل هذا التطبيق على هاتفك ويُعرض على وحدة الرأس في السيارة عبر USB، ليحل محل تطبيق Google الخاص com.google.android.projection.gearhead.
هذا المشروع في مراحل التطوير المبكرة. عملية المصافحة البروتوكولية وعرض الفيديو تعملان مع وحدة رأس حقيقية. يتم عرض شاشة الهاتف بنجاح على وحدة رأس السيارة لعدة ثوانٍ قبل انقطاع الاتصال (يتم تحسين استقرار الفيديو).
استخدم على مسؤوليتك الخاصة. هذا البرنامج مقدم "كما هو"، دون أي ضمان من أي نوع.
USB Plug-in → MainActivity → ProjectionService ↓ UsbAoaTransport (USB AOA accessory mode) ↓ MessageFramer (16KB frame fragmentation) ↓ InBandTls (TLSv1.2 via SSLEngine) ↓ ProtocolEngine (AAP state machine) ↓ ┌───────────┼───────────┐ Video Input Audio (H.264) (touch/keys) (PCM)
## الأخطاء المعروفة
- **افتراض ترتيب تعيين القناة** — نقوم بتعيين أول `av_channel` في SERVICE_DISCOVERY_RESPONSE كفيديو والثاني كصوت. يعمل هذا مع وحدة الرأس في السيارة (القناة 1 = فيديو) ولكن يفشل مع openauto (القناة 4 = صوت، وليس فيديو). الإصلاح: تحليل حقل `stream_type` داخل `av_channel` لتمييز `VIDEO(3)` عن `AUDIO(1)`.
- **استقرار الفيديو** — ينقطع الاتصال بعد البث الممتد بسبب تجاوز سعة المخزن المؤقت USB لوحدة الرأس. انظر نتائج الاختبار أدناه.
- **ظهور الجهاز مكررًا على وحدة الرأس** — تعرض صفحة الهاتف الذكي لوحدة الرأس تطبيقنا كمدخلين منفصلين (واحد لـ Android Auto وآخر لـ Bluetooth) بدلاً من إدخال واحد بكلتا الإمكانيتين. يحدث هذا بسبب منع Android 12+ الوصول إلى عنوان Bluetooth MAC الحقيقي (يعيد `02:00:00:00:00:00`). الحل البديل: كتابة العنوان الحقيقي إلى ملف إعدادات عبر `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. نحتاج إلى شاشة إعدادات واجهة مستخدم للسماح للمستخدم بإدخال عنوان BT MAC يدويًا.
- **زر المساعد الصوتي لا تتم معالجته** — عندما يضغط السائق على زر الصوت/المساعد على وحدة الرأس، نتلقى طلب VOICE_SESSION_REQUEST ونحاول تشغيل مساعد صوتي (Dicio أو الافتراضي للنظام). ومع ذلك، لا يتلقى المساعد المشغل الصوت من ميكروفون وحدة الرأس بعد.
### نتائج اختبار استقرار الفيديو
نمط اختبار (أشرطة الألوان) بدقة 800x480، فاصل الإطارات I ثانية واحدة:
| FPS | معدل البت | تجزئة | المدة | الإطارات | الحالة |
|-----|--------|----------|--------|--------|--------|
| 30 | 2Mbps | لا | ~3ث | ~90 | ❌ سريع جدًا |
| 15 | 2Mbps | لا | ~33ث | ~500 | ⚠️ أفضل |
| 10 | 2Mbps | لا | ~93ث | ~930 | ⚠️ جيد |
| 30 | 2Mbps | نعم (2KB) | 5-25ث | 150-750 | ⚠️ متغير |
| 30 | 500Kbps | نعم (2KB) | ~54ث | ~1691 | ⚠️ أفضل |
| 15 | 250Kbps | نعم (2KB) | ~67ث+ | 1000+ | ⚠️ جيد |
| 30 | 250Kbps | نعم (2KB)، I=5ث | ~20ث | ~600 | ❌ أسوأ مع إطار I طويل |
| 15 | 250Kbps | لا | ~13ث | ~200 | ❌ التجزئة ساعدت هنا |
السبب الجذري: تجاوز سعة المخزن المؤقت لاستقبال USB لوحدة الرأس مع الإنتاجية العالية المستمرة. معدل بيانات أقل = اتصال أطول.
**أفضل إعداد مؤكد:** 10 إطارات في الثانية، 2 ميجابت في الثانية، بدون تجزئة = 93 ثانية. تنفيذ التجزئة قبل التشفير معطل (لا تستطيع وحدة الرأس إعادة التجميع) — يحتاج إلى مزيد من التحقيق.
## بناء```bash
./gradlew assembleDebug
يتطلب Android SDK مع platform 35.
./gradlew testDebugUnitTest
113 اختبار وحدة وتكامل تغطي البروتوكول، والتأطير، وTLS، ومنطق القناة، وآلة حالة الفيديو، ومعالجة المستشعرات، والإدخال باللمس.
### اختبار التكامل مع openauto (Docker)
openauto هو محاكي وحدة رأس طرف ثالث يتحدث بروتوكول Android Auto بالكامل. نستخدمه للتحقق من تنفيذ بروتوكولنا دون الحاجة إلى سيارة حقيقية.
#### المتطلبات الأساسية
- Docker مثبت وقيد التشغيل
- هاتف متصل عبر ADB (USB أو لاسلكي)
- التطبيق مثبت على الهاتف: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. بناء صورة Docker الخاصة بـ openauto (مرة واحدة)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
يقوم هذا ببناء openauto مع جميع التبعيات (Qt5، boost، protobuf، OpenSSL) في حاوية Debian. يستغرق حوالي 5 دقائق عند البناء الأول.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto يستمع على المنفذ 5000 داخل الحاوية، المعين إلى المنفذ 5100 على المضيف. يعمل في وضع بدون واجهة (لا حاجة لعرض).
#### 3. إعداد إعادة توجيه المنفذ العكسي لـ ADB```bash
adb reverse tcp:5000 tcp:5100
هذا يجعل نفق localhost:5000 الخاص بالهاتف إلى localhost:5100 الخاص بالحاسوب (openauto). يتصل تطبيقنا بـ localhost:5000 كعميل TCP عندما لا يتم العثور على ملحق USB.
adb shell am start -n org.openandroidauto/.MainActivity
سيقوم التطبيق بما يلي:
1. يفشل في العثور على ملحق USB
2. يتصل بـ `localhost:5000` (openauto عبر adb reverse)
3. يقوم بتنفيذ المصافحة الكاملة للبروتوكول (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. يفتح القنوات (فيديو، صوت، إدخال، حساس)
5. يبدأ بث الفيديو (نمط اختبار)
#### 5. التحقق من سجلات openauto
يجب أن ترى في مخرجات openauto:```
[OpenAuto] handleNewClient() - Handle WIFI Client Connection
[OpenAuto] [AndroidAutoEntity] Send Version Request.
[OpenAuto] [AndroidAutoEntity] onVersionResponse()
[OpenAuto] [AndroidAutoEntity] Beginning SSL handshake.
[OpenAuto] [AndroidAutoEntity] Handshake completed.
[OpenAuto] [AndroidAutoEntity] onServiceDiscoveryRequest()
[OpenAuto] [AndroidAutoEntity] onAudioFocusRequest()
[OpenAuto] [AudioMediaSinkService] onChannelOpenRequest()
[OpenAuto] [VideoMediaSinkService] onChannelOpenRequest() (if video focus granted)
adb pull /sdcard/Android/data/org.openandroidauto/files/aa_log.txt cat aa_log.txt
ملف السجل يستمر على الهاتف بين مفاتيح USB (مفيد عند الاختبار مع وحدة رأس السيارة الحقيقية).
#### اختبار سريع بسطر واحد```bash
# Assumes openauto image already built and app installed
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen openauto-headless timeout 20 /src/build/bin/autoapp &
sleep 3 && adb reverse tcp:5000 tcp:5100 && adb shell am start -n org.openandroidauto/.MainActivity
Message Id not Handled: 4 لـ AUTH_COMPLETE — هذه خصوصية معروفة في openauto، وليس خطأتم هندسة عكسية لبروتوكول Android Auto عبر عدة مشاريع. كل مشروع بنى على سابقه:
ما يضيفه opencardev/aasdk مقارنة بـ AACS:
الاختلافات الهيكلية الرئيسية:
priority + channel_id في ChannelOpenRequest؛ يستخدم aasdk priority (sint32) + service_idدليل thirdparty/aasdk/protobuf/ هو المرجع الرسمي للبروتوكول لهذا المشروع.
يستخدم Android Auto TLS ثنائي الاتجاه. يعمل الهاتف كخادم TLS ويجب أن يقدم شهادة موقعة من Google Automotive Link CA (مضمنة في برنامج وحدة الرأس الثابت). بدون المفتاح الخاص الصحيح، ترفض وحدة الرأس الاتصال بـ AUTH_COMPLETE status=-3.
يقدم الهاتف سلسلة من شهادتين:
O=CarService، موقعة من Google Automotive Link CAO=Google Automotive Link (صالحة 2014-2044)يبدو أن Google تقوم بتدوير الشهادة+المفتاح المضمنين في APK Android Auto كل 8 أشهر تقريبًا (مطابقة لفترة صلاحية الشهادة). قد يكون هذا إجراءً متعمدًا للحد من فائدة المفاتيح المستخرجة — إذا كانت وحدة الرأس تتحقق من انتهاء صلاحية الشهادة، فإن مفتاحًا مستخرجًا قديمًا سيتوقف عن العمل. يتلقى مستخدمو التطبيق الرسمي شهادات جديدة عبر تحديثات التطبيق. إذا كانت هذه النظرية صحيحة، فقد يتم رفض مستخدم لا يقوم بتحديث التطبيق الرسمي أبدًا من قبل وحدات الرأس التي تفرض انتهاء الصلاحية. ليست كل وحدات الرأس تتحقق من انتهاء الصلاحية — هذا السلوك يعتمد على الطراز.
المفتاح الخاص مشفر بـ AES-256-CBC داخل APK Android Auto. تتحقق وحدة الرأس من صحة شهادة الهاتف مقابل Google Automotive Link CA — أي شهادة موقعة من تلك CA مقبولة.
المفتاح مضمن (مشفر) في APK Android Auto ويمكن فك تشفيره باستخدام خوارزمية الـ APK الخاصة. يتطلب ذلك أي جهاز Android مع وصول ADB (لا يحتاج جذر، ولا خدمات Google Play) لتشغيل عملية فك التشفير، لأن مفكك Base64 في Android يتصرف بشكل مختلف عن JVM على سطح المكتب.
المتطلبات:
adb pull، أو نزّله من APKPure/APKMirror)d8، adb)العملية:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmالعثور على فئة مزود الشهادة (الخطوة 2):
أسماء الفئات مبهمة وتتغير بين إصدارات APK، لكن الهيكل دائمًا هو نفسه. ابحث في JADX عن "-----BEGIN CERTIFICATE-----" — ستجد فئة صغيرة تنفذ واجهة بثلاث طرق:
a() → ترجع String (PEM شهادة CarService)b() → ترجع byte[] (~1712 بايت — المفتاح الخاص المشفر بـ AES)c() → ترجع byte[] (256 بايت — ملح KDF)أسماء الفئات المعروفة حسب الإصدار:
دالة فك التشفير موجودة في فئة مجاورة — ابحث عن "AES/CBC/PKCS5Padding" للعثور عليها. تأخذ واجهة مزود الشهادة كمعامل.
ملاحظة: خطوة فك التشفير (الخطوة 4) تحتاج فقط إلى dalvikvm — أي جهاز Android مع ADB يعمل، لا جذر أو خدمات Google Play مطلوبة. مطلوب وجود GApps فقط للخطوة 1 (سحب الـ APK، نظرًا لأن تطبيق AA يتم توزيعه عبر متجر Play).
ملاحظة: لا تقم بتعديل أو تشغيل كود الـ APK الذي تم فك تجميعه. بدلاً من ذلك، اكتب فئة Decrypt.java مستقلة تعيد تنفيذ منطق فك التشفير، وتقرأ مصفوفات البايت المستخرجة من ملفات، ولها نقطة دخول main() خاصة بها. يتم استخدام الكود المصدري المُفكّك فقط كمرجع لفهم الخوارزمية ونسخ مصفوفات البايت. راجع tools/decrypt_key_from_apk.md للحصول على كود Decrypt.java الكامل.
خلل JADX خطير: يقوم JADX بفك تجميع مساعد KDF كـ byte b = bArr2[i2] & 255; لكن يجب أن يكون int b = bArr2[i2] & 255;. النوع byte يقطع القيمة إلى signed، مما ينتج عنه ناتج غير صحيح. أصلح هذا إلى int وسيعمل فك التشفير.
دالة KDF (tweakBytes/ap):```java
static void tweakBytes(byte[] bArr, byte[] bArr2, byte[] bArr3) {
for (int i = 0; i < bArr.length; i++) {
for (int i2 = 0; i2 < 48; i2++) {
int b = bArr2[i2] & 255; // MUST be int, not byte
bArr2[i2] = (byte) (((((b >> 7) | (b + b)) + 33) ^ bArr3[i2 % bArr3.length]) ^ bArr[i]);
}
}
}
بعد فك تشفير AES، تقوم الدالة `T()` باستخراج المفتاح:
- تجاوز أول 28 بايت، واحذف آخر 26 بايت
- فك تشفير Base64 (URL_SAFE, flag=2) للجزء الأوسط
- النتيجة هي مفتاح RSA خاص مشفر بتنسيق PKCS#8 DER
**ملاحظة:** يجب تشغيله على Android (وليس JVM سطح المكتب) بسبب الاختلافات بين `android.util.Base64` و `java.util.Base64`. يرفض `Base64.getUrlDecoder()` الخاص بـ JVM سطح المكتب الأحرف القياسية لـ Base64 (`+`, `/`) والأسطر الجديدة التي يقبلها مفكك تشفير Android. استخدم `Base64.getMimeDecoder()` على سطح المكتب، أو قم بتشغيل فك التشفير على الجهاز باستخدام `dalvikvm`:```bash
# Compile to DEX and run on any Android device with ADB access
javac Decrypt.java -d out
d8 out/Decrypt.class --output dex_out
adb push dex_out/classes.dex /data/local/tmp/decrypt.dex
adb shell "dalvikvm -cp /data/local/tmp/decrypt.dex Decrypt"
يقوم هذا بإخراج المفتاح الخاص PKCS#8 بتنسيق base64. قم بتغليفه في رؤوس PEM ووضعه في المسار app/src/main/assets/carservice_key.pem.
انظر tools/decrypt_key_from_apk.md للحصول على الدليل الكامل خطوة بخطوة.
نظرًا لأن بعض وحدات الرأس قد لا تتحقق من انتهاء صلاحية الشهادة، فقد لا يزال زوج الشهادة + المفتاح المستخرج مسبقًا (حتى لو منتهي الصلاحية) يعمل. المصادر:
[email protected] (انظر gamelaster/opengal_proxy)KeyFactory.generatePrivate() (يتطلب كلاً من صلاحيات الجذر وخدمات Google Play على نفس الجهاز): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
بعد الحصول عليه، ضع الشهادة+المفتاح في app/src/main/assets/carservice_key.pem.
راجع tools/dump_key.sh و tools/dump_key_frida.js للحصول على نصوص الاستخراج في وقت التشغيل.
هذا المشروع مرخص بموجب رخصة جنو العمومية العامة الإصدار 3.0.
يتضمن هذا المشروع تعريفات protocol buffer من aasdk (رخصة GPLv3، حقوق الطبع والنشر © 2018 f1x.studio / Michal Szwaj) كوحدة فرعية لـ git.
| المشروع | السنة | ملفات Proto | الدور |
|---|
| f1xpl/aasdk | 2018 | 1 متجانس (Wifi.proto) | الهندسة العكسية الأصلية — البروتوكول الأساسي، الفيديو، الصوت، الإدخال، الحساسات |
| AACS | 2020 | 28 (مقسمة حسب الرسالة) | تنفيذ من جهة الهاتف — تغطية بسيطة لإسقاط الفيديو |
| opencardev/aasdk | 2024 | 254 (هرمية حسب الخدمة) | المرجع النهائي — البروتوكول الكامل مع جميع الخدمات |
| إصدار APK | فئة مزود الشهادة | فئة الملح+المفتاح | فئة فك التشفير |
|---|
| v6.4 | SslWrapper (الحقول o, p) | نفس الفئة | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (الحقول b, c) | ivq.d() |