
Implémentation open-source côté téléphone d'Android Auto avec rétro-ingénierie de protocole, authentification mutuelle TLS, projection vidéo H.264, injection de saisie tactile et streaming de données de capteurs via USB AOA.
Une implémentation open-source de l'application côté téléphone d'Android Auto. Cette application s'exécute sur votre téléphone et projette sur l'unité principale de la voiture via USB, remplaçant l'APK propriétaire de Google com.google.android.projection.gearhead.
Ce projet est en début de développement. La poignée de main du protocole et la projection vidéo fonctionnent avec une véritable unité principale. L'écran du téléphone est affiché avec succès sur l'unité principale de la voiture pendant plusieurs secondes avant la déconnexion (la stabilité vidéo est en cours d'amélioration).
UTILISATION À VOS RISQUES ET PÉRILS. Ce logiciel est fourni « en l'état », sans garantie d'aucune sorte.
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)
## Problèmes connus
- **L'attribution des canaux suppose un ordre** — Nous attribuons le premier `av_channel` dans SERVICE_DISCOVERY_RESPONSE comme vidéo et le second comme audio. Cela fonctionne avec l'autoradio (canal 1 = vidéo) mais échoue avec openauto (canal 4 = audio, pas vidéo). Correctif : analyser le champ `stream_type` dans `av_channel` pour distinguer `VIDEO(3)` de `AUDIO(1)`.
- **Stabilité vidéo** — Les connexions tombent après un streaming prolongé en raison d'un débordement du tampon USB de l'autoradio. Voir les résultats des tests ci-dessous.
- **Affichage en double de l'appareil sur l'autoradio** — La page smartphones de l'autoradio affiche notre application comme deux entrées distinctes (une pour Android Auto, une pour Bluetooth) au lieu d'une seule entrée avec les deux capacités. Cela est dû au fait qu'Android 12+ bloque l'accès à la véritable adresse MAC Bluetooth (renvoie `02:00:00:00:00:00`). Solution de contournement : écrire l'adresse réelle dans un fichier de configuration via `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. Besoin d'un écran de paramètres UI pour permettre à l'utilisateur de saisir manuellement son adresse MAC Bluetooth.
- **Bouton d'assistant vocal non géré** — Lorsque le conducteur appuie sur le bouton vocal/assistant de l'autoradio, nous recevons une VOICE_SESSION_REQUEST et tentons de lancer un assistant vocal (Dicio ou celui par défaut du système). Cependant, l'assistant lancé ne reçoit pas encore l'audio du microphone de l'autoradio.
### Résultats des tests de stabilité vidéo
Motif de test (barres de couleur) en 800x480, intervalle d'images I de 1 seconde :
| FPS | Bitrate | Fragment | Duration | Frames | Status |
|-----|---------|----------|----------|--------|--------|
| 30 | 2Mbps | Non | ~3s | ~90 | ❌ Trop rapide |
| 15 | 2Mbps | Non | ~33s | ~500 | ⚠️ Mieux |
| 10 | 2Mbps | Non | ~93s | ~930 | ⚠️ Bon |
| 30 | 2Mbps | Oui (2KB) | 5-25s | 150-750 | ⚠️ Variable |
| 30 | 500Kbps | Oui (2KB) | ~54s | ~1691 | ⚠️ Mieux |
| 15 | 250Kbps | Oui (2KB) | ~67s+ | 1000+ | ⚠️ Bon |
| 30 | 250Kbps | Oui (2KB), I=5s | ~20s | ~600 | ❌ Pire avec long intervalle I |
| 15 | 250Kbps | Non | ~13s | ~200 | ❌ La fragmentation a aidé ici |
Cause profonde : le tampon de réception USB de l'autoradio déborde avec un débit soutenu élevé. Un débit plus bas = une connexion plus longue.
**Meilleure configuration confirmée :** 10 ips, 2 Mbps, pas de fragmentation = 93 secondes. L'implémentation de la fragmentation avant chiffrement est défaillante (l'autoradio ne peut pas réassembler) — nécessite une enquête plus approfondie.
## Compilation```bash
./gradlew assembleDebug
Nécessite SDK Android avec la plateforme 35.
./gradlew testDebugUnitTest
113 tests unitaires et d'intégration couvrant le protocole, le cadrage, TLS, la logique des canaux, la machine d'état vidéo, la gestion des capteurs et l'entrée tactile.
### Tests d'intégration avec openauto (Docker)
openauto est un émulateur de head unit tiers qui parle le protocole complet d'Android Auto. Nous l'utilisons pour vérifier notre implémentation du protocole sans avoir besoin d'une vraie voiture.
#### Prérequis
- Docker installé et en cours d'exécution
- Téléphone connecté via ADB (USB ou sans fil)
- Application installée sur le téléphone : `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Construire l'image Docker openauto (une seule fois)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
Cela construit openauto avec toutes ses dépendances (Qt5, boost, protobuf, OpenSSL) dans un conteneur Debian. Prend ~5 minutes lors du premier build.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto écoute sur le port 5000 à l'intérieur du conteneur, mappé sur le port 5100 sur l'hôte. Il fonctionne en mode headless (aucun affichage nécessaire).
#### 3. Configurer le transfert de port inversé ADB```bash
adb reverse tcp:5000 tcp:5100
Cela fait que le localhost:5000 du téléphone est tunnelisé vers le localhost:5100 (openauto) de l'ordinateur. Notre application se connecte à localhost:5000 en tant que client TCP lorsqu'aucun accessoire USB n'est trouvé.
adb shell am start -n org.openandroidauto/.MainActivity
L'application va :
1. Ne pas trouver un accessoire USB
2. Se connecter à `localhost:5000` (openauto via adb reverse)
3. Effectuer la poignée de main complète du protocole (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Ouvrir les canaux (vidéo, audio, entrée, capteur)
5. Commencer le streaming vidéo (test pattern)
#### 5. Vérifier dans les logs openauto
Vous devriez voir dans la sortie 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
Le fichier journal persiste sur le téléphone entre les commutations USB (utile lors des tests avec un véritable autoradio).
#### Test rapide en une ligne```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 pour AUTH_COMPLETE — c'est un comportement connu d'openauto, pas une erreurLe protocole Android Auto a été rétro-conçu à travers plusieurs projets. Chacun s'appuyait sur le précédent :
Ce qu'opencardev/aasdk ajoute par rapport à AACS :
Différences structurelles clés :
priority + channel_id dans ChannelOpenRequest ; aasdk utilise priority (sint32) + service_idLe répertoire thirdparty/aasdk/protobuf/ est la référence de protocole faisant autorité pour ce projet.
Android Auto utilise TLS mutuelle. Le téléphone agit comme le serveur TLS et doit présenter un certificat signé par l'Autorité de certification Google Automotive Link (intégrée dans le firmware de l'unité principale). Sans la clé privée correcte, l'unité principale rejette la connexion avec le statut AUTH_COMPLETE=-3.
Le téléphone présente une chaîne de 2 certificats :
O=CarService, signé par l'autorité de certification Google Automotive LinkO=Google Automotive Link (valide 2014-2044)Google semble faire pivoter le certificat+clé intégré dans l'APK Android Auto environ tous les 8 mois (correspondant à la période de validité du certificat). Cela pourrait être une mesure délibérée pour limiter l'utilité des clés extraites — si une unité principale vérifie l'expiration du certificat, une ancienne clé extraite cesserait de fonctionner. Les utilisateurs de l'application officielle reçoivent de nouveaux certificats via les mises à jour de l'application. Si cette théorie est correcte, un utilisateur qui ne met jamais à jour l'application officielle pourrait être rejeté par les unités principales qui appliquent l'expiration. Toutes les unités principales ne vérifient peut-être pas l'expiration — ce comportement dépend du modèle.
La clé privée est chiffrée en AES-256-CBC à l'intérieur de l'APK Android Auto. L'unité principale valide le certificat du téléphone par rapport à l'autorité de certification Google Automotive Link — tout certificat signé par cette autorité est accepté.
La clé est intégrée (chiffrée) dans l'APK Android Auto et peut être déchiffrée en utilisant le propre algorithme de l'APK. Cela nécessite n'importe quel appareil Android avec accès ADB (pas de root, pas de Google Play Services nécessaires) pour exécuter le déchiffrement, car le décodeur Base64 d'Android se comporte différemment de la JVM de bureau.
Prérequis :
adb pull, ou téléchargez depuis APKPure/APKMirror)d8, adb)Processus :
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmTrouver la classe de fournisseur de certificat (étape 2) :
Les noms de classe sont obscurcis et changent entre les versions de l'APK, mais la structure est toujours la même. Recherchez dans JADX "-----BEGIN CERTIFICATE-----" — vous trouverez une petite classe implémentant une interface avec trois méthodes :
a() → retourne une String (le PEM du certificat CarService)b() → retourne un byte[] (~1712 octets — la clé privée chiffrée par AES)c() → retourne un byte[] (256 octets — le sel KDF)Noms de classe connus par version :
La fonction de déchiffrement se trouve dans une classe voisine — recherchez "AES/CBC/PKCS5Padding" pour la trouver. Elle prend l'interface fournisseur de certificat comme paramètre.
Remarque : L'étape de déchiffrement (étape 4) nécessite seulement dalvikvm — tout appareil Android avec ADB fonctionne, pas de root ni de Google Play Services requis. L'exigence de GApps n'est que pour l'étape 1 (récupération de l'APK, puisque l'application AA est distribuée via le Play Store).
Remarque : Vous ne modifiez ni n'exécutez le code APK décompilé. Au lieu de cela, vous écrivez une classe Decrypt.java autonome qui réimplémente la logique de déchiffrement, lit les tableaux d'octets extraits à partir de fichiers, et possède son propre point d'entrée main(). Le code source décompilé est seulement utilisé comme référence pour comprendre l'algorithme et copier les tableaux d'octets. Voir tools/decrypt_key_from_apk.md pour le code source complet de Decrypt.java.
Bogue critique de JADX : JADX décompile l'assistant KDF comme byte b = bArr2[i2] & 255; mais cela doit être int b = bArr2[i2] & 255;. Le type byte tronque en signé, produisant une sortie erronée. Corrigez cela en int et le déchiffrement fonctionne.
La fonction 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]);
}
}
}
Après le déchiffrement AES, la fonction `T()` extrait la clé :
- Ignorer les 28 premiers octets, supprimer les 26 derniers octets
- Décoder en Base64 (URL_SAFE, flag=2) la partie centrale
- Le résultat est une clé privée RSA encodée en PKCS#8 DER
**Remarque :** Doit être exécuté sur Android (pas sur JVM de bureau) en raison des différences entre `android.util.Base64` et `java.util.Base64`. Le `Base64.getUrlDecoder()` de la JVM de bureau rejette les caractères base64 standard (`+`, `/`) et les sauts de ligne que le décodeur Android accepte. Utilisez `Base64.getMimeDecoder()` sur le bureau, ou exécutez le déchiffrement sur l'appareil avec `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"
Ceci génère la clé privée PKCS#8 en base64. Entourez-la des en-têtes PEM et placez-la dans app/src/main/assets/carservice_key.pem.
Voir tools/decrypt_key_from_apk.md pour le guide complet étape par étape.
Étant donné que certaines unités principales peuvent ne pas vérifier l'expiration du certificat, une paire certificat+clé extraite précédemment (même expirée) peut toujours fonctionner. Sources :
[email protected] (voir gamelaster/opengal_proxy)KeyFactory.generatePrivate() (nécessite à la fois root ET Google Play Services sur le même appareil) : ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Une fois obtenus, placez le certificat+clé dans app/src/main/assets/carservice_key.pem.
Voir tools/dump_key.sh et tools/dump_key_frida.js pour les scripts d'extraction en runtime.
Ce projet est sous licence GNU General Public License v3.0.
Ce projet inclut des définitions de protocole buffer provenant de aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) en tant que sous-module git.
| Projet | Année | Fichiers Proto | Rôle |
|---|
| f1xpl/aasdk | 2018 | 1 monolithique (Wifi.proto) | RE original — protocole de base, vidéo, audio, entrée, capteurs |
| AACS | 2020 | 28 (répartis par message) | Implémentation côté téléphone — couverture minimale pour la projection vidéo |
| opencardev/aasdk | 2024 | 254 (hiérarchiques par service) | Référence définitive — protocole complet avec tous les services |
| Version APK | Classe fournisseur de certificat | Classe sel+clé | Classe de déchiffrement |
|---|
| v6.4 | SslWrapper (champs o, p) | même classe | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (champs b, c) | ivq.d() |