
オープンソースのAndroid Auto電話側実装。プロトコルリバースエンジニアリング、TLS相互認証、H.264ビデオ投影、タッチ入力インジェクション、USB AOAを介したセンサーデータストリーミングを備えています。
Android Autoの電話側アプリのオープンソース実装です。このアプリは携帯電話上で動作し、USB経由で車のヘッドユニットに投影し、Googleの独自規格である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` をビデオ、2 番目をオーディオとして割り当てています。これはカーヘッドユニット(チャンネル 1 = ビデオ)では動作しますが、openauto(チャンネル 4 = オーディオ、ビデオではない)では失敗します。修正: `av_channel` 内の `stream_type` フィールドを解析して `VIDEO(3)` と `AUDIO(1)` を区別します。
- **ビデオの安定性** — ヘッドユニットの USB バッファオーバーフローにより、長時間のストリーミング後に接続が切断されます。下記のテスト結果を参照してください。
- **ヘッドユニット上での重複デバイス表示** — ヘッドユニットのスマートフォンページに、アプリが 1 つのエントリ(Android Auto と Bluetooth の両方の機能を持つ)ではなく、2 つの別々のエントリ(1 つは Android Auto 用、もう 1 つは 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 | なし | ~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. openauto Docker イメージをビルドする(一度のみ)```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 にマッピングされています。ヘッドレスモードで実行されます(ディスプレイは不要)。
#### 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. チャンネルを開く(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 をログに記録します — これは既知の openauto の癖であり、エラーではありません。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 ステータス=-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 デバイス (root 不要、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 でデバイス上で実行するcert provider クラスの特定 (ステップ 2):
クラス名は難読化されており、APK バージョン間で変わりますが、構造は常に同じです。JADX で "-----BEGIN CERTIFICATE-----" を検索してください — 3 つのメソッドを持つインターフェースを実装した小さなクラスが見つかります。
a() → String を返します (CarService 証明書 PEM)b() → byte[] を返します (~1712 バイト — AES 暗号化された秘密鍵)c() → byte[] を返します (256 バイト — KDF ソルト)バージョン別の既知のクラス名:
復号化関数は近くのクラスにあります — "AES/CBC/PKCS5Padding" を検索して見つけてください。それは cert provider インターフェースをパラメータとして受け取ります。
注記: 復号化ステップ (ステップ 4) は dalvikvm のみを必要とします — ADB があれば任意の Android デバイスで動作し、root や Google Play Services は必要ありません。GApps の要件はステップ 1 のみです (AA アプリは Play Store 経由で配布されるため、APK をプルするため)。
注記: 逆コンパイルされた 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 型は符号付きに切り詰められ、ガベージ出力を生成します。これを 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、フラグ=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"
これは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 の下でライセンスされています。
このプロジェクトには、Gitサブモジュールとして aasdk (GPLv3, Copyright © 2018 f1x.studio / Michal Szwaj) のプロトコルバッファ定義が含まれています。
| Project | Year | Proto Files | Role |
|---|
| f1xpl/aasdk | 2018 | 1 monolithic (Wifi.proto) | オリジナルのリバースエンジニアリング — コアプロトコル、ビデオ、オーディオ、入力、センサー |
| AACS | 2020 | 28 (split by message) | 電話側実装 — ビデオプロジェクションの最小限のカバレッジ |
| opencardev/aasdk | 2024 | 254 (hierarchical by service) | 決定版リファレンス — 全サービスを含む完全なプロトコル |
| APK バージョン | cert provider クラス | ソルト+鍵クラス | 復号化クラス |
|---|
| v6.4 | SslWrapper (フィールド o, p) | 同じクラス | SslWrapper.m23915f() |
| v16.8 | ivo / rqi | ivq / rql (フィールド b, c) | ivq.d() |