
Реализация Android Auto на стороне телефона с открытым исходным кодом: реверс-инжиниринг протокола, взаимная аутентификация TLS, проекция видео H.264, внедрение сенсорного ввода и потоковая передача данных датчиков через USB AOA.
Открытая реализация приложения Android Auto со стороны телефона. Это приложение запускается на вашем телефоне и проецирует изображение на головное устройство автомобиля через USB, заменяя проприетарный APK Google com.google.android.projection.gearhead.
Этот проект находится на ранней стадии разработки. Рукопожатие протокола и видеопроекция работают с реальным головным устройством. Экран телефона успешно отображается на головном устройстве автомобиля в течение нескольких секунд до отключения (стабильность видео улучшается).
ИСПОЛЬЗУЙТЕ НА СВОЙ СТРАХ И РИСК. Данное программное обеспечение предоставляется «как есть», без каких-либо гарантий.
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` в SERVICE_DISCOVERY_RESPONSE как видео, а второй как аудио. Это работает с головным устройством автомобиля (канал 1 = видео), но не работает с openauto (канал 4 = аудио, не видео). Исправление: анализировать поле `stream_type` внутри `av_channel`, чтобы различать `VIDEO(3)` и `AUDIO(1)`.
- **Стабильность видео** — Соединение разрывается после продолжительной трансляции из-за переполнения USB-буфера головного устройства. См. результаты тестов ниже.
- **Дублирование устройства в списке на головном устройстве** — На странице смартфонов головного устройства наше приложение отображается как две отдельные записи (одна для Android Auto, одна для Bluetooth) вместо одной записи с обеими возможностями. Это вызвано тем, что Android 12+ блокирует доступ к реальному MAC-адресу Bluetooth (возвращает `02:00:00:00:00:00`). Обходной путь: запишите реальный адрес в файл конфигурации через `adb shell "echo $(adb shell settings get secure bluetooth_address) > /sdcard/Android/data/org.openandroidauto/files/bt_address.txt"`. Нужен экран настроек в UI, чтобы пользователь мог вручную ввести свой BT MAC.
- **Кнопка голосового ассистента не обрабатывается** — Когда водитель нажимает кнопку голоса/ассистента на головном устройстве, мы получаем VOICE_SESSION_REQUEST и пытаемся запустить голосовой ассистент (Dicio или системный по умолчанию). Однако запущенный ассистент пока не получает звук с микрофона головного устройства.
### Результаты тестирования стабильности видео
Тестовый паттерн (цветные полосы) при разрешении 800x480, интервал I-кадров 1 секунда:
| FPS | Битрейт | Фрагментация | Длительность | Кадры | Статус |
|-----|---------|----------|----------|--------|--------|
| 30 | 2Mbps | Нет | ~3s | ~90 | ❌ Слишком быстро |
| 15 | 2Mbps | Нет | ~33s | ~500 | ⚠️ Лучше |
| 10 | 2Mbps | Нет | ~93s | ~930 | ⚠️ Хорошо |
| 30 | 2Mbps | Да (2KB) | 5-25s | 150-750 | ⚠️ Нестабильно |
| 30 | 500Kbps | Да (2KB) | ~54s | ~1691 | ⚠️ Лучше |
| 15 | 250Kbps | Да (2KB) | ~67s+ | 1000+ | ⚠️ Хорошо |
| 30 | 250Kbps | Да (2KB), I=5s | ~20s | ~600 | ❌ Хуже с длинным I-кадром |
| 15 | 250Kbps | Нет | ~13s | ~200 | ❌ Здесь помогла фрагментация |
Основная причина: при длительном высоком потоке данных переполняется приёмный USB-буфер головного устройства. Чем ниже скорость передачи данных, тем дольше соединение.
**Лучшая подтверждённая конфигурация:** 10fps, 2Mbps, без фрагментации = 93 секунды. Реализация фрагментации перед шифрованием не работает (головное устройство не может собрать фрагменты) — требует дальнейшего изучения.
## Сборка```bash
./gradlew assembleDebug
Требуется Android SDK с платформой 35.
./gradlew testDebugUnitTest
113 модульных и интеграционных тестов, покрывающих протокол, фрейминг, TLS, логику каналов, конечный автомат видео, обработку датчиков и сенсорный ввод.
### Интеграционное тестирование с openauto (Docker)
openauto — это сторонний эмулятор головного устройства, поддерживающий полный протокол Android Auto. Мы используем его для проверки нашей реализации протокола без необходимости в реальном автомобиле.
#### Предварительные требования
- Установленный и запущенный Docker
- Телефон, подключённый через ADB (USB или беспроводно)
- Приложение установлено на телефон: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. Создание Docker-образа openauto (однократно)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
Это собирает openauto со всеми зависимостями (Qt5, boost, protobuf, OpenSSL) в контейнере Debian. При первой сборке занимает ~5 минут.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto прослушивает порт 5000 внутри контейнера, проброшенный на порт 5100 на хосте. Он работает в headless-режиме (без необходимости в дисплее).
#### 3. Настройка обратного проброса портов ADB```bash
adb reverse tcp:5000 tcp:5100
Это создаёт туннель с localhost:5000 на телефоне на localhost:5100 на компьютере (openauto). Наше приложение подключается к localhost:5000 как TCP-клиент, когда USB-аксессуар не найден.
adb shell am start -n org.openandroidauto/.MainActivity
Приложение будет:
1. Не сможет найти USB-аксессуар
2. Подключится к `localhost:5000` (openauto через adb reverse)
3. Выполнит полное рукопожатие протокола (VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. Откроет каналы (video, audio, input, sensor)
5. Начнёт потоковую передачу видео (тестовый шаблон)
#### 5. Проверка в логах openauto
В выводе 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
Журнал сохраняется на телефоне между переключениями USB (полезно при тестировании с реальным головным устройством автомобиля).
#### Быстрый однострочный тест```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 для AUTH_COMPLETE — это известная особенность openauto, а не ошибкаПротокол Android Auto был реверс-инженерен в рамках нескольких проектов. Каждый основывался на предыдущем:
Что opencardev/aasdk добавляет поверх AACS:
Ключевые структурные отличия:
priority + channel_id в ChannelOpenRequest; aasdk использует priority (sint32) + service_idКаталог thirdparty/aasdk/protobuf/ является авторитетным справочником по протоколу для этого проекта.
Android Auto использует взаимный TLS. Телефон выступает в роли TLS сервера и должен предъявить сертификат, подписанный Google Automotive Link CA (встроенным в прошивку головного устройства). Без правильного закрытого ключа головное устройство отклоняет соединение с AUTH_COMPLETE status=-3.
Телефон предъявляет цепочку из 2 сертификатов:
O=CarService, подписан Google Automotive Link CAO=Google Automotive Link (действителен с 2014 по 2044)Похоже, Google ротирует сертификат+ключ, встроенные в APK Android Auto, примерно каждые 8 месяцев (совпадает с периодом действия сертификата). Это может быть намеренной мерой для ограничения полезности извлечённых ключей — если головное устройство проверяет срок действия сертификата, старый извлечённый ключ перестанет работать. Пользователи официального приложения получают свежие сертификаты через обновления приложения. Если эта теория верна, пользователь, который никогда не обновляет официальное приложение, может в конечном итоге быть отклонён головными устройствами, которые enforce истечение срока. Не все головные устройства могут проверять срок действия — это поведение зависит от модели.
Закрытый ключ зашифрован с помощью AES-256-CBC внутри APK Android Auto. Головное устройство проверяет сертификат телефона относительно Google Automotive Link CA — принимается любой сертификат, подписанный этим CA.
Ключ встроен (зашифрован) в APK Android Auto и может быть расшифрован с использованием собственного алгоритма APK. Для этого требуется любое Android-устройство с доступом по ADB (root не нужен, Google Play Services не требуются) для выполнения расшифровки, потому что декодер Base64 в Android ведёт себя иначе, чем десктопная JVM.
Requirements:
adb pull или скачайте с APKPure/APKMirror)d8, 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):
Имена классов обфусцированы и меняются между версиями APK, но структура всегда одинакова. Найдите в JADX "-----BEGIN CERTIFICATE-----" — вы найдёте небольшой класс, реализующий интерфейс с тремя методами:
a() → возвращает String (PEM сертификата CarService)b() → возвращает byte[] (~1712 байт — зашифрованный AES закрытый ключ)c() → возвращает byte[] (256 байт — соль KDF)Известные имена классов по версиям:
Функция расшифровки находится в соседнем классе — ищите "AES/CBC/PKCS5Padding", чтобы найти её. Она принимает интерфейс провайдера сертификатов в качестве параметра.
Note: Шаг расшифровки (шаг 4) требует только dalvikvm — подойдёт любое Android-устройство с ADB, root или Google Play Services не нужны. Требование GApps относится только к шагу 1 (получение APK, так как приложение AA распространяется через Play Store).
Note: Вы не модифицируете и не запускаете декомпилированный код APK. Вместо этого вы пишете отдельный класс Decrypt.java, который заново реализует логику расшифровки, читает извлечённые байтовые массивы из файлов и имеет собственную точку входа main(). Декомпилированный исходник используется только как справочник для понимания алгоритма и копирования байтовых массивов. См. tools/decrypt_key_from_apk.md для полного исходника Decrypt.java.
Critical JADX bug: JADX декомпилирует KDF-хелпер как byte b = bArr2[i2] & 255;, но должно быть int b = bArr2[i2] & 255;. Тип byte обрезает обратно до знакового значения, давая мусорный вывод. Исправьте на int — и расшифровка заработает.
Функция 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]);
}
}
}
После расшифровки AES функция `T()` извлекает ключ:
- Пропустить первые 28 байт, отсечь последние 26 байт
- Декодировать среднюю часть из Base64 (URL_SAFE, flag=2)
- Результат — закрытый ключ RSA, закодированный в PKCS#8 DER
**Примечание:** необходимо запускать на Android (не на десктопной JVM) из-за различий между `android.util.Base64` и `java.util.Base64`. `Base64.getUrlDecoder()` десктопной JVM отклоняет стандартные символы base64 (`+`, `/`) и переводы строк, которые принимает декодер Android. На десктопе используйте `Base64.getMimeDecoder()`, либо запустите расшифровку на устройстве через `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"
Это выводит закрытый ключ PKCS#8 в формате base64. Оберните его в заголовки PEM и поместите в app/src/main/assets/carservice_key.pem.
См. tools/decrypt_key_from_apk.md для получения полного пошагового руководства.
Поскольку некоторые головные устройства могут не проверять срок действия сертификата, ранее извлечённая пара сертификат+ключ (даже просроченная) всё ещё может работать. Источники:
[email protected] (см. gamelaster/opengal_proxy)KeyFactory.generatePrivate() (требуются и root, и Google Play Services на одном устройстве): ```bash
frida -U -n "com.google.android.projection.gearhead" -l tools/dump_key_frida.js
После получения разместите сертификат+ключ в app/src/main/assets/carservice_key.pem.
См. tools/dump_key.sh и tools/dump_key_frida.js — скрипты для извлечения во время выполнения.
Этот проект распространяется под лицензией GNU General Public License v3.0.
Этот проект включает определения protocol buffer из aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) в качестве git-подмодуля.
| Project | Year | Proto Files | Role |
|---|
| f1xpl/aasdk | 2018 | 1 монолитный (Wifi.proto) | Оригинальный RE — ядро протокола, видео, аудио, ввод, сенсоры |
| AACS | 2020 | 28 (разбит по сообщениям) | Реализация на стороне телефона — минимальное покрытие для видеопроекции |
| opencardev/aasdk | 2024 | 254 (иерархически по сервисам) | Эталонный справочник — полный протокол со всеми сервисами |
| APK Version | Cert provider class | Salt+key class | Decryption class |
|---|
| v6.4 | SslWrapper (поля o, p) | тот же класс | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (поля b, c) | ivq.d() |