feat: 增加密码连接

This commit is contained in:
2026-07-22 20:32:52 +08:00
parent ef786b1529
commit bdde6ccd2e
19 changed files with 541 additions and 25 deletions

View File

@@ -32,6 +32,7 @@ import com.ttstd.controlled.base.BaseMvvmActivity;
import com.ttstd.controlled.databinding.ActivityMainBinding;
import com.ttstd.controlled.service.ScreenCaptureService;
import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
import com.ttstd.controlled.utils.AuthSettings;
import com.ttstd.controlled.utils.DeviceUtils;
import com.ttstd.controlled.utils.SignatureUtils;
import com.ttstd.controlled.webrtc.WebRtcClient;
@@ -88,6 +89,31 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
binding.btnStart.setOnClickListener(v -> startScreenSharing());
binding.btnStop.setOnClickListener(v -> stopScreenSharing());
// 安全验证:显示动态验证码,提供重新生成与固定密码设置
binding.tvDynamicCode.setText(AuthSettings.getDynamicCode(this));
binding.btnRegenerateCode.setOnClickListener(v -> {
String newCode = AuthSettings.generateDynamicCode();
AuthSettings.setDynamicCode(this, newCode);
binding.tvDynamicCode.setText(newCode);
binding.tvAuthHint.setText("动态验证码已重新生成");
});
binding.btnSavePassword.setOnClickListener(v -> {
String p1 = binding.etFixedPassword.getText().toString();
String p2 = binding.etConfirmPassword.getText().toString();
if (p1.isEmpty()) {
binding.tvAuthHint.setText("固定密码不能为空");
return;
}
if (!p1.equals(p2)) {
binding.tvAuthHint.setText("两次输入的密码不一致");
return;
}
AuthSettings.setFixedPassword(this, p1);
binding.etFixedPassword.setText("");
binding.etConfirmPassword.setText("");
binding.tvAuthHint.setText("固定密码已保存");
});
updateUI(false);
}

View File

@@ -37,6 +37,7 @@ import com.ttstd.controlled.input.ShellInputUtils;
import com.ttstd.controlled.input.SystemInputUtils;
import com.ttstd.controlled.utils.SignatureUtils;
import com.ttstd.controlled.signaling.SignalMessage;
import com.ttstd.controlled.utils.AuthSettings;
import com.ttstd.controlled.signaling.WebSocketClient;
import com.ttstd.controlled.webrtc.WebRtcClient;
@@ -457,18 +458,78 @@ public class ScreenCaptureService extends Service {
}
String controllerId = message.getFromDeviceId();
// 暂存请求,弹出确认对话框,交由用户决定是否接受。
// 暂存请求信息
pendingControllerId = controllerId;
pendingControllerName = (controllerName != null && !controllerName.isEmpty())
? controllerName : controllerId;
pendingOfferSdp = offerSdp;
// 鉴权:动态验证码(CODE) / 固定密码(PASSWORD)。
// 服务器仅做转发,真实校验在此完成(密钥不离开被控端)。
String authType = message.getAuthType();
String authValue = message.getAuthValue();
if (authType != null && !authType.trim().isEmpty()) {
String failReason = verifyAuth(authType.trim(), authValue);
if (failReason == null) {
// 校验通过:自动接受,直接建立连接(无需手动确认)
Log.i(TAG, "Auth passed (" + authType + "), auto-accepting " + controllerId);
mainHandler.post(this::acceptConnection);
} else {
Log.w(TAG, "Auth failed (" + authType + "): " + failReason + " from " + controllerId);
sendAuthRejected(failReason);
}
return;
}
// 未携带鉴权信息:沿用原有手动确认弹窗
Intent dialogIntent = new Intent(this, ConnectionRequestActivity.class);
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId);
startActivity(dialogIntent);
}
/**
* 校验控制端发来的鉴权信息。返回 null 表示通过,否则返回失败原因。
* CODE: 与被控端当前动态验证码比对;
* PASSWORD: 与被控端固定密码比对。
*/
private String verifyAuth(String authType, String authValue) {
if (authValue == null) authValue = "";
if ("CODE".equalsIgnoreCase(authType)) {
String expected = AuthSettings.getDynamicCode(this);
if (expected == null || expected.isEmpty()) {
return "被控端未生成动态验证码";
}
return expected.equals(authValue) ? null : "动态验证码错误";
} else if ("PASSWORD".equalsIgnoreCase(authType)) {
String expected = AuthSettings.getFixedPassword(this);
if (expected == null || expected.isEmpty()) {
return "被控端未设置固定密码";
}
return expected.equals(authValue) ? null : "固定密码错误";
}
return "未知的鉴权类型: " + authType;
}
/** 鉴权失败时,向控制端回送带原因的拒绝消息并清理待处理状态。 */
private void sendAuthRejected(String reason) {
if (pendingControllerId == null || wsClient == null) {
return;
}
String controllerId = pendingControllerId;
clearPendingRequest();
SignalMessage msg = new SignalMessage();
msg.setType("CONNECTION_REJECTED");
msg.setFromDeviceId(deviceId);
msg.setToDeviceId(controllerId);
msg.setDeviceType("CONTROLLED");
JsonObject payload = new JsonObject();
payload.addProperty("reason", reason);
msg.setPayload(payload.toString());
wsClient.sendMessage(msg);
Log.i(TAG, "Sent CONNECTION_REJECTED(" + reason + ") to " + controllerId);
}
/**
* 用户在弹窗中点击“接受”:创建 Answer 完成连接。
*/

