
Implementação de código aberto do lado do telefone do Android Auto com engenharia reversa de protocolo, autenticação mútua TLS, projeção de vídeo H.264, injeção de entrada por toque e streaming de dados de sensores via USB AOA.
Uma implementação open-source do aplicativo lado do telefone do Android Auto. Este aplicativo é executado no seu telefone e projeta para a unidade principal do carro via USB, substituindo o APK proprietário do Google com.google.android.projection.gearhead.
Este projeto está em desenvolvimento inicial. O handshake do protocolo e a projeção de vídeo estão funcionando com uma unidade principal real. A tela do telefone é exibida com sucesso na unidade principal do carro por vários segundos antes de desconectar (a estabilidade do vídeo está sendo melhorada).
USE POR SUA CONTA E RISCO. Este software é fornecido "como está", sem garantia de qualquer 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)
## Bugs Conhecidos
- **Atribuição de canal assume ordem** — Atribuímos o primeiro `av_channel` na SERVICE_DISCOVERY_RESPONSE como vídeo e o segundo como áudio. Isso funciona com a unidade principal do carro (canal 1 = vídeo), mas falha com openauto (canal 4 = áudio, não vídeo). Correção: analisar o campo `stream_type` dentro de `av_channel` para distinguir `VIDEO(3)` de `AUDIO(1)`.
- **Estabilidade de vídeo** — A conexão cai após streaming prolongado devido a estouro de buffer USB da unidade principal. Veja os resultados dos testes abaixo.
- **Listagem duplicada de dispositivo na unidade principal** — A página de smartphone da unidade principal mostra nosso aplicativo como duas entradas separadas (uma para Android Auto, uma para Bluetooth) em vez de uma única entrada com ambas as capacidades. Isso é causado pelo Android 12+ bloquear o acesso ao endereço MAC Bluetooth real (retorna `02:00:00:00:00:00`). Solução alternativa: escrever o endereço real em um arquivo de configuração via `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. Precisa de uma tela de configurações na interface para permitir que o usuário insira seu MAC BT manualmente.
- **Botão de assistente de voz não tratado** — Quando o motorista pressiona o botão de voz/assistente na unidade principal, recebemos uma VOICE_SESSION_REQUEST e tentamos iniciar um assistente de voz (Dicio ou padrão do sistema). No entanto, o assistente iniciado ainda não recebe áudio do microfone da unidade principal.
### Resultados dos Testes de Estabilidade de Vídeo
Padrão de teste (barras coloridas) a 800x480, intervalo de quadro I de 1 segundo:
| FPS | Bitrate | Fragmento | Duração | Quadros | Status |
|-----|---------|-----------|---------|---------|--------|
| 30 | 2Mbps | Não | ~3s | ~90 | ❌ Muito rápido |
| 15 | 2Mbps | Não | ~33s | ~500 | ⚠️ Melhor |
| 10 | 2Mbps | Não | ~93s | ~930 | ⚠️ Bom |
| 30 | 2Mbps | Sim (2KB) | 5-25s | 150-750 | ⚠️ Variável |
| 30 | 500Kbps | Sim (2KB) | ~54s | ~1691 | ⚠️ Melhor |
| 15 | 250Kbps | Sim (2KB) | ~67s+ | 1000+ | ⚠️ Bom |
| 30 | 250Kbps | Sim (2KB), I=5s | ~20s | ~600 | ❌ Pior com quadro I longo |
| 15 | 250Kbps | Não | ~13s | ~200 | ❌ Fragmentação ajudou aqui |
Causa raiz: O buffer de recebimento USB da unidade principal transborda com alta taxa de transferência sustentada. Taxa de dados mais baixa = conexão mais longa.
**Melhor configuração confirmada:** 10fps, 2Mbps, sem fragmentação = 93 segundos. A implementação de fragmentar-antes-de-criptografar está quebrada (unidade principal não consegue remontar) — precisa de mais investigação.
## Compilação```bash
./gradlew assembleDebug
Requer Android SDK com a plataforma 35.
./gradlew testDebugUnitTest
113 testes unitários e de integração cobrindo protocolo, framing, TLS, lógica de canal, máquina de estado de vídeo, manipulação de sensores e entrada por toque.
### Testes de Integração com openauto (Docker)
openauto é um emulador de unidade principal de terceiros que implementa o protocolo completo do Android Auto. Nós o usamos para verificar nossa implementação do protocolo sem precisar de um carro real.
#### Pré-requisitos
- Docker instalado e em execução
- Telefone conectado via ADB (USB ou sem fio)
- Aplicativo instalado no telefone: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Construir a imagem Docker do openauto (uma vez)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
Isto constrói o openauto com todas as dependências (Qt5, boost, protobuf, OpenSSL) em um container Debian. Leva ~5 minutos na primeira compilação.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto escuta na porta 5000 dentro do container, mapeada para a porta 5100 no host. Executa em modo headless (nenhum display necessário).
#### 3. Configurar redirecionamento reverso de portas ADB```bash
adb reverse tcp:5000 tcp:5100
Isso faz com que o localhost:5000 do telefone faça túnel para o localhost:5100 do computador (openauto). Nosso app se conecta ao localhost:5000 como um cliente TCP quando nenhum acessório USB é encontrado.
adb shell am start -n org.openandroidauto/.MainActivity
O aplicativo irá:
1. Falhar ao encontrar um acessório USB
2. Conectar a `localhost:5000` (openauto via adb reverse)
3. Realizar o handshake completo do protocolo (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Abrir canais (vídeo, áudio, entrada, sensor)
5. Iniciar streaming de vídeo (padrão de teste)
#### 5. Verificar nos logs do openauto
Você deve ver na saída do 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
O ficheiro de log persiste no telefone entre as mudanças de USB (útil quando se testa com uma unidade de head unit real).
#### Teste rápido de uma linha```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 — esta é uma peculiaridade conhecida do openauto, não um erroO protocolo Android Auto foi engenharia reversa ao longo de vários projetos. Cada um baseou-se no anterior:
O que o opencardev/aasdk adiciona em relação ao AACS:
Principais diferenças estruturais:
priority + channel_id em ChannelOpenRequest; aasdk usa priority (sint32) + service_idO diretório thirdparty/aasdk/protobuf/ é a referência do protocolo oficial para este projeto.
O Android Auto usa TLS mútuo. O telefone atua como servidor TLS e deve apresentar um certificado assinado pela Autoridade de Certificação Google Automotive Link (incorporada no firmware da head unit). Sem a chave privada correta, a head unit rejeita a ligação com AUTH_COMPLETE status=-3.
O telefone apresenta uma cadeia de 2 certificados:
O=CarService, assinado pela Autoridade de Certificação Google Automotive LinkO=Google Automotive Link (válida 2014-2044)O Google parece rodar o certificado+chave incorporados no APK do Android Auto aproximadamente a cada 8 meses (correspondendo ao período de validade do certificado). Isto pode ser uma medida deliberada para limitar a utilidade de chaves extraídas — se uma head unit verificar a expiração do certificado, uma chave extraída antiga deixaria de funcionar. Os utilizadores da aplicação oficial recebem novos certificados através de atualizações da aplicação. Se esta teoria estiver correta, um utilizador que nunca atualize a aplicação oficial pode eventualmente ser rejeitado por head units que imponham a expiração. Nem todas as head units podem verificar a expiração — este comportamento depende do modelo.
A chave privada é encriptada com AES-256-CBC dentro do APK do Android Auto. A head unit valida o certificado do telefone contra a Autoridade de Certificação Google Automotive Link — qualquer certificado assinado por essa AC é aceite.
A chave está incorporada (encriptada) no APK do Android Auto e pode ser desencriptada usando o próprio algoritmo do APK. Isto requer qualquer dispositivo Android com acesso ADB (sem root, sem Google Play Services necessário) para executar a desencriptação, porque o descodificador Base64 do Android comporta-se de forma diferente do JVM de ambiente de trabalho.
Requisitos:
adb pull, ou descarregar do APKPure/APKMirror)d8 build tool, adb)Processo:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmEncontrar a classe do fornecedor de certificado (passo 2):
Os nomes das classes são ofuscados e mudam entre versões do APK, mas a estrutura é sempre a mesma. Procure no JADX por "-----BEGIN CERTIFICATE-----" — encontrará uma classe pequena que implementa uma interface com três métodos:
a() → devolve uma String (o PEM do certificado CarService)b() → devolve um byte[] (~1712 bytes — a chave privada encriptada AES)c() → devolve um byte[] (256 bytes — o salt KDF)Nomes de classe conhecidos por versão:
A função de desencriptação está numa classe próxima — procure por "AES/CBC/PKCS5Padding" para a encontrar. Ela recebe a interface do fornecedor de certificado como parâmetro.
Nota: O passo de desencriptação (passo 4) só precisa de dalvikvm — qualquer dispositivo Android com ADB funciona, sem root ou Google Play Services. O requisito de GApps é apenas para o passo 1 (obter o APK, pois a aplicação AA é distribuída via Play Store).
Nota: Não se modifica ou executa o código descompilado do APK. Em vez disso, escreve-se uma classe Decrypt.java autónoma que reimplementa a lógica de desencriptação, lê os arrays de bytes extraídos de ficheiros e tem o seu próprio ponto de entrada main(). O código-fonte descompilado é usado apenas como referência para entender o algoritmo e copiar os arrays de bytes. Consulte tools/decrypt_key_from_apk.md para o código Decrypt.java completo.
Bug crítico do JADX: O JADX descompila o helper KDF como byte b = bArr2[i2] & 255; mas deve ser int b = bArr2[i2] & 255;. O tipo byte trunca de volta para signed, produzindo saída inválida. Corrija para int e a desencriptação funciona.
A função 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]);
}
}
}
Após a descriptografia AES, a função `T()` extrai a chave:
- Pular os primeiros 28 bytes, cortar os últimos 26 bytes
- Decodificar em Base64 (URL_SAFE, flag=2) a parte do meio
- O resultado é uma chave privada RSA codificada em PKCS#8 DER
**Nota:** Deve ser executado no Android (não no JVM de desktop) devido às diferenças entre `android.util.Base64` e `java.util.Base64`. O `Base64.getUrlDecoder()` do JVM de desktop rejeita caracteres base64 padrão (`+`, `/`) e novas linhas que o decodificador do Android aceita. Use `Base64.getMimeDecoder()` no desktop, ou execute a descriptografia no dispositivo com `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"
Isso gera a chave privada PKCS#8 em base64. Envolva-a em cabeçalhos PEM e coloque em app/src/main/assets/carservice_key.pem.
Veja tools/decrypt_key_from_apk.md para o guia completo passo a passo.
Como algumas unidades principais podem não verificar a expiração do certificado, um par certificado+chave extraído anteriormente (mesmo expirado) ainda pode funcionar. Fontes:
[email protected] (veja gamelaster/opengal_proxy)KeyFactory.generatePrivate() (requer tanto root quanto os Google Play Services no mesmo dispositivo): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Uma vez obtido, coloque o cert+key em app/src/main/assets/carservice_key.pem.
Veja tools/dump_key.sh e tools/dump_key_frida.js para scripts de extração em tempo de execução.
Este projeto está licenciado sob a GNU General Public License v3.0.
Este projeto inclui definições de buffer de protocolo de aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) como um submódulo git.
| Projeto | Ano | Ficheiros Proto | Função |
|---|
| f1xpl/aasdk | 2018 | 1 monolítico (Wifi.proto) | RE original — protocolo base, vídeo, áudio, entrada, sensores |
| AACS | 2020 | 28 (divididos por mensagem) | Implementação do lado do telefone — cobertura mínima para projeção de vídeo |
| opencardev/aasdk | 2024 | 254 (hierárquicos por serviço) | Referência definitiva — protocolo completo com todos os serviços |
| Versão APK | Classe fornecedora de certificado | Classe salt+chave | Classe de desencriptação |
|---|
| v6.4 | SslWrapper (campos o, p) | mesma classe | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (campos b, c) | ivq.d() |