
Open-Source-Implementierung der Android-Auto-Telefonseite mit Reverse Engineering des Protokolls, gegenseitiger TLS-Authentifizierung, H.264-Videoprojektion, Injektion von Touch-Eingaben und Streaming von Sensordaten über USB AOA.
Eine Open-Source-Implementierung der telefonseitigen Android-Auto-App. Diese App läuft auf deinem Telefon und projiziert über USB auf die Headunit deines Autos. Sie ersetzt die proprietäre Google-APK com.google.android.projection.gearhead.
Dieses Projekt befindet sich in einem frühen Entwicklungsstadium. Der Protokoll-Handshake und die Videoprojektion funktionieren mit einer echten Headunit. Der Telefonbildschirm wird auf der Headunit des Autos für mehrere Sekunden erfolgreich angezeigt, bevor die Verbindung getrennt wird (die Videostabilität wird verbessert).
NUTZUNG AUF EIGENE GEFAHR. Diese Software wird „wie besehen" ohne jegliche Gewährleistung bereitgestellt.
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)
## Bekannte Fehler
- **Kanalzuordnung setzt Reihenfolge voraus** – Wir weisen den ersten `av_channel` in der SERVICE_DISCOVERY_RESPONSE als Video und den zweiten als Audio zu. Das funktioniert mit dem Headunit (Kanal 1 = Video), scheitert aber bei openauto (Kanal 4 = Audio, nicht Video). Fix: Das Feld `stream_type` innerhalb von `av_channel` parsen, um `VIDEO(3)` von `AUDIO(1)` zu unterscheiden.
- **Videostabilität** – Verbindungsabbrüche nach längerem Streaming aufgrund eines USB-Pufferüberlaufs im Headunit. Siehe Testergebnisse unten.
- **Doppelte Geräteliste auf dem Headunit** – Die Smartphone-Seite des Headunits zeigt unsere App als zwei separate Einträge (einen für Android Auto, einen für Bluetooth), statt eines einzelnen Eintrags mit beiden Fähigkeiten. Ursache ist, dass Android 12+ den Zugriff auf die echte Bluetooth-MAC-Adresse blockiert (`02:00:00:00:00:00` wird zurückgegeben). Workaround: Die echte Adresse über `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"` in eine Konfigurationsdatei schreiben. Es wird ein UI-Einstellungsbildschirm benötigt, damit der Benutzer seine BT-MAC manuell eingeben kann.
- **Sprachassistent-Taste nicht behandelt** – Wenn der Fahrer die Sprach-/Assistent-Taste auf dem Headunit drückt, empfangen wir eine VOICE_SESSION_REQUEST und versuchen, einen Sprachassistenten (Dicio oder Systemstandard) zu starten. Der gestartete Assistent empfängt jedoch noch kein Audio vom Mikrofon des Headunits.
### Testergebnisse zur Videostabilität
Testmuster (Farbbalken) bei 800x480, I-Frame-Intervall 1 Sekunde:
| FPS | Bitrate | Fragment | Dauer | Frames | Status |
|-----|---------|----------|----------|--------|--------|
| 30 | 2Mbps | Nein | ~3s | ~90 | ❌ Zu schnell |
| 15 | 2Mbps | Nein | ~33s | ~500 | ⚠️ Besser |
| 10 | 2Mbps | Nein | ~93s | ~930 | ⚠️ Gut |
| 30 | 2Mbps | Ja (2KB) | 5-25s | 150-750 | ⚠️ Variabel |
| 30 | 500Kbps | Ja (2KB) | ~54s | ~1691 | ⚠️ Besser |
| 15 | 250Kbps | Ja (2KB) | ~67s+ | 1000+ | ⚠️ Gut |
| 30 | 250Kbps | Ja (2KB), I=5s | ~20s | ~600 | ❌ Schlechter mit langem I-Frame |
| 15 | 250Kbps | Nein | ~13s | ~200 | ❌ Fragmentierung half hier |
Hauptursache: Der USB-Empfangspuffer des Headunits läuft bei dauerhaft hohem Datendurchsatz über. Niedrigere Datenrate = längere Verbindung.
**Beste bestätigte Konfiguration:** 10fps, 2Mbps, keine Fragmentierung = 93 Sekunden. Die Fragment-vor-Verschlüsselung-Implementierung ist defekt (das Headunit kann nicht reassemblen) – bedarf weiterer Untersuchung.
## Build```bash
./gradlew assembleDebug
Erfordert Android SDK mit Plattform 35.
./gradlew testDebugUnitTest
113 Unit- und Integrationstests, die Protokoll, Framing, TLS, Kanallogik, Video-Zustandsmaschine, Sensorverarbeitung und Touch-Eingabe abdecken.
### Integrationstests mit openauto (Docker)
openauto ist ein Drittanbieter-Head-Unit-Emulator, der das vollständige Android-Auto-Protokoll spricht. Wir verwenden ihn, um unsere Protokollimplementierung zu verifizieren, ohne ein echtes Auto zu benötigen.
#### Voraussetzungen
- Docker installiert und läuft
- Telefon per ADB verbunden (USB oder kabellos)
- App auf dem Telefon installiert: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Das openauto-Docker-Image erstellen (einmalig)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
Dies erstellt openauto mit allen Abhängigkeiten (Qt5, boost, protobuf, OpenSSL) in einem Debian-Container. Dauert beim ersten Build etwa 5 Minuten.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto lauscht auf Port 5000 im Container, der auf Port 5100 des Hosts gemappt ist. Es läuft im Headless-Modus (kein Display erforderlich).
#### 3. ADB-Reverse-Port-Weiterleitung einrichten```bash
adb reverse tcp:5000 tcp:5100
Dadurch wird der localhost:5000 des Telefons zum localhost:5100 (openauto) des Computers getunnelt. Unsere App verbindet sich als TCP-Client mit localhost:5000, wenn kein USB-Zubehör gefunden wird.
adb shell am start -n org.openandroidauto/.MainActivity
Die App wird:
1. Kein USB-Zubehör finden
2. Zu `localhost:5000` verbinden (openauto per adb reverse)
3. Den vollständigen Protokoll-Handshake durchführen (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Kanäle öffnen (video, audio, input, sensor)
5. Video-Streaming starten (Testbild)
#### 5. In openauto-Logs überprüfen
In der openauto-Ausgabe sollten Sie Folgendes sehen:```
[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
Die Protokolldatei bleibt auf dem Telefon zwischen USB-Wechseln erhalten (nützlich beim Testen mit einem echten Autoradio).
#### Schneller Einzeiler-Test```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 für AUTH_COMPLETE — das ist eine bekannte openauto-Eigenheit, kein FehlerDas Android-Auto-Protokoll wurde in mehreren Projekten per Reverse Engineering untersucht. Jedes baute auf dem vorherigen auf:
Was opencardev/aasdk gegenüber AACS zusätzlich bietet:
Wichtige strukturelle Unterschiede:
priority + channel_id in ChannelOpenRequest; aasdk verwendet priority (sint32) + service_idDas Verzeichnis thirdparty/aasdk/protobuf/ ist die maßgebliche Protokollreferenz für dieses Projekt.
Android Auto verwendet gegenseitiges TLS. Das Telefon fungiert als TLS-Server und muss ein Zertifikat vorweisen, das von der Google Automotive Link CA signiert ist (fest in der Firmware der Head-Unit eingebettet). Ohne den korrekten privaten Schlüssel lehnt die Head-Unit die Verbindung mit AUTH_COMPLETE status=-3 ab.
Das Telefon präsentiert eine Kette aus 2 Zertifikaten:
O=CarService, signiert von der Google Automotive Link CAO=Google Automotive Link (gültig 2014-2044)Google scheint das in der Android-Auto-APK eingebettete Zertifikat und den Schlüssel ungefähr alle 8 Monate zu rotieren (passend zur Gültigkeitsdauer des Zertifikats). Dies könnte eine bewusste Maßnahme sein, um den Nutzen extrahierter Schlüssel zu begrenzen — wenn eine Head-Unit das Ablaufdatum des Zertifikats prüft, würde ein alter extrahierter Schlüssel nicht mehr funktionieren. Benutzer der offiziellen App erhalten über App-Updates frische Zertifikate. Wenn diese Theorie korrekt ist, könnte ein Benutzer, der die offizielle App nie aktualisiert, schließlich von Head-Units abgelehnt werden, die den Ablauf erzwingen. Nicht alle Head-Units prüfen möglicherweise den Ablauf — dieses Verhalten ist modellabhängig.
Der private Schlüssel ist in der Android-Auto-APK mit AES-256-CBC verschlüsselt. Die Head-Unit validiert das Zertifikat des Telefons gegen die Google Automotive Link CA — jedes von dieser CA signierte Zertifikat wird akzeptiert.
Der Schlüssel ist (verschlüsselt) in der Android-Auto-APK eingebettet und kann mit dem eigenen Algorithmus der APK entschlüsselt werden. Dafür ist ein beliebiges Android-Gerät mit ADB-Zugriff erforderlich (kein Root, keine Google Play Dienste nötig), um die Entschlüsselung auszuführen, da der Base64-Decoder von Android sich anders verhält als die Desktop-JVM.
Voraussetzungen:
adb pull von einem Telefon ziehen oder von APKPure/APKMirror herunterladen)d8-Buildtool, adb)Vorgang:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvm auf dem Gerät ausführenDie Zertifikatsanbieter-Klasse finden (Schritt 2):
Die Klassennamen sind verschleiert und ändern sich zwischen APK-Versionen, aber die Struktur ist immer gleich. Suche in JADX nach "-----BEGIN CERTIFICATE-----" — du findest eine kleine Klasse, die ein Interface mit drei Methoden implementiert:
a() → gibt einen String zurück (das CarService-Zertifikat als PEM)b() → gibt ein byte[] zurück (~1712 Bytes — der AES-verschlüsselte private Schlüssel)c() → gibt ein byte[] zurück (256 Bytes — das KDF-Salt)Bekannte Klassennamen nach Version:
Die Entschlüsselungsfunktion befindet sich in einer benachbarten Klasse — suche nach "AES/CBC/PKCS5Padding", um sie zu finden. Sie nimmt das Zertifikatsanbieter-Interface als Parameter entgegen.
Hinweis: Der Entschlüsselungsschritt (Schritt 4) benötigt nur dalvikvm — jedes Android-Gerät mit ADB funktioniert, kein Root oder Google Play Dienste erforderlich. Die GApps-Anforderung gilt nur für Schritt 1 (Ziehen der APK, da die AA-App über den Play Store vertrieben wird).
Hinweis: Du modifizierst oder führst den dekompilierten APK-Code nicht aus. Stattdessen schreibst du eine eigenständige Decrypt.java-Klasse, die die Entschlüsselungslogik neu implementiert, die extrahierten Byte-Arrays aus Dateien liest und einen eigenen main()-Einstiegspunkt hat. Der dekompilierte Quellcode wird nur als Referenz verwendet, um den Algorithmus zu verstehen und die Byte-Arrays zu kopieren. Siehe tools/decrypt_key_from_apk.md für den vollständigen Decrypt.java-Quellcode.
Kritischer JADX-Fehler: JADX dekompiliert den KDF-Helfer als byte b = bArr2[i2] & 255;, aber es muss int b = bArr2[i2] & 255; sein. Der Typ byte schneidet auf vorzeichenbehaftet zurück und erzeugt Müllausgabe. Ändere das zu int und die Entschlüsselung funktioniert.
Die KDF-Funktion (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]);
}
}
}
Nach der AES-Entschlüsselung extrahiert die Funktion `T()` den Schlüssel:
- Überspringe die ersten 28 Bytes, entferne die letzten 26 Bytes
- Den mittleren Teil per Base64 dekodieren (URL_SAFE, flag=2)
- Das Ergebnis ist ein PKCS#8-DER-codierter privater RSA-Schlüssel
**Hinweis:** Muss auf Android ausgeführt werden (nicht auf der Desktop-JVM), da sich `android.util.Base64` und `java.util.Base64` unterscheiden. Der `Base64.getUrlDecoder()` der Desktop-JVM lehnt Standard-Base64-Zeichen (`+`, `/`) und Zeilenumbrüche ab, die der Android-Decoder akzeptiert. Verwende auf dem Desktop `Base64.getMimeDecoder()` oder führe die Entschlüsselung auf dem Gerät mit `dalvikvm` aus:```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"
This outputs the PKCS#8 private key in base64. Wrap it in PEM headers and place at app/src/main/assets/carservice_key.pem.
See tools/decrypt_key_from_apk.md for the full step-by-step guide.
Da einige Head-Units das Ablaufdatum des Zertifikats möglicherweise nicht prüfen, kann ein zuvor extrahiertes Zertifikat+Schlüssel-Paar (auch wenn es abgelaufen ist) weiterhin funktionieren. Quellen:
[email protected] (siehe gamelaster/opengal_proxy)KeyFactory.generatePrivate() zu hooken (erfordert sowohl Root als auch Google Play Services auf demselben Gerät): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Sobald du sie erhalten hast, lege das Zertifikat und den Schlüssel unter app/src/main/assets/carservice_key.pem ab.
Siehe tools/dump_key.sh und tools/dump_key_frida.js für Skripte zur Laufzeit-Extraktion.
Dieses Projekt ist unter der GNU General Public License v3.0 lizenziert.
Dieses Projekt enthält Protocol-Buffer-Definitionen von aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) als Git-Submodul.
| Projekt | Jahr | Proto-Dateien | Rolle |
|---|
| f1xpl/aasdk | 2018 | 1 monolithisch (Wifi.proto) | Ursprüngliches RE — Kernprotokoll, Video, Audio, Eingabe, Sensoren |
| AACS | 2020 | 28 (nach Nachricht aufgeteilt) | Telefonseitige Implementierung — minimale Abdeckung für Videoprojektion |
| opencardev/aasdk | 2024 | 254 (hierarchisch nach Dienst) | Definitive Referenz — vollständiges Protokoll mit allen Diensten |
| APK-Version | Zertifikatsanbieter-Klasse | Salt+Schlüssel-Klasse | Entschlüsselungsklasse |
|---|
| v6.4 | SslWrapper (Felder o, p) | dieselbe Klasse | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (Felder b, c) | ivq.d() |