
프로토콜 리버스 엔지니어링, TLS 상호 인증, H.264 비디오 프로젝션, 터치 입력 주입 및 USB AOA를 통한 센서 데이터 스트리밍을 갖춘 오픈소스 Android Auto 휴대폰 측 구현
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.
이 프로젝트는 초기 개발 단계에 있습니다. 프로토콜 핸드셰이크와 비디오 투사는 실제 헤드 유닛에서 작동합니다. 휴대폰 화면이 연결 해제되기 전까지 수 초 동안 차량의 헤드 유닛에 성공적으로 표시됩니다(비디오 안정성이 개선 중입니다).
사용에 따른 책임은 사용자 본인에게 있습니다. 이 소프트웨어는 어떠한 종류의 보증 없이 "있는 그대로" 제공됩니다.
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)
## 알려진 버그
- **채널 할당이 순서를 가정함** — SERVICE_DISCOVERY_RESPONSE의 첫 번째 `av_channel`을 비디오로, 두 번째를 오디오로 할당합니다. 이는 차량 헤드 유닛(채널 1 = 비디오)에서는 작동하지만 openauto(채널 4 = 오디오, 비디오 아님)에서는 실패합니다. 수정: `av_channel` 내부의 `stream_type` 필드를 파싱하여 `VIDEO(3)`와 `AUDIO(1)`을 구분합니다.
- **비디오 안정성** — 장시간 스트리밍 후 헤드 유닛 USB 버퍼 오버플로로 인해 연결이 끊깁니다. 아래 테스트 결과를 참조하세요.
- **헤드 유닛의 중복 장치 표시** — 헤드 유닛의 스마트폰 페이지에는 두 가지 기능을 모두 가진 단일 항목 대신 앱이 두 개의 별도 항목(하나는 Android Auto, 하나는 Bluetooth)으로 표시됩니다. 이는 Android 12+가 실제 Bluetooth MAC 주소에 대한 액세스를 차단(`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"`를 통해 실제 주소를 구성 파일에 기록합니다. 사용자가 BT MAC을 수동으로 입력할 수 있는 UI 설정 화면이 필요합니다.
- **음성 비서 버튼 처리되지 않음** — 운전자가 헤드 유닛의 음성/비서 버튼을 누르면 VOICE_SESSION_REQUEST를 수신하고 음성 비서(Dicio 또는 시스템 기본값)를 실행하려 시도합니다. 그러나 실행된 비서는 아직 헤드 유닛 마이크에서 오디오를 받지 못합니다.
### 비디오 안정성 테스트 결과
테스트 패턴(컬러 바) 800x480, I-프레임 간격 1초:
| FPS | 비트레이트 | 프래그먼트 | 지속 시간 | 프레임 | 상태 |
|-----|---------|----------|----------|--------|--------|
| 30 | 2Mbps | No | ~3s | ~90 | ❌ 너무 빠름 |
| 15 | 2Mbps | No | ~33s | ~500 | ⚠️ 더 나음 |
| 10 | 2Mbps | No | ~93s | ~930 | ⚠️ 양호 |
| 30 | 2Mbps | Yes (2KB) | 5-25s | 150-750 | ⚠️ 변동 |
| 30 | 500Kbps | Yes (2KB) | ~54s | ~1691 | ⚠️ 더 나음 |
| 15 | 250Kbps | Yes (2KB) | ~67s+ | 1000+ | ⚠️ 양호 |
| 30 | 250Kbps | Yes (2KB), I=5s | ~20s | ~600 | ❌ 긴 I-프레임에서 더 나쁨 |
| 15 | 250Kbps | No | ~13s | ~200 | ❌ 여기서는 프래그먼테이션이 도움이 되었음 |
근본 원인: 지속적인 높은 처리량에서 헤드 유닛 USB 수신 버퍼가 오버플로됩니다. 데이터 전송률이 낮을수록 연결 시간이 길어집니다.
**확인된 최상의 구성:** 10fps, 2Mbps, 프래그먼테이션 없음 = 93초. 암호화 전 프래그먼트화 구현이 손상되어 있습니다(헤드 유닛이 재조립할 수 없음) — 추가 조사가 필요합니다.
## 빌드```bash
./gradlew assembleDebug
플랫폼 35가 포함된 Android SDK가 필요합니다.
./gradlew testDebugUnitTest
프로토콜, 프레이밍, TLS, 채널 로직, 비디오 상태 머신, 센서 처리 및 터치 입력을 다루는 113개의 유닛 및 통합 테스트.
### openauto(Docker)를 이용한 통합 테스트
openauto는 Android Auto 프로토콜 전체를 구현하는 서드파티 헤드 유닛 에뮬레이터입니다. 실제 차량 없이도 프로토콜 구현을 검증하는 데 사용합니다.
#### 사전 요구사항
- Docker 설치 및 실행 중
- ADB(USB 또는 무선)로 연결된 휴대폰
- 휴대폰에 앱 설치: `./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk`
#### 1. openauto Docker 이미지 빌드 (1회)```bash
cd thirdparty/openauto
docker build -f Dockerfile.headless -t openauto-headless .
이것은 Debian 컨테이너에서 모든 종속성(Qt5, boost, protobuf, OpenSSL)과 함께 openauto를 빌드합니다. 첫 빌드에는 약 5분이 소요됩니다.
docker run --rm -p 5100:5000 -e QT_QPA_PLATFORM=offscreen
openauto-headless timeout 60 /src/build/bin/autoapp
openauto는 컨테이너 내부의 포트 5000에서 수신 대기하며, 호스트의 포트 5100에 매핑됩니다. 헤드리스 모드(디스플레이 필요 없음)로 실행됩니다.
#### 3. ADB 역방향 포트 포워딩 설정```bash
adb reverse tcp:5000 tcp:5100
이렇게 하면 휴대폰의 localhost:5000이 컴퓨터의 localhost:5100(openauto)으로 터널링됩니다. USB 액세서리가 발견되지 않으면 우리 앱은 TCP 클라이언트로 localhost:5000에 연결합니다.
adb shell am start -n org.openandroidauto/.MainActivity
앱은 다음을 수행합니다:
1. USB 액세서리를 찾지 못합니다
2. `localhost:5000`에 연결합니다(adb reverse를 통한 openauto)
3. 전체 프로토콜 핸드셰이크를 수행합니다(VERSION → TLS → AUTH → SERVICE_DISCOVERY)
4. 채널을 엽니다(비디오, 오디오, 입력, 센서)
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를 로그로 남깁니다 — 이는 알려진 openauto 특성(quirk)이며 오류가 아닙니다Android Auto 프로토콜은 여러 프로젝트에 걸쳐 리버스 엔지니어링되었습니다. 각 프로젝트는 이전 프로젝트를 기반으로 했습니다:
opencardev/aasdk가 AACS보다 추가하는 내용:
주요 구조적 차이점:
priority + channel_id를 사용합니다. 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 CA가 서명O=Google Automotive Link(2014-2044 유효)Google은 Android Auto APK에 내장된 인증서+키를 약 8개월마다 교체하는 것으로 보입니다(인증서 유효 기간과 일치). 이는 추출된 키의 유용성을 제한하기 위한 의도적인 조치일 수 있습니다 — 헤드 유닛이 인증서 만료를 확인하면 오래된 추출 키는 더 이상 작동하지 않을 수 있습니다. 공식 앱 사용자는 앱 업데이트를 통해 새 인증서를 받습니다. 이 이론이 맞다면 공식 앱을 업데이트하지 않는 사용자는 결국 만료를 강제하는 헤드 유닛에서 거부될 수 있습니다. 모든 헤드 유닛이 만료를 확인하는 것은 아니며, 이 동작은 모델에 따라 다릅니다.
개인 키는 Android Auto APK 내부에서 AES-256-CBC로 암호화되어 있습니다. 헤드 유닛은 Google Automotive Link CA를 기준으로 휴대폰 인증서를 검증합니다 — 해당 CA가 서명한 모든 인증서가 허용됩니다.
키는 Android Auto APK에 (암호화된 상태로) 내장되어 있으며 APK 자체 알고리즘을 사용하여 복호화할 수 있습니다. 복호화를 실행하려면 ADB 접근이 가능한 Android 기기(루트 불필요, Google Play Services 불필요)가 필요한데, Android의 Base64 디코더가 데스크톱 JVM과 다르게 동작하기 때문입니다.
요구 사항:
adb pull로 휴대폰에서 가져오거나 APKPure/APKMirror에서 다운로드)d8 빌드 도구, adb)절차:
adb pull $(adb shell pm path com.google.android.projection.gearhead | grep base | cut -d: -f2) aa.apkdalvikvm으로 기기에서 실행합니다.인증서 공급자 클래스 찾기(2단계):
클래스 이름은 난독화되어 APK 버전마다 바뀌지만 구조는 항상 동일합니다. JADX에서 "-----BEGIN CERTIFICATE-----"를 검색하면 세 가지 메서드를 구현하는 작은 클래스를 찾을 수 있습니다:
a() → String 반환(CarService 인증서 PEM)b() → byte[] 반환(~1712바이트 — AES 암호화된 개인 키)c() → byte[] 반환(256바이트 — KDF 솔트)버전별 알려진 클래스 이름:
복호화 함수는 인근 클래스에 있습니다 — "AES/CBC/PKCS5Padding"을 검색하여 찾을 수 있습니다. 이 함수는 인증서 공급자 인터페이스를 매개변수로 받습니다.
참고: 복호화 단계(4단계)는 dalvikvm만 필요합니다 — ADB가 있는 모든 Android 기기에서 작동하며 루트나 Google Play Services가 필요하지 않습니다. GApps 요구 사항은 1단계(APK 가져오기, AA 앱이 Play Store를 통해 배포되므로)에만 해당합니다.
참고: 디컴파일된 APK 코드를 수정하거나 실행하지 않습니다. 대신 복호화 로직을 재구현하고 추출된 바이트 배열을 파일에서 읽으며 자체 main() 진입점을 가진 독립형 Decrypt.java 클래스를 작성합니다. 디컴파일된 소스는 알고리즘을 이해하고 바이트 배열을 복사하기 위한 참조로만 사용됩니다. 전체 Decrypt.java 소스는 tools/decrypt_key_from_apk.md를 참조하세요.
중요한 JADX 버그: JADX는 KDF 헬퍼를 byte b = bArr2[i2] & 255;로 디컴파일하지만 int b = bArr2[i2] & 255;여야 합니다. byte 타입은 다시 signed로 잘려서 잘못된 출력을 생성합니다. 이를 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)
- 결과는 PKCS#8 DER 인코딩 RSA 개인 키입니다
**참고:** `android.util.Base64`와 `java.util.Base64`의 차이로 인해 Android(데스크톱 JVM이 아닌)에서 실행해야 합니다. 데스크톱 JVM의 `Base64.getUrlDecoder()`는 Android 디코더가 허용하는 표준 base64 문자(`+`, `/`)와 줄바꿈을 거부합니다. 데스크톱에서는 `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"
This outputs the PKCS#8 private key in base64. Wrap it in PEM headers and place at app/src/main/assets/carservice_key.pem.
전체 단계별 가이드는 tools/decrypt_key_from_apk.md를 참조하세요.
일부 헤드 유닛은 인증서 만료를 확인하지 않을 수 있으므로, 이전에 추출한 cert+key 쌍(만료된 것이라도)이 여전히 작동할 수 있습니다. 출처:
[email protected] (참조: gamelaster/opengal_proxy)KeyFactory.generatePrivate()를 후킹합니다 (동일한 기기에 루트 권한과 Google Play 서비스가 모두 필요합니다): ```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에 따라 라이선스가 부여됩니다.
이 프로젝트는 git 서브모듈로 aasdk의 프로토콜 버퍼 정의를 포함합니다 (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj).
| 프로젝트 | 연도 | Proto 파일 | 역할 |
|---|
| f1xpl/aasdk | 2018 | 1 monolithic (Wifi.proto) | 최초 RE — 핵심 프로토콜, 비디오, 오디오, 입력, 센서 |
| AACS | 2020 | 28 (메시지별 분리) | 폰 측 구현 — 비디오 프로젝션에 대한 최소한의 범위 |
| opencardev/aasdk | 2024 | 254 (서비스별 계층 구조) | 결정적 참조 자료 — 모든 서비스를 포함한 전체 프로토콜 |
| APK 버전 | 인증서 공급자 클래스 | 솔트+키 클래스 | 복호화 클래스 |
|---|
| v6.4 | SslWrapper (필드 o, p) | 동일 클래스 | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (필드 b, c) | ivq.d() |