
Implementazione open-source lato telefono di Android Auto con reverse engineering del protocollo, autenticazione reciproca TLS, proiezione video H.264, iniezione di input tattile e streaming di dati dei sensori tramite USB AOA.
Un'implementazione open-source dell'app lato telefono di Android Auto. Questa app viene eseguita sul telefono e proietta sull'unità head dell'auto tramite USB, sostituendo l'APK proprietario com.google.android.projection.gearhead di Google.
Questo progetto è in fase iniziale di sviluppo. L'handshake del protocollo e la proiezione video funzionano con una vera unità head. Lo schermo del telefono viene visualizzato correttamente sull'unità head dell'auto per diversi secondi prima di disconnettersi (la stabilità video è in fase di miglioramento).
UTILIZZARE A PROPRIO RISCHIO. Questo software è fornito "così com'è", senza alcuna garanzia.
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)
## Bug noti
- **L'assegnazione dei canali presume un ordine** — Assegniamo il primo `av_channel` in SERVICE_DISCOVERY_RESPONSE come video e il secondo come audio. Questo funziona con l'autoradio (canale 1 = video) ma fallisce con openauto (canale 4 = audio, non video). Correzione: analizzare il campo `stream_type` all'interno di `av_channel` per distinguere `VIDEO(3)` da `AUDIO(1)`.
- **Stabilità video** — La connessione cade dopo uno streaming prolungato a causa dell'overflow del buffer USB dell'autoradio. Vedi i risultati dei test qui sotto.
- **Elenco dispositivi duplicato sull'autoradio** — La pagina degli smartphone dell'autoradio mostra la nostra app come due voci separate (una per Android Auto, una per Bluetooth) invece di una singola voce con entrambe le capacità. Ciò è causato dal blocco di Android 12+ all'accesso al vero indirizzo MAC Bluetooth (restituisce `02:00:00:00:00:00`). Soluzione alternativa: scrivere l'indirizzo reale in un file di configurazione tramite `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. Serve una schermata delle impostazioni UI per permettere all'utente di inserire manualmente il proprio MAC BT.
- **Pulsante dell'assistente vocale non gestito** — Quando il conducente preme il pulsante vocale/dell'assistente sull'autoradio, riceviamo una VOICE_SESSION_REQUEST e tentiamo di avviare un assistente vocale (Dicio o quello predefinito di sistema). Tuttavia, l'assistente avviato non riceve ancora l'audio dal microfono dell'autoradio.
### Risultati dei test di stabilità video
Pattern di test (barre colorate) a 800x480, intervallo I-frame 1 secondo:
| FPS | Bitrate | Frammento | Durata | Frame | Stato |
|-----|---------|-----------|--------|-------|-------|
| 30 | 2Mbps | No | ~3s | ~90 | ❌ Troppo veloce |
| 15 | 2Mbps | No | ~33s | ~500 | ⚠️ Migliore |
| 10 | 2Mbps | No | ~93s | ~930 | ⚠️ Buono |
| 30 | 2Mbps | Sì (2KB) | 5-25s | 150-750 | ⚠️ Variabile |
| 30 | 500Kbps | Sì (2KB) | ~54s | ~1691 | ⚠️ Migliore |
| 15 | 250Kbps | Sì (2KB) | ~67s+ | 1000+ | ⚠️ Buono |
| 30 | 250Kbps | Sì (2KB), I=5s | ~20s | ~600 | ❌ Peggio con I-frame lungo |
| 15 | 250Kbps | No | ~13s | ~200 | ❌ La frammentazione ha aiutato qui |
Causa principale: Il buffer di ricezione USB dell'autoradio va in overflow con un throughput elevato sostenuto. Una velocità di trasmissione dati più bassa = connessione più lunga.
**Configurazione migliore confermata:** 10fps, 2Mbps, nessuna frammentazione = 93 secondi. L'implementazione della frammentazione prima della crittografia è danneggiata (l'autoradio non riesce a riassemblare) — necessita di ulteriori indagini.
## Compilazione```bash
./gradlew assembleDebug
Richiede Android SDK con piattaforma 35.
./gradlew testDebugUnitTest
113 test unit e di integrazione che coprono protocollo, framing, TLS, logica dei canali, macchina a stati video, gestione dei sensori e input tattile.
### Test di integrazione con openauto (Docker)
openauto è un emulatore di head unit di terze parti che parla il protocollo Android Auto completo. Lo utilizziamo per verificare la nostra implementazione del protocollo senza bisogno di un'auto reale.
#### Prerequisiti
- Docker installato e in esecuzione
- Telefono connesso tramite ADB (USB o wireless)
- App installata sul telefono: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Build dell'immagine Docker openauto (una tantum)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
Questo costruisce openauto con tutte le dipendenze (Qt5, boost, protobuf, OpenSSL) in un container Debian. Richiede circa 5 minuti al primo avvio.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto ascolta sulla porta 5000 all'interno del container, mappata sulla porta 5100 dell'host. Funziona in modalità headless (nessun display richiesto).
#### 3. Configura il reverse port forwarding ADB```bash
adb reverse tcp:5000 tcp:5100
Questo fa sì che il tunnel localhost:5000 del telefono vada al localhost:5100 del computer (openauto). La nostra app si connette a localhost:5000 come client TCP quando non viene trovato un accessorio USB.
adb shell am start -n org.openandroidauto/.MainActivity
L'app:
1. Fallire nel trovare un accessorio USB
2. Connettersi a `localhost:5000` (openauto tramite adb reverse)
3. Eseguire l'intero handshake del protocollo (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Aprire i canali (video, audio, input, sensore)
5. Avviare lo streaming video (pattern di test)
#### 5. Verifica nei log di openauto
Dovresti vedere nell'output di 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
Il file di log persiste sul telefono tra le commutazioni USB (utile quando si testa con un'autoradio reale).
#### Test one-liner rapido```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 per AUTH_COMPLETE — questa è una stranezza nota di openauto, non un erroreIl protocollo Android Auto è stato ottenuto tramite reverse engineering in diversi progetti. Ciascuno si basava sul precedente:
Cosa aggiunge opencardev/aasdk rispetto ad AACS:
Differenze strutturali chiave:
priority + channel_id in ChannelOpenRequest; aasdk utilizza priority (sint32) + service_idLa directory thirdparty/aasdk/protobuf/ è il riferimento del protocollo autorevole per questo progetto.
Android Auto utilizza TLS mutuo. Il telefono funge da server TLS e deve presentare un certificato firmato dalla Google Automotive Link CA (integrata nel firmware dell'unità principale). Senza la chiave privata corretta, l'unità principale rifiuta la connessione con AUTH_COMPLETE status=-3.
Il telefono presenta una catena di 2 certificati:
O=CarService, firmato dalla Google Automotive Link CAO=Google Automotive Link (valido 2014-2044)Google sembra ruotare il certificato+chiave incorporati nell'APK di Android Auto circa ogni 8 mesi (coincidendo con il periodo di validità del certificato). Potrebbe essere una misura deliberata per limitare l'utilità delle chiavi estratte — se un'unità principale controlla la scadenza del certificato, una vecchia chiave estratta smetterebbe di funzionare. Gli utenti dell'app ufficiale ricevono certificati nuovi tramite gli aggiornamenti dell'app. Se questa teoria è corretta, un utente che non aggiorna mai l'app ufficiale potrebbe alla fine essere rifiutato dalle unità principali che impongono la scadenza. Non tutte le unità principali potrebbero controllare la scadenza — questo comportamento dipende dal modello.
La chiave privata è crittografata con AES-256-CBC all'interno dell'APK di Android Auto. L'unità principale valida il certificato del telefono rispetto alla Google Automotive Link CA — qualsiasi certificato firmato da quella CA è accettato.
La chiave è incorporata (crittografata) nell'APK di Android Auto e può essere decriptata utilizzando l'algoritmo stesso dell'APK. Ciò richiede qualsiasi dispositivo Android con accesso ADB (nessun root, nessun servizio Google Play necessario) per eseguire la decriptazione, perché il decoder Base64 di Android si comporta diversamente dalla JVM desktop.
Requisiti:
adb pull, o scaricalo da APKPure/APKMirror)d8 build tool, adb)Procedimento:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmTrovare la classe del fornitore del certificato (passaggio 2):
I nomi delle classi sono offuscati e cambiano tra le versioni dell'APK, ma la struttura è sempre la stessa. Cerca in JADX "-----BEGIN CERTIFICATE-----" — troverai una piccola classe che implementa un'interfaccia con tre metodi:
a() → restituisce una String (il certificato CarService PEM)b() → restituisce un byte[] (~1712 byte — la chiave privata crittografata con AES)c() → restituisce un byte[] (256 byte — il sale KDF)Nomi di classi noti per versione:
La funzione di decriptazione si trova in una classe vicina — cerca "AES/CBC/PKCS5Padding" per trovarla. Prende l'interfaccia del fornitore del certificato come parametro.
Nota: Il passaggio di decriptazione (passaggio 4) richiede solo dalvikvm — qualsiasi dispositivo Android con ADB funziona, nessun root o servizi Google Play richiesti. Il requisito di GApps è solo per il passaggio 1 (estrarre l'APK, poiché l'app AA è distribuita tramite Play Store).
Nota: Non si modifica né si esegue il codice APK decompilato. Invece, si scrive una classe autonoma Decrypt.java che reimplementa la logica di decriptazione, legge i byte array estratti dai file e ha il proprio punto di ingresso main(). Il sorgente decompilato viene utilizzato solo come riferimento per comprendere l'algoritmo e copiare i byte array. Vedi tools/decrypt_key_from_apk.md per il sorgente completo di Decrypt.java.
Bug critico di JADX: JADX decompila l'helper KDF come byte b = bArr2[i2] & 255; ma deve essere int b = bArr2[i2] & 255;. Il tipo byte tronca di nuovo a signed, producendo output spazzatura. Correggi in int e la decriptazione funziona.
La funzione 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]);
}
}
}
Dopo la decifratura AES, la funzione `T()` estrae la chiave:
- Salta i primi 28 byte, taglia gli ultimi 26 byte
- Decodifica Base64 (URL_SAFE, flag=2) della parte centrale
- Il risultato è una chiave privata RSA codificata in PKCS#8 DER
**Nota:** Deve essere eseguito su Android (non su JVM desktop) a causa delle differenze tra `android.util.Base64` e `java.util.Base64`. Il `Base64.getUrlDecoder()` della JVM desktop rifiuta i caratteri base64 standard (`+`, `/`) e i newline che il decoder di Android accetta. Usa `Base64.getMimeDecoder()` sul desktop, oppure esegui la decifratura sul dispositivo con `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"
Questo produce la chiave privata PKCS#8 in base64. Avvolgila nelle intestazioni PEM e posizionala in app/src/main/assets/carservice_key.pem.
Vedi tools/decrypt_key_from_apk.md per la guida completa passo-passo.
Poiché alcune unità head potrebbero non controllare la scadenza del certificato, una coppia cert+key precedentemente estratta (anche scaduta) potrebbe ancora funzionare. Fonti:
[email protected] (vedi gamelaster/opengal_proxy)KeyFactory.generatePrivate() (richiede sia root che Google Play Services sullo stesso dispositivo): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Una volta ottenuti, posiziona il cert+key in app/src/main/assets/carservice_key.pem.
Vedi tools/dump_key.sh e tools/dump_key_frida.js per script di estrazione runtime.
Questo progetto è concesso in licenza secondo i termini della GNU General Public License v3.0.
Questo progetto include definizioni di protocol buffer da aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) come git submodule.
| Progetto | Anno | File Proto | Ruolo |
|---|
| f1xpl/aasdk | 2018 | 1 monolitico (Wifi.proto) | RE originale — protocollo core, video, audio, input, sensori |
| AACS | 2020 | 28 (suddivisi per messaggio) | Implementazione lato telefono — copertura minima per la proiezione video |
| opencardev/aasdk | 2024 | 254 (gerarchici per servizio) | Riferimento definitivo — protocollo completo con tutti i servizi |
| Versione APK | Classe fornitore certificato | Classe salt+key | Classe di decriptazione |
|---|
| v6.4 | SslWrapper (campi o, p) | stessa classe | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (campi b, c) | ivq.d() |