View File

@@ -7,6 +7,8 @@ public class SignalMessage {
private String toDeviceId;
private String deviceType;
private String payload;
private String authType;
private String authValue;
public SignalMessage() {}
@@ -31,4 +33,10 @@ public class SignalMessage {
public String getPayload() { return payload; }
public void setPayload(String payload) { this.payload = payload; }
public String getAuthType() { return authType; }
public void setAuthType(String authType) { this.authType = authType; }
public String getAuthValue() { return authValue; }
public void setAuthValue(String authValue) { this.authValue = authValue; }
}

View File

@@ -0,0 +1,60 @@
package com.ttstd.controlled.utils;
import android.content.Context;
import android.content.SharedPreferences;
import java.security.SecureRandom;
/**
* 被控端安全验证配置:动态验证码与固定密码。
* 密钥仅保存在被控端本机,服务器只做转发,不接触任何密钥。
*/
public class AuthSettings {
private static final String PREF_NAME = "controlled_auth_prefs";
private static final String KEY_DYNAMIC_CODE = "dynamic_code";
private static final String KEY_FIXED_PASSWORD = "fixed_password";
private static final String CODE_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final int CODE_LENGTH = 6;
private static final SecureRandom RANDOM = new SecureRandom();
/** 生成一个 6 位、由大小写字母与数字组成的随机验证码。 */
public static String generateDynamicCode() {
StringBuilder sb = new StringBuilder(CODE_LENGTH);
for (int i = 0; i < CODE_LENGTH; i++) {
sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length())));
}
return sb.toString();
}
/**
* 获取当前动态验证码;若不存在则随机生成并持久化。
*/
public static String getDynamicCode(Context context) {
SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String code = sp.getString(KEY_DYNAMIC_CODE, null);
if (code == null || code.isEmpty()) {
code = generateDynamicCode();
sp.edit().putString(KEY_DYNAMIC_CODE, code).apply();
}
return code;
}
public static void setDynamicCode(Context context, String code) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit().putString(KEY_DYNAMIC_CODE, code == null ? "" : code).apply();
}
/** 获取固定密码(未设置时为空字符串)。 */
public static String getFixedPassword(Context context) {
return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.getString(KEY_FIXED_PASSWORD, "");
}
public static void setFixedPassword(Context context, String password) {
context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
.edit().putString(KEY_FIXED_PASSWORD, password == null ? "" : password).apply();
}
}

View File

@@ -61,6 +61,85 @@
android:textSize="16sp"
android:textStyle="bold" />
<!-- 安全验证:动态验证码 + 固定密码 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="6dp"
android:text="安全验证"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="动态验证码(连接时由控制端输入):"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/tv_dynamic_code"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="------"
android:textSize="22sp"
android:textStyle="bold"
android:fontFamily="monospace"
android:letterSpacing="0.1" />
<Button
android:id="@+id/btn_regenerate_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="重新生成" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="固定密码(手动设置并确认):"
android:textSize="14sp" />
<EditText
android:id="@+id/et_fixed_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="输入固定密码"
android:inputType="textPassword" />
<EditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="再次输入固定密码"
android:inputType="textPassword" />
<Button
android:id="@+id/btn_save_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="保存固定密码" />
<TextView
android:id="@+id/tv_auth_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textSize="13sp"
android:textColor="#888888" />
<Button
android:id="@+id/btn_start"
android:layout_width="match_parent"