From b8edd5eff55eabce3699f4c232a02eeda1336883 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Fri, 17 Jul 2026 19:17:10 +0800 Subject: [PATCH] =?UTF-8?q?project:=20=E5=A2=9E=E5=8A=A0web=E5=92=8Cadb?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=EF=BC=8C=E6=9C=AA=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AdbLoopbackController/.gitignore | 6 + AdbLoopbackController/app/build.gradle | 73 + AdbLoopbackController/app/proguard-rules.pro | 2 + .../app/src/main/AndroidManifest.xml | 27 + .../com/ttstd/adbloopback/AdbManager.java | 89 ++ .../com/ttstd/adbloopback/MainActivity.java | 154 ++ .../ttstd/adbloopback/adb/AdbConnection.java | 110 ++ .../ttstd/adbloopback/adb/AdbConstants.java | 24 + .../com/ttstd/adbloopback/adb/AdbKeyPair.java | 144 ++ .../com/ttstd/adbloopback/adb/AdbMessage.java | 84 + .../com/ttstd/adbloopback/adb/AdbPairing.java | 88 ++ .../com/ttstd/adbloopback/adb/AdbStream.java | 96 ++ .../com/ttstd/adbloopback/tls/TlsCrypto.java | 72 + .../ttstd/adbloopback/tls/TlsPskClient.java | 332 ++++ .../app/src/main/res/layout/activity_main.xml | 236 +++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes .../app/src/main/res/values/strings.xml | 4 + .../main/res/xml/network_security_config.xml | 8 + AdbLoopbackController/build.gradle | 5 + AdbLoopbackController/gradle.properties | 3 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + AdbLoopbackController/settings.gradle | 20 + .../ttstd/controlled/webrtc/WebRtcClient.java | 26 + WebRTCControllerWeb/.gitignore | 6 + WebRTCControllerWeb/README.md | 105 ++ WebRTCControllerWeb/index.html | 15 + WebRTCControllerWeb/package-lock.json | 1346 +++++++++++++++++ WebRTCControllerWeb/package.json | 20 + WebRTCControllerWeb/pnpm-lock.yaml | 844 +++++++++++ .../public/control_message.proto | 41 + WebRTCControllerWeb/server/index.js | 109 ++ WebRTCControllerWeb/server/package.json | 13 + WebRTCControllerWeb/src/App.vue | 42 + .../src/components/ConnectionPanel.vue | 66 + .../src/components/ControlBar.vue | 33 + .../src/components/RemoteScreen.vue | 112 ++ .../src/components/StatsBar.vue | 15 + WebRTCControllerWeb/src/main.js | 5 + .../src/proto/controlMessage.js | 31 + .../src/services/SignalingClient.js | 91 ++ .../src/services/WebRtcController.js | 205 +++ .../src/store/controllerStore.js | 170 +++ WebRTCControllerWeb/src/style.css | 173 +++ WebRTCControllerWeb/vite.config.js | 10 + 49 files changed, 5061 insertions(+) create mode 100644 AdbLoopbackController/.gitignore create mode 100644 AdbLoopbackController/app/build.gradle create mode 100644 AdbLoopbackController/app/proguard-rules.pro create mode 100644 AdbLoopbackController/app/src/main/AndroidManifest.xml create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/AdbManager.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/MainActivity.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConnection.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConstants.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbKeyPair.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbMessage.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbPairing.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbStream.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsCrypto.java create mode 100644 AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsPskClient.java create mode 100644 AdbLoopbackController/app/src/main/res/layout/activity_main.xml create mode 100644 AdbLoopbackController/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 AdbLoopbackController/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 AdbLoopbackController/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 AdbLoopbackController/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 AdbLoopbackController/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 AdbLoopbackController/app/src/main/res/values/strings.xml create mode 100644 AdbLoopbackController/app/src/main/res/xml/network_security_config.xml create mode 100644 AdbLoopbackController/build.gradle create mode 100644 AdbLoopbackController/gradle.properties create mode 100644 AdbLoopbackController/gradle/wrapper/gradle-wrapper.jar create mode 100644 AdbLoopbackController/gradle/wrapper/gradle-wrapper.properties create mode 100644 AdbLoopbackController/settings.gradle create mode 100644 WebRTCControllerWeb/.gitignore create mode 100644 WebRTCControllerWeb/README.md create mode 100644 WebRTCControllerWeb/index.html create mode 100644 WebRTCControllerWeb/package-lock.json create mode 100644 WebRTCControllerWeb/package.json create mode 100644 WebRTCControllerWeb/pnpm-lock.yaml create mode 100644 WebRTCControllerWeb/public/control_message.proto create mode 100644 WebRTCControllerWeb/server/index.js create mode 100644 WebRTCControllerWeb/server/package.json create mode 100644 WebRTCControllerWeb/src/App.vue create mode 100644 WebRTCControllerWeb/src/components/ConnectionPanel.vue create mode 100644 WebRTCControllerWeb/src/components/ControlBar.vue create mode 100644 WebRTCControllerWeb/src/components/RemoteScreen.vue create mode 100644 WebRTCControllerWeb/src/components/StatsBar.vue create mode 100644 WebRTCControllerWeb/src/main.js create mode 100644 WebRTCControllerWeb/src/proto/controlMessage.js create mode 100644 WebRTCControllerWeb/src/services/SignalingClient.js create mode 100644 WebRTCControllerWeb/src/services/WebRtcController.js create mode 100644 WebRTCControllerWeb/src/store/controllerStore.js create mode 100644 WebRTCControllerWeb/src/style.css create mode 100644 WebRTCControllerWeb/vite.config.js diff --git a/AdbLoopbackController/.gitignore b/AdbLoopbackController/.gitignore new file mode 100644 index 0000000..08fba81 --- /dev/null +++ b/AdbLoopbackController/.gitignore @@ -0,0 +1,6 @@ +/build/ +/app/build/ +/.idea/ +/.gradle/ +config.gradle +/local.properties diff --git a/AdbLoopbackController/app/build.gradle b/AdbLoopbackController/app/build.gradle new file mode 100644 index 0000000..cc1e3ba --- /dev/null +++ b/AdbLoopbackController/app/build.gradle @@ -0,0 +1,73 @@ +plugins { + id 'com.android.application' +} + +def releaseTime() { + return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC")) +} + +def appName() { + return "AdbLoopbackController" +} + +android { + namespace 'com.ttstd.adbloopback' + compileSdk 34 + + defaultConfig { + applicationId "com.ttstd.adbloopback" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildFeatures { + buildConfig true + } + + signingConfigs { + keypub { + storeFile file(rootProject.ext.signingConfigs.keypub.storeFile) + storePassword rootProject.ext.signingConfigs.keypub.storePassword + keyAlias rootProject.ext.signingConfigs.keypub.keyAlias + keyPassword rootProject.ext.signingConfigs.keypub.keyPassword + } + } + + buildTypes { + debug { + versionNameSuffix "_debug" + debuggable true + minifyEnabled false + signingConfig signingConfigs.keypub + } + + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.keypub + } + } + + applicationVariants.all { variant -> + variant.outputs.each { output -> + def buildType = variant.buildType.name + def fileName = (buildType.contains("debug")) + ? "${appName()}_V${defaultConfig.versionName}_${releaseTime()}.apk" + : "${appName()}_${variant.versionCode}_V${variant.versionName}_${releaseTime()}_${buildType}.apk" + output.outputFileName = fileName + } + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' +} diff --git a/AdbLoopbackController/app/proguard-rules.pro b/AdbLoopbackController/app/proguard-rules.pro new file mode 100644 index 0000000..cdbb478 --- /dev/null +++ b/AdbLoopbackController/app/proguard-rules.pro @@ -0,0 +1,2 @@ +# proguard 规则(demo 无需混淆) +-dontwarn ** diff --git a/AdbLoopbackController/app/src/main/AndroidManifest.xml b/AdbLoopbackController/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..3fcfba7 --- /dev/null +++ b/AdbLoopbackController/app/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/AdbManager.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/AdbManager.java new file mode 100644 index 0000000..f75e54c --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/AdbManager.java @@ -0,0 +1,89 @@ +package com.ttstd.adbloopback; + +import com.ttstd.adbloopback.adb.AdbConnection; +import com.ttstd.adbloopback.adb.AdbKeyPair; +import com.ttstd.adbloopback.adb.AdbPairing; +import com.ttstd.adbloopback.adb.AdbStream; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +/** + * ADB 管理器:封装密钥持久化、配对、连接及 Shell 指令执行。 + * 它是上层 UI 与底层 ADB 协议库之间的桥梁。 + */ +public class AdbManager { + private final File privateKeyFile; + private final File publicKeyFile; + private AdbKeyPair keyPair; + private AdbConnection connection; + + public AdbManager(File filesDir) { + this.privateKeyFile = new File(filesDir, "adb_private.key"); + this.publicKeyFile = new File(filesDir, "adb_public.key"); + loadOrCreateKeyPair(); + } + + private void loadOrCreateKeyPair() { + try { + if (privateKeyFile.exists() && publicKeyFile.exists()) { + keyPair = AdbKeyPair.read(privateKeyFile, publicKeyFile); + } else { + keyPair = AdbKeyPair.generate(); + keyPair.write(privateKeyFile, publicKeyFile); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** 发起无线调试配对(TLS-PSK) */ + public void pair(int port, String pairingCode) throws Exception { + AdbPairing.pair("127.0.0.1", port, pairingCode, keyPair); + } + + /** 连接无线调试主端口 */ + public void connect(int port) throws Exception { + if (connection != null) { + try { connection.close(); } catch (Exception ignored) {} + } + connection = AdbConnection.connect("127.0.0.1", port, keyPair); + } + + /** 断开连接 */ + public void disconnect() { + if (connection != null) { + try { + connection.close(); + } catch (IOException e) { + e.printStackTrace(); + } + connection = null; + } + } + + /** 执行 Shell 命令并获取全部输出 */ + public String execShell(String command) throws Exception { + if (connection == null || !connection.isConnected()) { + throw new IOException("ADB 未连接"); + } + AdbStream stream = connection.open("shell:" + command); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] data; + while ((data = stream.read()) != null) { + bos.write(data); + } + stream.close(); + return bos.toString("UTF-8"); + } + + /** 快速发送点击指令 */ + public void tap(int x, int y) throws Exception { + execShell("input tap " + x + " " + y); + } + + public boolean isConnected() { + return connection != null && connection.isConnected(); + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/MainActivity.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/MainActivity.java new file mode 100644 index 0000000..49cae2b --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/MainActivity.java @@ -0,0 +1,154 @@ +package com.ttstd.adbloopback; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.view.View; +import android.widget.Button; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.google.android.material.textfield.TextInputEditText; + +public class MainActivity extends AppCompatActivity { + + private AdbManager adbManager; + + private TextInputEditText etPairPort; + private TextInputEditText etPairCode; + private TextInputEditText etMainPort; + private TextInputEditText etTapX; + private TextInputEditText etTapY; + private TextInputEditText etCmd; + private TextView tvStatus; + private TextView tvLog; + private ScrollView scrollLog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + adbManager = new AdbManager(getFilesDir()); + + etPairPort = findViewById(R.id.et_pair_port); + etPairCode = findViewById(R.id.et_pair_code); + etMainPort = findViewById(R.id.et_main_port); + etTapX = findViewById(R.id.et_tap_x); + etTapY = findViewById(R.id.et_tap_y); + etCmd = findViewById(R.id.et_cmd); + tvStatus = findViewById(R.id.tv_status); + tvLog = findViewById(R.id.tv_log); + scrollLog = findViewById(R.id.scroll_root); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + log("⚠️ 当前系统低于 Android 11(API 30),无线调试不可用。"); + } + + findViewById(R.id.btn_open_settings).setOnClickListener(v -> openDeveloperOptions()); + findViewById(R.id.btn_pair).setOnClickListener(v -> doPair()); + findViewById(R.id.btn_connect).setOnClickListener(v -> doConnect()); + findViewById(R.id.btn_disconnect).setOnClickListener(v -> doDisconnect()); + findViewById(R.id.btn_tap).setOnClickListener(v -> doTap()); + findViewById(R.id.btn_exec).setOnClickListener(v -> doExec()); + } + + private void openDeveloperOptions() { + try { + startActivity(new Intent(Settings.ACTION_APPLICATION_DEVELOPMENT_SETTINGS)); + Toast.makeText(this, "请开启「无线调试」,并记录配对端口/码与主端口", Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Toast.makeText(this, "无法打开开发者选项,请手动进入", Toast.LENGTH_LONG).show(); + } + } + + private void doPair() { + String portStr = etPairPort.getText().toString().trim(); + String code = etPairCode.getText().toString().trim(); + if (portStr.isEmpty() || code.isEmpty()) { + Toast.makeText(this, "请填写配对端口与配对码", Toast.LENGTH_SHORT).show(); + return; + } + int port = Integer.parseInt(portStr); + runBg("配对", () -> { + adbManager.pair(port, code); + log("✅ 配对成功(TLS-PSK 完成,公钥已加入 adbd 授权列表)"); + }); + } + + private void doConnect() { + String portStr = etMainPort.getText().toString().trim(); + if (portStr.isEmpty()) { + Toast.makeText(this, "请填写无线调试主端口", Toast.LENGTH_SHORT).show(); + return; + } + int port = Integer.parseInt(portStr); + runBg("连接", () -> { + adbManager.connect(port); + log("✅ 已连接无线调试主端口 " + port + ",获得 ADB 权限管道"); + runOnUiThread(() -> tvStatus.setText("状态:已连接(ADB 权限)")); + }); + } + + private void doDisconnect() { + runBg("断开", () -> { + adbManager.disconnect(); + runOnUiThread(() -> tvStatus.setText("状态:未连接")); + log("🔌 已断开连接"); + }); + } + + private void doTap() { + String xStr = etTapX.getText().toString().trim(); + String yStr = etTapY.getText().toString().trim(); + if (xStr.isEmpty() || yStr.isEmpty()) { + Toast.makeText(this, "请填写 X/Y 坐标", Toast.LENGTH_SHORT).show(); + return; + } + int x = Integer.parseInt(xStr); + int y = Integer.parseInt(yStr); + runBg("input tap", () -> { + adbManager.tap(x, y); + log("👆 已发送 input tap " + x + " " + y); + }); + } + + private void doExec() { + String cmd = etCmd.getText().toString().trim(); + if (cmd.isEmpty()) { + Toast.makeText(this, "请输入 shell 命令", Toast.LENGTH_SHORT).show(); + return; + } + runBg("exec: " + cmd, () -> { + String out = adbManager.execShell(cmd); + log("▶ " + cmd + "\n" + (out.isEmpty() ? "(无输出)" : out)); + }); + } + + private interface ThrowingRunnable { + void run() throws Exception; + } + + private void runBg(String name, ThrowingRunnable task) { + log("⏳ 执行 " + name + " …"); + new Thread(() -> { + try { + task.run(); + } catch (Exception e) { + log("❌ " + name + " 失败:" + e.getMessage()); + e.printStackTrace(); + } + }).start(); + } + + private void log(String line) { + runOnUiThread(() -> { + tvLog.append(line + "\n"); + scrollLog.post(() -> scrollLog.fullScroll(View.FOCUS_DOWN)); + }); + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConnection.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConnection.java new file mode 100644 index 0000000..a3a948c --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConnection.java @@ -0,0 +1,110 @@ +package com.ttstd.adbloopback.adb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; + +/** + * 自建 ADB 客户端连接:与系统 adbd(无线调试守护进程)建立明文 TCP 连接, + * 完成 CNXN / AUTH 握手,然后可开启 shell 流执行指令。 + */ +public class AdbConnection { + private final Socket socket; + private final AdbKeyPair keyPair; + private InputStream in; + private OutputStream out; + private boolean connected = false; + private int lastLocalId = 1; + + public AdbConnection(Socket socket, AdbKeyPair keyPair) { + this.socket = socket; + this.keyPair = keyPair; + } + + public static AdbConnection connect(String host, int port, AdbKeyPair keyPair) throws Exception { + Socket s = new Socket(); + s.connect(new InetSocketAddress(host, port), 5000); + s.setSoTimeout(2000); // 读超时,供 shell 读取在无输出时及时返回 + s.setTcpNoDelay(true); + AdbConnection c = new AdbConnection(s, keyPair); + c.connect(); + return c; + } + + private void connect() throws Exception { + in = socket.getInputStream(); + out = socket.getOutputStream(); + + // 1) 发送 CNXN + AdbMessage.write(out, new AdbMessage(AdbConstants.CNXN, AdbConstants.VERSION, + AdbConstants.MAX_PAYLOAD, "device::adb-loopback-demo".getBytes("UTF-8"))); + + // 2) 处理 AUTH 挑战,直到收到 CNXN 确认 + while (true) { + AdbMessage resp = readMessage(); + if (resp.command == AdbConstants.AUTH) { + int sub = resp.arg0; + if (sub == AdbConstants.AUTH_TOKEN) { + byte[] signature = keyPair.sign(resp.data); + AdbMessage.write(out, new AdbMessage(AdbConstants.AUTH, + AdbConstants.AUTH_SIGNATURE, 0, signature)); + } else if (sub == AdbConstants.AUTH_RSAPUBLICKEY) { + // 某些场景下服务端主动索要公钥,一并提交 + AdbMessage.write(out, new AdbMessage(AdbConstants.AUTH, + AdbConstants.AUTH_RSAPUBLICKEY, 0, + keyPair.getAdbPublicKey().getBytes("UTF-8"))); + } else { + throw new IOException("ADB AUTH 失败,sub=" + sub); + } + } else if (resp.command == AdbConstants.CNXN) { + connected = true; + break; + } else { + throw new IOException("ADB 握手收到未知消息: 0x" + + Integer.toHexString(resp.command)); + } + } + if (!connected) { + throw new IOException("ADB 连接建立失败"); + } + } + + public AdbStream open(String destination) throws Exception { + int localId = lastLocalId++; + AdbMessage.write(out, new AdbMessage(AdbConstants.OPEN, localId, 0, + (destination + "\0").getBytes("UTF-8"))); + + AdbMessage resp = readMessage(); + if (resp.command == AdbConstants.OKAY) { + AdbStream stream = new AdbStream(this, localId, destination); + stream.setRemoteId(resp.arg0); + return stream; + } else if (resp.command == AdbConstants.CLSE) { + throw new IOException("ADB 流打开被拒绝"); + } else { + throw new IOException("ADB 打开流收到未知响应: 0x" + + Integer.toHexString(resp.command)); + } + } + + synchronized void sendInternal(int command, int arg0, int arg1, byte[] data) throws IOException { + AdbMessage.write(out, new AdbMessage(command, arg0, arg1, data)); + } + + synchronized AdbMessage readMessage() throws IOException { + return AdbMessage.read(in); + } + + public void close() throws IOException { + socket.close(); + connected = false; + } + + public boolean isConnected() { + return connected; + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConstants.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConstants.java new file mode 100644 index 0000000..ae45333 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbConstants.java @@ -0,0 +1,24 @@ +package com.ttstd.adbloopback.adb; + +/** ADB 网络协议常量(协议字段均为小端序 / little-endian)。 */ +public final class AdbConstants { + // 消息命令(4 字节 ASCII,按小端读取) + public static final int CNXN = 0x4e584e43; // "CNXN" + public static final int AUTH = 0x48545541; // "AUTH" + public static final int OPEN = 0x4e45504f; // "OPEN" + public static final int OKAY = 0x59414b4f; // "OKAY" + public static final int CLSE = 0x45534c43; // "CLSE" + public static final int WRTE = 0x45545257; // "WRTE" + + // AUTH 子类型 + public static final int AUTH_TOKEN = 1; // 服务端下发的随机挑战 + public static final int AUTH_SIGNATURE = 2; // 客户端对挑战的签名 + public static final int AUTH_RSAPUBLICKEY = 3; // 客户端公钥 + + public static final int VERSION = 0x01000000; // ADB 协议版本 1.0(小端) + public static final int MAX_PAYLOAD = 4096; + + public static final String TAG = "AdbLoopback"; + + private AdbConstants() {} +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbKeyPair.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbKeyPair.java new file mode 100644 index 0000000..11b21d7 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbKeyPair.java @@ -0,0 +1,144 @@ +package com.ttstd.adbloopback.adb; + +import android.util.Base64; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.security.interfaces.RSAPublicKey; + +/** + * ADB 密钥对:负责生成 RSA 密钥、对 AUTH 挑战做 SHA1withRSA 签名, + * 以及把公钥编码成 adbd 可识别的 adb 公钥格式(base64(PKCS#1 RSAPublicKey) + 注释)。 + */ +public class AdbKeyPair { + private final PrivateKey privateKey; + private final PublicKey publicKey; + + public AdbKeyPair(PrivateKey privateKey, PublicKey publicKey) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + public static AdbKeyPair generate() throws Exception { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(2048, new SecureRandom()); + KeyPair kp = gen.generateKeyPair(); + return new AdbKeyPair(kp.getPrivate(), kp.getPublic()); + } + + public static AdbKeyPair read(File privateFile, File publicFile) throws Exception { + byte[] pk = readAllBytes(privateFile); + byte[] pb = readAllBytes(publicFile); + KeyFactory kf = KeyFactory.getInstance("RSA"); + PrivateKey priv = kf.generatePrivate(new PKCS8EncodedKeySpec(pk)); + PublicKey pub = kf.generatePublic(new X509EncodedKeySpec(pb)); + return new AdbKeyPair(priv, pub); + } + + public void write(File privateFile, File publicFile) throws Exception { + KeyFactory kf = KeyFactory.getInstance("RSA"); + byte[] pk = kf.getKeySpec(privateKey, PKCS8EncodedKeySpec.class).getEncoded(); + byte[] pb = kf.getKeySpec(publicKey, X509EncodedKeySpec.class).getEncoded(); + writeAllBytes(privateFile, pk); + writeAllBytes(publicFile, pb); + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + /** 对服务端下发的 AUTH token 做签名:SHA1(token) 再用 RSA PKCS#1 v1.5 签名。 */ + public byte[] sign(byte[] token) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] hash = md.digest(token); + Signature sig = Signature.getInstance("SHA1withRSA"); + sig.initSign(privateKey); + sig.update(hash); + return sig.sign(); + } + + /** + * 生成 adb 格式公钥字符串:base64( DER(PKCS#1 RSAPublicKey) ) + " " + 注释 + "\n" + * adbd 解析时只取 base64 部分,按 (modulus || exponent) 还原 RSA 公钥。 + */ + public String getAdbPublicKey() throws Exception { + RSAPublicKey rsa = (RSAPublicKey) publicKey; + byte[] modulus = toDerInteger(rsa.getModulus()); + byte[] exponent = toDerInteger(rsa.getPublicExponent()); + byte[] pkcs1 = encodeSequence(modulus, exponent); + String b64 = Base64.encodeToString(pkcs1, Base64.NO_WRAP); + return b64 + " unknown@unknown\n"; + } + + // ---- DER 编码辅助 ---- + + private static byte[] toDerInteger(BigInteger v) { + return derTlv((byte) 0x02, v.toByteArray()); + } + + private static byte[] encodeSequence(byte[]... parts) throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + for (byte[] p : parts) bos.write(p); + return derTlv((byte) 0x30, bos.toByteArray()); + } + + private static byte[] derTlv(byte tag, byte[] content) { + return concat(new byte[]{tag}, derLen(content.length), content); + } + + private static byte[] derLen(int len) { + if (len < 0x80) { + return new byte[]{(byte) len}; + } else if (len < 0x100) { + return new byte[]{(byte) 0x81, (byte) len}; + } else { + return new byte[]{(byte) 0x82, (byte) (len >> 8), (byte) len}; + } + } + + private static byte[] concat(byte[] a, byte[] b) { + byte[] r = new byte[a.length + b.length]; + System.arraycopy(a, 0, r, 0, a.length); + System.arraycopy(b, 0, r, a.length, b.length); + return r; + } + + private static byte[] concat(byte[] a, byte[] b, byte[] c) { + return concat(concat(a, b), c); + } + + private static byte[] readAllBytes(File f) throws IOException { + try (FileInputStream in = new FileInputStream(f)) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int n; + while ((n = in.read(buf)) > 0) bos.write(buf, 0, n); + return bos.toByteArray(); + } + } + + private static void writeAllBytes(File f, byte[] data) throws IOException { + try (FileOutputStream out = new FileOutputStream(f)) { + out.write(data); + } + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbMessage.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbMessage.java new file mode 100644 index 0000000..41610de --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbMessage.java @@ -0,0 +1,84 @@ +package com.ttstd.adbloopback.adb; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.zip.CRC32; + +/** + * ADB 消息封装与读写。 + * + *
+ * struct message {
+ *     unsigned command;       // 命令
+ *     unsigned arg0;          // 第一参数(本地 id)
+ *     unsigned arg1;          // 第二参数(远端 id)
+ *     unsigned data_length;   // 负载长度
+ *     unsigned data_crc32;    // 负载 CRC32
+ *     unsigned magic;         // command ^ 0xffffffff
+ * };
+ * 
+ * 所有整型字段均为小端序(little-endian)。 + */ +public class AdbMessage { + public int command; + public int arg0; + public int arg1; + public byte[] data; + + public AdbMessage(int command, int arg0, int arg1, byte[] data) { + this.command = command; + this.arg0 = arg0; + this.arg1 = arg1; + this.data = data != null ? data : new byte[0]; + } + + public static void write(OutputStream out, AdbMessage msg) throws IOException { + byte[] payload = msg.data; + int crc = crc32(payload); + ByteBuffer buf = ByteBuffer.allocate(24 + payload.length); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(msg.command); + buf.putInt(msg.arg0); + buf.putInt(msg.arg1); + buf.putInt(payload.length); + buf.putInt(crc); + buf.putInt(msg.command ^ 0xffffffff); + buf.put(payload); + out.write(buf.array()); + out.flush(); + } + + public static AdbMessage read(InputStream in) throws IOException { + byte[] header = readExactly(in, 24); + ByteBuffer buf = ByteBuffer.wrap(header); + buf.order(ByteOrder.LITTLE_ENDIAN); + int command = buf.getInt(); + int arg0 = buf.getInt(); + int arg1 = buf.getInt(); + int len = buf.getInt(); + /* int crc = */ buf.getInt(); + /* int magic = */ buf.getInt(); + byte[] data = len > 0 ? readExactly(in, len) : new byte[0]; + return new AdbMessage(command, arg0, arg1, data); + } + + public static byte[] readExactly(InputStream in, int n) throws IOException { + byte[] b = new byte[n]; + int off = 0; + while (off < n) { + int r = in.read(b, off, n - off); + if (r < 0) throw new IOException("ADB 连接意外断开 (EOF)"); + off += r; + } + return b; + } + + public static int crc32(byte[] data) { + CRC32 c = new CRC32(); + c.update(data); + return (int) c.getValue(); + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbPairing.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbPairing.java new file mode 100644 index 0000000..4d0e4b5 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbPairing.java @@ -0,0 +1,88 @@ +package com.ttstd.adbloopback.adb; + +import com.ttstd.adbloopback.tls.TlsPskClient; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * 无线调试配对:在 TLS-PSK 通道之上,与 adbd pairing 守护进程交换 RSA 公钥。 + * + *

配对成功后,本 App 的公钥被加入 adbd 的授权列表,之后即可用同一密钥对 + * 无线调试主端口发起 ADB 连接(无需电脑 / Shizuku)。 + * + *

线规(AOSP pairing 协议):每条消息 = 4 字节头 [type(1)][reserved(1)][len(2, 大端)] + payload。 + */ +public class AdbPairing { + + // AOSP pairing.h MsgType + private static final byte MSG_PING = 0; + private static final byte MSG_PONG = 1; + private static final byte MSG_PAIRING_REQ = 2; + private static final byte MSG_PAIRING_RESULT = 3; + private static final byte MSG_PAIRING_ACK = 4; + + public static void pair(String host, int port, String pairingCode, AdbKeyPair keyPair) throws Exception { + // 配对码同时作为 TLS-PSK 的 PSK 与 identity(与 AOSP adb pair 一致) + byte[] psk = pairingCode.getBytes("UTF-8"); + TlsPskClient tls = new TlsPskClient(host, port, pairingCode, psk); + tls.handshake(); + + InputStream in = tls.getInputStream(); + OutputStream out = tls.getOutputStream(); + + // 向服务端提交本 App 的 ADB 公钥 + byte[] pubKey = keyPair.getAdbPublicKey().getBytes("UTF-8"); + writePairingMsg(out, MSG_PAIRING_REQ, pubKey); + + // 读取服务端回应(可能先有 Ping,然后 PairingResult) + boolean paired = false; + for (int i = 0; i < 4; i++) { + PairingMsg msg = readPairingMsg(in); + if (msg.type == MSG_PING) { + writePairingMsg(out, MSG_PONG, new byte[0]); + } else if (msg.type == MSG_PAIRING_RESULT) { + paired = true; + break; + } else { + throw new IOException("无线调试配对收到意外消息类型: " + msg.type); + } + } + if (!paired) { + throw new IOException("无线调试配对未完成"); + } + + // 回 ACK(部分实现需要) + try { + writePairingMsg(out, MSG_PAIRING_ACK, new byte[0]); + } catch (IOException ignored) { + } + } + + private static void writePairingMsg(OutputStream out, byte type, byte[] payload) throws IOException { + byte[] header = {type, 0x00, (byte) (payload.length >> 8), (byte) payload.length}; + out.write(header); + out.write(payload); + out.flush(); + } + + private static PairingMsg readPairingMsg(InputStream in) throws IOException { + byte[] header = AdbMessage.readExactly(in, 4); + byte type = header[0]; + int len = ((header[2] & 0xff) << 8) | (header[3] & 0xff); + byte[] payload = len > 0 ? AdbMessage.readExactly(in, len) : new byte[0]; + return new PairingMsg(type, payload); + } + + private static class PairingMsg { + final byte type; + @SuppressWarnings("unused") + final byte[] payload; + + PairingMsg(byte type, byte[] payload) { + this.type = type; + this.payload = payload; + } + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbStream.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbStream.java new file mode 100644 index 0000000..c0113d2 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/adb/AdbStream.java @@ -0,0 +1,96 @@ +package com.ttstd.adbloopback.adb; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.Queue; + +/** + * ADB 流(对应 ADB 协议里的 OPEN/WRTE/OKAY/CLSE 多路复用)。 + * 本 demo 仅在单流场景下使用,内部通过 connection 的同步读写防止消息交错。 + */ +public class AdbStream { + private final AdbConnection connection; + private final int localId; + private final String destination; + private int remoteId = -1; + private boolean closed = false; + private final Queue incoming = new LinkedList<>(); + private final Object lock = new Object(); + + AdbStream(AdbConnection connection, int localId, String destination) { + this.connection = connection; + this.localId = localId; + this.destination = destination; + } + + void setRemoteId(int remoteId) { + this.remoteId = remoteId; + } + + public int write(byte[] data) throws IOException { + synchronized (lock) { + connection.sendInternal(AdbConstants.WRTE, localId, remoteId, data); + // 等待对端 OKAY,其间若收到 WRTE 数据则先缓存起来 + while (true) { + AdbMessage msg = connection.readMessage(); + if (msg.command == AdbConstants.OKAY) { + break; + } else if (msg.command == AdbConstants.WRTE) { + incoming.add(msg.data); + } else if (msg.command == AdbConstants.CLSE) { + closed = true; + break; + } else { + break; + } + } + } + return data.length; + } + + /** 读取对端发来的数据;无数据且对端未关闭时按 socket 超时返回 null。 */ + public byte[] read() throws IOException { + synchronized (lock) { + if (!incoming.isEmpty()) { + return incoming.poll(); + } + while (!closed) { + try { + AdbMessage msg = connection.readMessage(); + if (msg.command == AdbConstants.WRTE) { + incoming.add(msg.data); + return incoming.poll(); + } else if (msg.command == AdbConstants.OKAY) { + // 暂无数据,继续等待 + } else if (msg.command == AdbConstants.CLSE) { + closed = true; + break; + } + } catch (IOException e) { + // 多为 socket 读超时,返回当前缓存(可能为 null) + break; + } + } + return incoming.poll(); + } + } + + public void close() throws IOException { + synchronized (lock) { + if (closed) return; + try { + connection.sendInternal(AdbConstants.CLSE, localId, remoteId, new byte[0]); + } catch (IOException ignored) { + } + closed = true; + } + } + + public boolean isClosed() { + return closed; + } + + public String getDestination() { + return destination; + } +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsCrypto.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsCrypto.java new file mode 100644 index 0000000..131b902 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsCrypto.java @@ -0,0 +1,72 @@ +package com.ttstd.adbloopback.tls; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.MessageDigest; + +/** TLS 1.2 所需的密码学原语:SHA-256、HMAC-SHA256、TLS PRF、AES-128-GCM。 */ +public final class TlsCrypto { + + public static byte[] sha256(byte[] data) throws Exception { + return MessageDigest.getInstance("SHA-256").digest(data); + } + + public static byte[] hmacSha256(byte[] key, byte[] data) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(data); + } + + /** + * TLS 1.2 PRF,使用 SHA-256: + * PRF(secret, label, seed) = P_SHA256(secret, label + seed) + */ + public static byte[] prf(byte[] secret, String label, byte[] seed, int outLen) throws Exception { + byte[] labelSeed = concat(label.getBytes("ASCII"), seed); + byte[] result = new byte[outLen]; + byte[] prev = hmacSha256(secret, labelSeed); // A(1) + int generated = 0; + while (generated < outLen) { + byte[] block = hmacSha256(secret, concat(prev, labelSeed)); + int take = Math.min(block.length, outLen - generated); + System.arraycopy(block, 0, result, generated, take); + generated += take; + prev = hmacSha256(secret, prev); // A(i+1) + } + return result; + } + + public static byte[] aesGcmEncrypt(byte[] key, byte[] nonce12, byte[] plaintext, byte[] aad) throws Exception { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce12)); + c.updateAAD(aad); + return c.doFinal(plaintext); + } + + public static byte[] aesGcmDecrypt(byte[] key, byte[] nonce12, byte[] ciphertextWithTag, byte[] aad) throws Exception { + Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, nonce12)); + c.updateAAD(aad); + return c.doFinal(ciphertextWithTag); + } + + public static byte[] concat(byte[]... arrays) { + int len = 0; + for (byte[] a : arrays) { + if (a != null) len += a.length; + } + byte[] r = new byte[len]; + int off = 0; + for (byte[] a : arrays) { + if (a != null) { + System.arraycopy(a, 0, r, off, a.length); + off += a.length; + } + } + return r; + } + + private TlsCrypto() {} +} diff --git a/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsPskClient.java b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsPskClient.java new file mode 100644 index 0000000..d7cade1 --- /dev/null +++ b/AdbLoopbackController/app/src/main/java/com/ttstd/adbloopback/tls/TlsPskClient.java @@ -0,0 +1,332 @@ +package com.ttstd.adbloopback.tls; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.security.SecureRandom; +import java.util.Arrays; + +/** + * 最小化 TLS 1.2 PSK 客户端,仅支持 {@code TLS_PSK_WITH_AES_128_GCM_SHA256}。 + * + *

Android 11+ 的「无线调试 → 使用配对码配对设备」走的就是这条 TLS-PSK 通道: + * 配对码同时作为 PSK 与 PSK identity。标准 Android (JSSE / Conscrypt) 不暴露 PSK 接口, + * 故这里用 Android 自带密码学原语(AES-GCM / HMAC / SHA-256)手搓实现。 + * + *

握手完成后,通过 {@link #getInputStream()}/{@link #getOutputStream()} 获得透明加密的流, + * 上层即可在其上跑 adb 的配对公钥交换协议。 + */ +public class TlsPskClient { + + // 仅支持这一个密码套件(与 AOSP adbd pairing 一致) + private static final int CIPHER_PSK_AES128_GCM_SHA256 = 0x00A8; + + private final Socket socket; + private InputStream in; + private OutputStream out; + private final byte[] psk; + private final byte[] identity; + + private final byte[] clientRandom = new byte[32]; + private final byte[] serverRandom = new byte[32]; + private byte[] clientWriteKey, serverWriteKey, clientWriteIV, serverWriteIV; + private byte[] masterSecret; + private long clientSeq = 0; + private long serverSeq = 0; + + private final ByteArrayOutputStream handshakeBuf = new ByteArrayOutputStream(); + + public TlsPskClient(String host, int port, String identityStr, byte[] psk) throws IOException { + this.psk = psk; + this.identity = identityStr.getBytes("UTF-8"); + this.socket = new Socket(); + this.socket.connect(new InetSocketAddress(host, port), 8000); + this.socket.setSoTimeout(8000); + this.socket.setTcpNoDelay(true); + this.in = socket.getInputStream(); + this.out = socket.getOutputStream(); + new SecureRandom().nextBytes(clientRandom); + } + + public InputStream getInputStream() { + return new TlsInputStream(); + } + + public OutputStream getOutputStream() { + return new TlsOutputStream(); + } + + public void handshake() throws Exception { + sendClientHello(); + readServerHelloAndDone(); + deriveKeys(); + sendClientKeyExchange(); + sendChangeCipherSpec(); + sendFinished(); + readServerFinished(); + } + + // ---------- 握手各阶段 ---------- + + private void sendClientHello() throws Exception { + byte[] sigAlgs = { + 0x00, 0x0c, // 签名算法列表长度 = 12 + 0x04, 0x03, // sha256 + ecdsa + 0x05, 0x03, // sha384 + ecdsa + 0x06, 0x03, // sha512 + ecdsa + 0x04, 0x01, // sha256 + rsa + 0x05, 0x01, // sha384 + rsa + 0x06, 0x01, // sha512 + rsa + 0x02, 0x01 // sha1 + rsa + }; + byte[] ext = concat(shortBytes(0x000d), shortBytes(sigAlgs.length), sigAlgs); + byte[] extensions = concat(shortBytes(ext.length), ext); + + byte[] cipherSuites = concat(shortBytes(2), new byte[]{(byte) 0x00, (byte) 0xA8}); + byte[] clientHello = concat( + new byte[]{0x03, 0x03}, // client_version TLS 1.2 + clientRandom, // 32 字节 + new byte[]{0x00}, // session_id 长度 0 + cipherSuites, // 4 字节 + new byte[]{0x01, 0x00}, // compression: 1 个方法, null(0x00) + extensions + ); + byte[] body = concat(new byte[]{0x01}, int24(clientHello.length), clientHello); + out.write(buildPlainRecord((byte) 22, body)); + out.flush(); + handshakeBuf.write(body); + } + + private void readServerHelloAndDone() throws Exception { + boolean gotServerHello = false; + boolean gotDone = false; + while (!gotDone) { + Record rec = readRecordAny(); + if (rec.type == 21) { + throw new IOException("TLS 握手收到 alert: " + + (rec.payload.length > 1 ? (rec.payload[1] & 0xff) : 0)); + } + if (rec.type != 22) { + throw new IOException("期望握手记录,实际类型: " + rec.type); + } + int off = 0; + byte[] p = rec.payload; + while (off < p.length) { + int htype = p[off] & 0xff; + int hlen = ((p[off + 1] & 0xff) << 16) | ((p[off + 2] & 0xff) << 8) | (p[off + 3] & 0xff); + byte[] hbody = Arrays.copyOfRange(p, off + 4, off + 4 + hlen); + handshakeBuf.write(p, off, 4 + hlen); + if (htype == 2) { + parseServerHello(hbody); + gotServerHello = true; + } else if (htype == 14) { + gotDone = true; + } + off += 4 + hlen; + } + } + if (!gotServerHello) { + throw new IOException("未收到 ServerHello"); + } + } + + private void parseServerHello(byte[] b) throws IOException { + System.arraycopy(b, 2, serverRandom, 0, 32); // version(2) + random(32) + int sidLen = b[34] & 0xff; + int pos = 35 + sidLen; + int cs = ((b[pos] & 0xff) << 8) | (b[pos + 1] & 0xff); + if (cs != CIPHER_PSK_AES128_GCM_SHA256) { + throw new IOException("服务端未选择 PSK_AES_128_GCM 密码套件,实际: 0x" + + Integer.toHexString(cs)); + } + } + + private void deriveKeys() throws Exception { + // PSK 预主密钥:other_secret(空) || psk + byte[] premaster = concat(new byte[]{0x00, 0x00}, shortBytes(psk.length), psk); + masterSecret = TlsCrypto.prf(premaster, "master secret", + concat(clientRandom, serverRandom), 48); + byte[] keyBlock = TlsCrypto.prf(masterSecret, "key expansion", + concat(serverRandom, clientRandom), 40); + clientWriteKey = Arrays.copyOfRange(keyBlock, 0, 16); + serverWriteKey = Arrays.copyOfRange(keyBlock, 16, 32); + clientWriteIV = Arrays.copyOfRange(keyBlock, 32, 36); + serverWriteIV = Arrays.copyOfRange(keyBlock, 36, 40); + } + + private void sendClientKeyExchange() throws Exception { + byte[] body = concat(new byte[]{0x10}, int24(2 + identity.length), + shortBytes(identity.length), identity); + out.write(buildPlainRecord((byte) 22, body)); + out.flush(); + handshakeBuf.write(body); + } + + private void sendChangeCipherSpec() throws IOException { + out.write(new byte[]{20, 3, 3, 0, 1, 1}); + out.flush(); + } + + private void sendFinished() throws Exception { + byte[] hash = TlsCrypto.sha256(handshakeBuf.toByteArray()); + byte[] verify = TlsCrypto.prf(masterSecret, "client finished", hash, 12); + byte[] body = concat(new byte[]{0x14}, int24(12), verify); + handshakeBuf.write(body); // 计入,供服务端 Finished 校验 + byte[] record = buildEncryptedRecord((byte) 23, body); + out.write(record); + out.flush(); + } + + private void readServerFinished() throws Exception { + Record r = readRecordAny(); + if (r.type == 20) { // 服务端 ChangeCipherSpec(明文) + r = readRecordAny(); + } + if (r.type != 23) { + throw new IOException("期望加密 Finished,实际类型: " + r.type); + } + byte[] plaintext = decryptRecord(r); + byte[] expected = TlsCrypto.prf(masterSecret, "server finished", + TlsCrypto.sha256(handshakeBuf.toByteArray()), 12); + byte[] actual = Arrays.copyOfRange(plaintext, 4, 16); + if (!Arrays.equals(expected, actual)) { + throw new IOException("TLS Finished 校验失败"); + } + } + + // ---------- 记录层 ---------- + + private static class Record { + int type; + byte[] payload; + Record(int type, byte[] payload) { + this.type = type; + this.payload = payload; + } + } + + private Record readRecordAny() throws IOException { + byte[] header = readExactly(5); + int type = header[0] & 0xff; + int len = ((header[3] & 0xff) << 8) | (header[4] & 0xff); + byte[] payload = readExactly(len); + return new Record(type, payload); + } + + private byte[] buildPlainRecord(byte contentType, byte[] body) { + byte[] header = {contentType, 3, 3, (byte) (body.length >> 8), (byte) body.length}; + return concat(header, body); + } + + private byte[] buildEncryptedRecord(byte contentType, byte[] plaintext) throws Exception { + long seq = clientSeq++; + byte[] explicit = longTo8(seq); + byte[] nonce = concat(clientWriteIV, explicit); + byte[] aad = {contentType, 3, 3, (byte) (plaintext.length >> 8), (byte) plaintext.length}; + byte[] ct = TlsCrypto.aesGcmEncrypt(clientWriteKey, nonce, plaintext, aad); + byte[] payload = concat(explicit, ct); + byte[] header = {contentType, 3, 3, (byte) (payload.length >> 8), (byte) payload.length}; + return concat(header, payload); + } + + private byte[] decryptRecord(Record r) throws Exception { + byte[] payload = r.payload; + byte[] explicit = Arrays.copyOfRange(payload, 0, 8); + byte[] ct = Arrays.copyOfRange(payload, 8, payload.length); + byte[] nonce = concat(serverWriteIV, explicit); + int plainLen = payload.length - 8 - 16; + byte[] aad = {23, 3, 3, (byte) (plainLen >> 8), (byte) plainLen}; + return TlsCrypto.aesGcmDecrypt(serverWriteKey, nonce, ct, aad); + } + + private byte[] readExactly(int n) throws IOException { + byte[] b = new byte[n]; + int off = 0; + while (off < n) { + int r = in.read(b, off, n - off); + if (r < 0) throw new IOException("TLS 连接断开 (EOF)"); + off += r; + } + return b; + } + + // ---------- 透明加密流 ---------- + + private class TlsInputStream extends InputStream { + private byte[] buf = new byte[0]; + private int bufPos = 0; + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int n = read(b, 0, 1); + return n <= 0 ? -1 : (b[0] & 0xff); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (bufPos >= buf.length) { + Record r = readRecordAny(); + if (r.type != 23) { + throw new IOException("TLS 流期望应用数据记录,实际: " + r.type); + } + try { + buf = decryptRecord(r); + } catch (Exception e) { + throw new IOException("TLS 解密失败", e); + } + bufPos = 0; + } + int avail = buf.length - bufPos; + int n = Math.min(len, avail); + System.arraycopy(buf, bufPos, b, off, n); + bufPos += n; + return n; + } + } + + private class TlsOutputStream extends OutputStream { + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] slice = Arrays.copyOfRange(b, off, off + len); + try { + byte[] record = buildEncryptedRecord((byte) 23, slice); + out.write(record); + out.flush(); + } catch (Exception e) { + throw new IOException("TLS 加密失败", e); + } + } + } + + // ---------- 字节工具 ---------- + + private static byte[] shortBytes(int v) { + return new byte[]{(byte) (v >> 8), (byte) v}; + } + + private static byte[] int24(int v) { + return new byte[]{(byte) (v >> 16), (byte) (v >> 8), (byte) v}; + } + + private static byte[] longTo8(long v) { + byte[] b = new byte[8]; + for (int i = 7; i >= 0; i--) { + b[i] = (byte) (v & 0xff); + v >>= 8; + } + return b; + } + + private static byte[] concat(byte[]... arrays) { + return TlsCrypto.concat(arrays); + } +} diff --git a/AdbLoopbackController/app/src/main/res/layout/activity_main.xml b/AdbLoopbackController/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..0cb5d1b --- /dev/null +++ b/AdbLoopbackController/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,236 @@ + + + + + + + + + +