
Implementación de código abierto del lado del teléfono de Android Auto con ingeniería inversa de protocolo, autenticación mutua TLS, proyección de video H.264, inyección de entrada táctil y transmisión de datos de sensores a través de USB AOA.
Una implementación de código abierto de la app del lado del teléfono de Android Auto. Esta app se ejecuta en tu teléfono y se proyecta en la unidad principal del automóvil a través de USB, reemplazando el APK propietario de Google com.google.android.projection.gearhead.
Este proyecto se encuentra en desarrollo temprano. El handshake del protocolo y la proyección de vídeo funcionan con una unidad principal real. La pantalla del teléfono se muestra con éxito en la unidad principal del automóvil durante varios segundos antes de desconectarse (se está mejorando la estabilidad del vídeo).
ÚSALO BAJO TU PROPIO RIESGO. Este software se proporciona «tal cual», sin garantía de ningún tipo.
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)
## Errores conocidos
- **La asignación de canales asume un orden** — Asignamos el primer `av_channel` en `SERVICE_DISCOVERY_RESPONSE` como vídeo y el segundo como audio. Esto funciona con la unidad principal del coche (canal 1 = vídeo) pero falla con openauto (canal 4 = audio, no vídeo). Solución: analizar el campo `stream_type` dentro de `av_channel` para distinguir `VIDEO(3)` de `AUDIO(1)`.
- **Estabilidad del vídeo** — La conexión se cae después de una transmisión prolongada debido al desbordamiento del búfer USB de la unidad principal. Ver resultados de las pruebas abajo.
- **Listado duplicado del dispositivo en la unidad principal** — La página de smartphone de la unidad principal muestra nuestra aplicación como dos entradas separadas (una para Android Auto, una para Bluetooth) en lugar de una única entrada con ambas capacidades. Esto lo causa Android 12+, que bloquea el acceso a la dirección MAC Bluetooth real (devuelve `02:00:00:00:00:00`). Solución: escribir la dirección real en un archivo de configuración mediante `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. Se necesita una pantalla de ajustes en la interfaz para que el usuario pueda introducir su MAC Bluetooth manualmente.
- **Botón de asistente de voz no gestionado** — Cuando el conductor pulsa el botón de voz/asistente en la unidad principal, recibimos una `VOICE_SESSION_REQUEST` e intentamos lanzar un asistente de voz (Dicio o el predeterminado del sistema). Sin embargo, el asistente lanzado aún no recibe audio del micrófono de la unidad principal.
### Resultados de las pruebas de estabilidad de vídeo
Patrón de prueba (barras de color) a 800x480, intervalo de I-frame de 1 segundo:
| FPS | Bitrate | Fragment | Duration | Frames | Status |
|-----|---------|----------|----------|--------|--------|
| 30 | 2Mbps | No | ~3s | ~90 | ❌ Demasiado rápido |
| 15 | 2Mbps | No | ~33s | ~500 | ⚠️ Mejor |
| 10 | 2Mbps | No | ~93s | ~930 | ⚠️ Bueno |
| 30 | 2Mbps | Sí (2KB) | 5-25s | 150-750 | ⚠️ Variable |
| 30 | 500Kbps | Sí (2KB) | ~54s | ~1691 | ⚠️ Mejor |
| 15 | 250Kbps | Sí (2KB) | ~67s+ | 1000+ | ⚠️ Bueno |
| 30 | 250Kbps | Sí (2KB), I=5s | ~20s | ~600 | ❌ Peor con I-frame largo |
| 15 | 250Kbps | No | ~13s | ~200 | ❌ La fragmentación ayudó aquí |
Causa raíz: el búfer de recepción USB de la unidad principal se desborda con un alto rendimiento sostenido. Una tasa de datos más baja = conexión más duradera.
**Mejor configuración confirmada:** 10 fps, 2 Mbps, sin fragmentación = 93 segundos. La implementación de fragmentar antes de cifrar está rota (la unidad principal no puede reensamblar) — necesita más investigación.
## Compilación```bash
./gradlew assembleDebug
Requiere Android SDK con la plataforma 35.
./gradlew testDebugUnitTest
113 pruebas unitarias y de integración que cubren protocolo, tramas, TLS, lógica de canales, máquina de estados de vídeo, manejo de sensores y entrada táctil.
### Pruebas de integración con openauto (Docker)
openauto es un emulador de unidad principal de terceros que habla el protocolo Android Auto completo. Lo usamos para verificar nuestra implementación del protocolo sin necesidad de un automóvil real.
#### Requisitos previos
- Docker instalado y en ejecución
- Teléfono conectado mediante ADB (USB o inalámbrico)
- Aplicación instalada en el teléfono: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Crear la imagen Docker de openauto (una sola vez)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
This builds openauto with all dependencies (Qt5, boost, protobuf, OpenSSL) in a Debian container. Takes ~5 minutes on first build.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto escucha en el puerto 5000 dentro del contenedor, mapeado al puerto 5100 en el host. Se ejecuta en modo headless (no se necesita pantalla).
#### 3. Configurar el reenvío inverso de puertos de ADB```bash
adb reverse tcp:5000 tcp:5100
Esto hace que el localhost:5000 del teléfono se tunelice al localhost:5100 del ordenador (openauto). Nuestra app se conecta a localhost:5000 como cliente TCP cuando no se encuentra un accesorio USB.
adb shell am start -n org.openandroidauto/.MainActivity
La aplicación:
1. Fallará al buscar un accesorio USB
2. Se conectará a `localhost:5000` (openauto mediante adb reverse)
3. Realizará el handshake completo del protocolo (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Abrirá canales (video, audio, input, sensor)
5. Comenzará a transmitir video (patrón de prueba)
#### 5. Verificar en los registros de openauto
Deberías ver en la salida de 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
El archivo de registro persiste en el teléfono entre cambios de USB (útil al probar con una unidad principal de automóvil real).
#### Prueba rápida de una línea```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 para AUTH_COMPLETE — esto es una peculiaridad conocida de openauto, no un errorEl protocolo de Android Auto fue sometido a ingeniería inversa en varios proyectos. Cada uno se basó en el anterior:
Lo que opencardev/aasdk añade sobre AACS:
Diferencias estructurales clave:
priority + channel_id en ChannelOpenRequest; aasdk usa priority (sint32) + service_idEl directorio thirdparty/aasdk/protobuf/ es la referencia de protocolo autoritativa para este proyecto.
Android Auto utiliza TLS mutuo. El teléfono actúa como servidor TLS y debe presentar un certificado firmado por la CA de Google Automotive Link (integrada en el firmware de la unidad principal). Sin la clave privada correcta, la unidad principal rechaza la conexión con AUTH_COMPLETE status=-3.
El teléfono presenta una cadena de 2 certificados:
O=CarService, firmado por la CA de Google Automotive LinkO=Google Automotive Link (válida 2014-2044)Google parece rotar el cert+clave integrado en el APK de Android Auto aproximadamente cada 8 meses (coincidiendo con el período de validez del certificado). Esto podría ser una medida deliberada para limitar la utilidad de las claves extraídas — si una unidad principal comprueba la caducidad del certificado, una clave extraída antigua dejaría de funcionar. Los usuarios de la app oficial reciben certificados nuevos mediante las actualizaciones de la app. Si esta teoría es correcta, un usuario que nunca actualice la app oficial podría, con el tiempo, ser rechazado por las unidades principales que imponen la caducidad. No todas las unidades principales pueden comprobar la caducidad — este comportamiento depende del modelo.
La clave privada está cifrada con AES-256-CBC dentro del APK de Android Auto. La unidad principal valida el certificado del teléfono contra la CA de Google Automotive Link — se acepta cualquier certificado firmado por esa CA.
La clave está integrada (cifrada) en el APK de Android Auto y puede descifrarse usando el propio algoritmo del APK. Esto requiere cualquier dispositivo Android con acceso ADB (sin root, sin Google Play Services) para ejecutar el descifrado, porque el decodificador Base64 de Android se comporta de manera diferente al JVM de escritorio.
Requisitos:
adb pull, o descargar desde APKPure/APKMirror)d8, adb)Proceso:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmEncontrar la clase proveedora de certificados (paso 2):
Los nombres de clase están ofuscados y cambian entre versiones del APK, pero la estructura es siempre la misma. Busca en JADX "-----BEGIN CERTIFICATE-----" — encontrarás una clase pequeña que implementa una interfaz con tres métodos:
a() → devuelve un String (el PEM del certificado CarService)b() → devuelve un byte[] (~1712 bytes — la clave privada cifrada con AES)c() → devuelve un byte[] (256 bytes — la sal KDF)Nombres de clase conocidos por versión:
La función de descifrado está en una clase cercana — busca "AES/CBC/PKCS5Padding" para encontrarla. Toma la interfaz del proveedor de certificados como parámetro.
Nota: El paso de descifrado (paso 4) solo necesita dalvikvm — funciona con cualquier dispositivo Android con ADB, sin necesidad de root ni Google Play Services. El requisito de GApps es solo para el paso 1 (extraer el APK, ya que la app de AA se distribuye a través de Play Store).
Nota: No modificas ni ejecutas el código descompilado del APK. En su lugar, escribes una clase Decrypt.java independiente que reimplementa la lógica de descifrado, lee los arrays de bytes extraídos de archivos y tiene su propio punto de entrada main(). El código fuente descompilado solo se usa como referencia para entender el algoritmo y copiar los arrays de bytes. Consulta tools/decrypt_key_from_apk.md para ver el código fuente completo de Decrypt.java.
Bug crítico de JADX: JADX descompila el helper KDF como byte b = bArr2[i2] & 255; pero debe ser int b = bArr2[i2] & 255;. El tipo byte trunca de vuelta a con signo, produciendo una salida basura. Cámbialo a int y el descifrado funciona.
La función 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]);
}
}
}
Después del descifrado AES, la función `T()` extrae la clave:
- Omitir los primeros 28 bytes, recortar los últimos 26 bytes
- Decodificar en Base64 (URL_SAFE, flag=2) la parte central
- El resultado es una clave privada RSA codificada en PKCS#8 DER
**Nota:** Debe ejecutarse en Android (no en JVM de escritorio) debido a las diferencias entre `android.util.Base64` y `java.util.Base64`. El `Base64.getUrlDecoder()` de la JVM de escritorio rechaza los caracteres base64 estándar (`+`, `/`) y los saltos de línea que el decodificador de Android acepta. Use `Base64.getMimeDecoder()` en escritorio, o ejecute el descifrado en el 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"
This outputs la clave privada PKCS#8 en base64. Envuélvela en cabeceras PEM y colócala en app/src/main/assets/carservice_key.pem.
Consulta tools/decrypt_key_from_apk.md para la guía completa paso a paso.
Dado que algunas unidades principales pueden no comprobar la expiración del certificado, un par cert+key extraído previamente (incluso caducado) puede seguir funcionando. Fuentes:
[email protected] (ver gamelaster/opengal_proxy)KeyFactory.generatePrivate() (requiere tanto root Y Google Play Services en el mismo dispositivo): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Una vez obtenidos, coloca el cert+key en app/src/main/assets/carservice_key.pem.
Consulta tools/dump_key.sh y tools/dump_key_frida.js para los scripts de extracción en tiempo de ejecución.
Este proyecto está licenciado bajo la GNU General Public License v3.0.
Este proyecto incluye definiciones de protocol buffer de aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) como un submódulo de git.
| Proyecto | Año | Archivos Proto | Rol |
|---|
| f1xpl/aasdk | 2018 | 1 monolítico (Wifi.proto) | RE original — protocolo central, video, audio, entrada, sensores |
| AACS | 2020 | 28 (divididos por mensaje) | Implementación del lado del teléfono — cobertura mínima para proyección de video |
| opencardev/aasdk | 2024 | 254 (jerárquicos por servicio) | Referencia definitiva — protocolo completo con todos los servicios |
| Versión del APK | Clase proveedora de certificados | Clase sal+clave | Clase de descifrado |
|---|
| v6.4 | SslWrapper (campos o, p) | misma clase | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (campos b, c) | ivq.d() |