project: 增加web和adb控制,未测试
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 消息封装与读写。
|
||||
*
|
||||
* <pre>
|
||||
* struct message {
|
||||
* unsigned command; // 命令
|
||||
* unsigned arg0; // 第一参数(本地 id)
|
||||
* unsigned arg1; // 第二参数(远端 id)
|
||||
* unsigned data_length; // 负载长度
|
||||
* unsigned data_crc32; // 负载 CRC32
|
||||
* unsigned magic; // command ^ 0xffffffff
|
||||
* };
|
||||
* </pre>
|
||||
* 所有整型字段均为小端序(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();
|
||||
}
|
||||
}
|
||||
@@ -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 公钥。
|
||||
*
|
||||
* <p>配对成功后,本 App 的公钥被加入 adbd 的授权列表,之后即可用同一密钥对
|
||||
* 无线调试主端口发起 ADB 连接(无需电脑 / Shizuku)。
|
||||
*
|
||||
* <p>线规(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<byte[]> 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;
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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}。
|
||||
*
|
||||
* <p>Android 11+ 的「无线调试 → 使用配对码配对设备」走的就是这条 TLS-PSK 通道:
|
||||
* 配对码同时作为 PSK 与 PSK identity。标准 Android (JSSE / Conscrypt) 不暴露 PSK 接口,
|
||||
* 故这里用 Android 自带密码学原语(AES-GCM / HMAC / SHA-256)手搓实现。
|
||||
*
|
||||
* <p>握手完成后,通过 {@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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user