
Open-source Android Auto phone-side implementation with protocol reverse engineering, TLS mutual authentication, H.264 video projection, touch input injection, and sensor data streaming over USB AOA.
An open-source implementation of the Android Auto phone-side app. This app runs on your phone and projects to a car's head unit over USB, replacing Google's proprietary com.google.android.projection.gearhead APK.
This project is in early development. The protocol handshake and video projection are working with a real head unit. The phone screen is successfully displayed on the car's head unit for several seconds before disconnecting (video stability is being improved).
USE AT YOUR OWN RISK. This software is provided "as is", without warranty of any kind.
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)
av_channel in SERVICE_DISCOVERY_RESPONSE as video and the second as audio. This works with the car head unit (channel 1 = video) but fails with openauto (channel 4 = audio, not video). Fix: parse the stream_type field inside av_channel to distinguish VIDEO(3) from AUDIO(1).02:00:00:00:00:00). Workaround: write the real address to a config file via adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt". Need a UI settings screen to let the user enter their BT MAC manually.Test pattern (color bars) at 800x480, I-frame interval 1 second:
Root cause: Head unit USB receive buffer overflows with sustained high throughput. Lower data rate = longer connection.
Best confirmed config: 10fps, 2Mbps, no fragmentation = 93 seconds. Fragment-before-encrypt implementation is broken (head unit can't reassemble) — needs further investigation.
./gradlew assembleDebug
Requires Android SDK with platform 35.
./gradlew testDebugUnitTest
113 unit and integration tests covering protocol, framing, TLS, channel logic, video state machine, sensor handling, and touch input.
openauto is a third-party head unit emulator that speaks the full Android Auto protocol. We use it to verify our protocol implementation without needing a real car.
./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apkcd 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 listens on port 5000 inside the container, mapped to port 5100 on the host. It runs in headless mode (no display needed).
adb reverse tcp:5000 tcp:5100
This makes the phone's localhost:5000 tunnel to the computer's localhost:5100 (openauto). Our app connects to localhost:5000 as a TCP client when no USB accessory is found.
adb shell am start -n org.openandroidauto/.MainActivity
The app will:
localhost:5000 (openauto via adb reverse)You should see in the openauto output:
[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
The log file persists on the phone between USB switches (useful when testing with a real car head unit).
# 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 for AUTH_COMPLETE — this is a known openauto quirk, not an errorThe Android Auto protocol was reverse-engineered across several projects. Each built on the previous:
What opencardev/aasdk adds over AACS:
Key structural differences:
priority + channel_id in ChannelOpenRequest; aasdk uses priority (sint32) + service_idThe thirdparty/aasdk/protobuf/ directory is the authoritative protocol reference for this project.
Android Auto uses mutual TLS. The phone acts as the TLS server and must present a certificate signed by the Google Automotive Link CA (baked into head unit firmware). Without the correct private key, the head unit rejects the connection with AUTH_COMPLETE status=-3.
The phone presents a 2-cert chain:
O=CarService, signed by the Google Automotive Link CAO=Google Automotive Link (valid 2014-2044)Google appears to rotate the cert+key embedded in the Android Auto APK approximately every 8 months (matching the cert's validity period). This may be a deliberate measure to limit the usefulness of extracted keys — if a head unit checks certificate expiry, an old extracted key would stop working. Users of the official app receive fresh certs via app updates. If this theory is correct, a user who never updates the official app could eventually be rejected by head units that enforce expiry. Not all head units may check expiry — this behaviour is model-dependent.
The private key is AES-256-CBC encrypted inside the Android Auto APK. The head unit validates the phone's cert against the Google Automotive Link CA — any cert signed by that CA is accepted.
The key is embedded (encrypted) in the Android Auto APK and can be decrypted using the APK's own algorithm. This requires any Android device with ADB access (no root, no Google Play Services needed) to run the decryption, because Android's Base64 decoder behaves differently from the desktop JVM.
Requirements:
adb pull, or download from APKPure/APKMirror)d8 build tool, adb)Process:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvmFinding the cert provider class (step 2):
The class names are obfuscated and change between APK versions, but the structure is always the same. Search JADX for "-----BEGIN CERTIFICATE-----" — you'll find a small class implementing an interface with three methods:
a() → returns a String (the CarService cert PEM)b() → returns a byte[] (~1712 bytes — the AES-encrypted private key)c() → returns a byte[] (256 bytes — the KDF salt)Known class names by version:
The decryption function is in a nearby class — search for "AES/CBC/PKCS5Padding" to find it. It takes the cert provider interface as a parameter.
Note: The decryption step (step 4) only needs dalvikvm — any Android device with ADB works, no root or Google Play Services required. The GApps requirement is only for step 1 (pulling the APK, since the AA app is distributed via Play Store).
Note: You don't modify or run the decompiled APK code. Instead, you write a standalone Decrypt.java class that reimplements the decryption logic, reads the extracted byte arrays from files, and has its own main() entry point. The decompiled source is only used as a reference to understand the algorithm and copy out the byte arrays. See tools/decrypt_key_from_apk.md for the complete Decrypt.java source.
Critical JADX bug: JADX decompiles the KDF helper as byte b = bArr2[i2] & 255; but it must be int b = bArr2[i2] & 255;. The byte type truncates back to signed, producing garbage output. Fix this to int and the decryption works.
The KDF (tweakBytes/ap) function:
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]);
}
}
}
After AES decryption, the T() function extracts the key:
Note: Must run on Android (not desktop JVM) due to android.util.Base64 vs java.util.Base64 differences. The desktop JVM's Base64.getUrlDecoder() rejects standard base64 characters (+, /) and newlines that Android's decoder accepts. Use Base64.getMimeDecoder() on desktop, or run the decryption on-device with dalvikvm:
# 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.
Since some head units may not check certificate expiry, a previously extracted cert+key pair (even expired) may still work. Sources:
[email protected] (see gamelaster/opengal_proxy)KeyFactory.generatePrivate() (requires both root AND Google Play Services on the same device):
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
Once obtained, place the cert+key at app/src/main/assets/carservice_key.pem.
See tools/dump_key.sh and tools/dump_key_frida.js for runtime extraction scripts.
This project is licensed under the GNU General Public License v3.0.
This project includes protocol buffer definitions from aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) as a git submodule.
| FPS | Bitrate | Fragment | Duration | Frames | Status |
|---|
| 30 | 2Mbps | No | ~3s | ~90 | ❌ Too fast |
| 15 | 2Mbps | No | ~33s | ~500 | ⚠️ Better |
| 10 | 2Mbps | No | ~93s | ~930 | ⚠️ Good |
| 30 | 2Mbps | Yes (2KB) | 5-25s | 150-750 | ⚠️ Variable |
| 30 | 500Kbps | Yes (2KB) | ~54s | ~1691 | ⚠️ Better |
| 15 | 250Kbps | Yes (2KB) | ~67s+ | 1000+ | ⚠️ Good |
| 30 | 250Kbps | Yes (2KB), I=5s | ~20s | ~600 | ❌ Worse with long I-frame |
| 15 | 250Kbps | No | ~13s | ~200 | ❌ Fragmentation helped here |
| Project | Year | Proto Files | Role |
|---|
| f1xpl/aasdk | 2018 | 1 monolithic (Wifi.proto) | Original RE — core protocol, video, audio, input, sensors |
| AACS | 2020 | 28 (split by message) | Phone-side impl — minimal coverage for video projection |
| opencardev/aasdk | 2024 | 254 (hierarchical by service) | Definitive reference — full protocol with all services |
| APK Version | Cert provider class | Salt+key class | Decryption class |
|---|
| v6.4 | SslWrapper (fields o, p) | same class | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (fields b, c) | ivq.d() |