[feat] add Android trusted device authentication

This commit is contained in:
dijunkun committed 2026-11-13 11:07:00 +08:00
1 parent e6e72ed511
commit 94a5f57bea
5 files changed
+91 -6

No files matched your search

@@ -397,6 +397,8 @@ public final class MainActivity extends Activity implements NativeSession.Listen
toggle.setOnCheckedChangeListener((button,checked)->{inputs.setVisibility(checked?View.VISIBLE:View.GONE);if(!checked){host="api.crossdesk.cn";port=9099;preferences.edit().putString("host",host).putInt("port",port).apply();ensureSignaling();} });
backAction=()->{if(toggle.isChecked()&&!saveServer(address,number))return;hideKeyboard();home("");};
identitySummary=text("",12,false);identitySummary.setTextColor(SECONDARY);identitySummary.setGravity(Gravity.CENTER);identitySummary.setPadding(dp(16),dp(12),dp(16),dp(16));identitySummary.setTextIsSelectable(true);content.addView(identitySummary);renderSignaling();
LinearLayout trust=section(content,L10n.tr("本机配对信息"),MobileUi.SETTINGS_CARD_RADIUS);
link(trust,L10n.tr("查看指纹"),"",this::showPairingInfo);ui.gap(content,28);
LinearLayout privacy=section(content,L10n.tr("隐私"),MobileUi.SETTINGS_CARD_RADIUS);link(privacy,L10n.tr("隐私与授权"),preferences.getBoolean("networkConsent",false)?L10n.tr("已授权"):L10n.tr("未授权"),this::privacyPage);ui.divider(privacy);
LinearLayout preview=row();preview.setPadding(dp(16),dp(10),dp(16),dp(10));preview.addView(text(L10n.tr("保存远程画面预览"),16,false),new LinearLayout.LayoutParams(0,-2,1));previewSwitch=new Switch(this);previewSwitch.setContentDescription(L10n.tr("保存远程画面预览"));preview.addView(previewSwitch);privacy.addView(preview);
previewSwitch.setOnCheckedChangeListener((button,checked)->{if(updatingPreviewSwitch)return;if(checked){renderPreviews();confirmPreviewSaving();}else previews.setEnabled(false);});
@@ -406,6 +408,21 @@ public final class MainActivity extends Activity implements NativeSession.Listen
aboutUpdateLabel=(TextView)aboutLink.getChildAt(1);aboutUpdateLabel.setPadding(0,0,dp(8),0);
aboutUpdateDot=updateDot();LinearLayout.LayoutParams dotPosition=new LinearLayout.LayoutParams(dp(8),dp(8));dotPosition.setMarginEnd(dp(8));aboutLink.addView(aboutUpdateDot,2,dotPosition);renderAppUpdates();
}
private void showPairingInfo(){
NativeSession.pairingInfo(this,localIdentity,info->{
if(isFinishing()||isDestroyed()||!page.equals("settings"))return;
if(info==null){toast(L10n.tr("安全存储不可用,请解锁设备后重试"));return;}
TextView fingerprint=text(info.optString("fingerprint").replaceAll("(.{4})(?!$)","$1 "),14,false);
fingerprint.setTypeface(android.graphics.Typeface.MONOSPACE);fingerprint.setTextIsSelectable(true);fingerprint.setPadding(dp(24),dp(16),dp(24),dp(16));
new AlertDialog.Builder(this).setTitle(L10n.tr("本机配对信息"))
.setMessage(L10n.tr("将配对信息添加到被控端的可信设备列表,并核对指纹。"))
.setView(fingerprint).setNegativeButton(L10n.tr("取消"),null)
.setPositiveButton(L10n.tr("复制配对信息"),(dialog,which)->{
try{((ClipboardManager)getSystemService(CLIPBOARD_SERVICE)).setPrimaryClip(ClipData.newPlainText("CrossDesk",info.getString("pairing")));toast(L10n.tr("配对信息已复制。"));}
catch(Exception error){toast(L10n.tr("无法复制配对信息,请重试。"));}
}).show();
});
}
private void confirmPreviewSaving(){
previewDialog=new AlertDialog.Builder(this).setTitle(L10n.tr("保存远程画面预览?"))
.setMessage(L10n.tr("预览可能包含远程电脑上的个人或工作信息。开启后,每次连接会自动将一张画面保存在本机并显示在最近连接中。你可以随时关闭并清除。"))
@@ -94,7 +94,7 @@ final class NativeSession implements AutoCloseable {
File certificates = exportTrust();
File logs = new File(context.getNoBackupFilesDir(), "logs");
if (!logs.isDirectory() && !logs.mkdirs()) throw new IllegalStateException("Log directory unavailable");
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath());
handle = nCreate(this, host, port, identity, logs.getAbsolutePath(), certificates.getAbsolutePath(), trustKey(context));
if (handle == 0) { fail(L10n.tr("原生连接初始化失败")); return; }
nFileDirectory(handle,new File(documents.directory,"received").getAbsolutePath());
if (surface != null && surface.isValid()) nSurface(handle, surface);
@@ -116,6 +116,33 @@ final class NativeSession implements AutoCloseable {
});
}
boolean isClosed() { return closed; }
// Called only on RTC, shared by every foreground/background session and UI export.
// This key belongs to the installation, not to a signaling server or login ID.
private static String trustKey(Context context) throws Exception {
SecretStore store = new SecretStore(context);
var preferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE);
String seed = store.get("device-trust:v1");
if (seed.isEmpty()) {
if (preferences.getBoolean("deviceTrustInitialized", false))
throw new IllegalStateException("Device signing identity missing");
seed = nGenerateTrustKey();
if (seed == null) throw new IllegalStateException("Device signing identity unavailable");
store.put("device-trust:v1", seed);
}
if (!preferences.edit().putBoolean("deviceTrustInitialized", true).commit())
throw new IllegalStateException("Device signing identity marker unavailable");
return seed;
}
static void pairingInfo(Context context, String deviceId, java.util.function.Consumer<JSONObject> completed) {
Context app = context.getApplicationContext();
RTC.execute(() -> {
JSONObject info = null;
try { info = new JSONObject(nTrustInfo(trustKey(app), deviceId.split("@", 2)[0])); }
catch (Exception ignored) { }
JSONObject result = info;
new Handler(Looper.getMainLooper()).post(() -> completed.accept(result));
});
}
// UI callbacks run on the main thread. A service-owned state can replace
// the Activity listener without recreating either native peer.
void setListener(Listener value) { listener = java.util.Objects.requireNonNull(value); }
@@ -347,7 +374,9 @@ final class NativeSession implements AutoCloseable {
static native boolean nValidAppVersion(String version);
static native boolean nHasAppUpdate(String version, byte[] releaseJson);
static native String nCheckMobileUpdate(String version, byte[] releaseJson);
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots);
private static native long nCreate(NativeSession owner, String host, int port, String identity, String logs, String roots, String trustKey);
private static native String nGenerateTrustKey();
private static native String nTrustInfo(String seed, String deviceId);
private static native boolean nConnect(long handle, String identity, String remote, String password);
private static native void nReady(long handle,byte[] displayResolutions);
private static native void nPointer(long handle, float x, float y, int flag, int wheel);