[feat] add HarmonyOS mobile trusted device authentication

This commit is contained in:
dijunkun committed 2026-11-13 11:07:00 +08:00
1 parent 8a1e58d62d
commit dec9e445c4
9 files changed
+121 -3

No files matched your search

@@ -3,6 +3,40 @@
#include <string>
namespace {
napi_value TrustKey(napi_env env, napi_callback_info) {
try {
const auto seed = crossdesk::harmony::MobileGenerateTrustKey();
napi_value result;
napi_create_string_utf8(env, seed.data(), seed.size(), &result);
return result;
} catch (const std::exception&) {
napi_throw_error(env, nullptr, "Device signing identity unavailable"); return nullptr;
}
}
napi_value TrustInfo(napi_env env, napi_callback_info info) {
size_t count = 2; napi_value arguments[2];
if (napi_get_cb_info(env, info, &count, arguments, nullptr, nullptr) != napi_ok || count != 2) {
napi_throw_type_error(env, nullptr, "Expected device identity and public ID"); return nullptr;
}
std::string values[2];
for (size_t i = 0; i < 2; ++i) {
size_t size = 0;
if (napi_get_value_string_utf8(env, arguments[i], nullptr, 0, &size) != napi_ok || size > 128) {
napi_throw_type_error(env, nullptr, "Invalid device identity"); return nullptr;
}
values[i].resize(size + 1);
napi_get_value_string_utf8(env, arguments[i], values[i].data(), values[i].size(), &size);
values[i].resize(size);
}
try {
const auto info = crossdesk::harmony::MobileTrustInfo(values[0], values[1]);
napi_value result;
napi_create_string_utf8(env, info.data(), info.size(), &result);
return result;
} catch (const std::exception&) {
napi_throw_error(env, nullptr, "Device signing identity unavailable"); return nullptr;
}
}
napi_value Command(napi_env env, napi_callback_info info) {
size_t count = 1; napi_value argument;
if (napi_get_cb_info(env, info, &count, &argument, nullptr, nullptr) != napi_ok || count != 1) {
@@ -24,6 +58,8 @@ napi_value Poll(napi_env env, napi_callback_info) {
}
napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor properties[] = {
{"mobileGenerateTrustKey", nullptr, TrustKey, nullptr, nullptr, nullptr, napi_default, nullptr},
{"mobileTrustInfo", nullptr, TrustInfo, nullptr, nullptr, nullptr, napi_default, nullptr},
{"mobileCommand", nullptr, Command, nullptr, nullptr, nullptr, napi_default, nullptr},
{"mobilePoll", nullptr, Poll, nullptr, nullptr, nullptr, napi_default, nullptr}};
napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties);
@@ -1,2 +1,4 @@
export const mobileCommand: (json: string) => void;
export const mobilePoll: () => string;
export const mobileGenerateTrustKey: () => string;
export const mobileTrustInfo: (seed: string, deviceId: string) => string;
@@ -12,6 +12,7 @@ import { AnnouncementInbox, AnnouncementItem, AnnouncementRequest, AnnouncementR
import { VideoSettings, VideoValues, NetworkStatistics, NetworkReport, RecentPresence, PresenceMessage, asciiKey } from './ControllerModels';
interface Command {
trustKey?: string;
displayResolutions?: ResolutionPreference[];
displayId?: number; displayKey?: string; width?: number; height?: number;
op: string; session?: number; connection?: number; server?: string; port?: number; identity?: string;
@@ -34,9 +35,11 @@ interface VideoCodecStatus { encoder: string; }
interface ControlData { display_resolution?: ResolutionRequest; display_resolutions?: ResolutionDisplay[]; type: number; host_info?: HostInfo; video_settings?: SettingAck; service_status?: ServiceStatus; privacy_status?: PrivacyStatus; video_codec?: VideoCodecStatus; }
interface PresenceRequest { type: string; devices: string[]; subscribe: boolean; }
interface SignalType { type: string; }
interface TrustInfo { pairing: string; fingerprint: string; }
@Observed
export class MobileState {
trustFingerprint: string = '';
page: string = 'home';
consent: boolean = false;
privacyAvailable: boolean = false;
@@ -200,12 +203,13 @@ export class MobileController {
this.starting = true; const session = ++this.session, scope = this.scope();
try {
let identity = await this.store.secret(`identity:${scope}`);
const trustKey = await this.store.trustKey();
// A public ID alone is not a login credential. Discard legacy incomplete records.
if (identity && !/^[0-9]{9}@.+$/.test(identity)) { await this.store.removeSecret(`identity:${scope}`); identity = ''; }
if (session !== this.session || !this.foreground || !this.state.consent) return;
this.state.signalText = tr("正在连接…"); this.state.signal = 0; this.started = true;
this.send({ op: 'start', session: session, server: this.state.server, port: Number(this.state.port), identity: identity,
logs: this.context.cacheDir, certificates: '/etc/security/certificates', directory: this.store.directory,
trustKey: trustKey, logs: this.context.cacheDir, certificates: '/etc/security/certificates', directory: this.store.directory,
previewDirectory: this.store.previewDirectory, version: this.state.version });
} catch { this.state.signalText = tr("安全存储不可用,请解锁设备后重试"); this.state.signal = -1; }
finally { if (session === this.session) this.starting = false; }
@@ -228,6 +232,20 @@ export class MobileController {
this.navigate('settings');
}
navigate(page: string): void { this.state.page = page; this.state.modal = ''; }
async pairingInfo(copy: boolean): Promise<void> {
try {
const seed = await this.store.trustKey();
const info = JSON.parse(bridge.mobileTrustInfo(seed, this.state.identity)) as TrustInfo;
if (!this.foreground || this.state.page !== 'settings') return;
this.state.trustFingerprint = info.fingerprint.replace(/(.{4})(?!$)/g, '$1 ');
if (copy) {
try {
await pasteboard.getSystemPasteboard().setData(pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, info.pairing));
this.toast(tr("配对信息已复制。"));
} catch { this.toast(tr("无法复制配对信息,请重试。")); }
}
} catch { this.toast(tr("安全存储不可用,请解锁设备后重试")); }
}
history(): void {
const values = this.store.recent(this.scope()), now = this.now(); this.presence?.watch(values.map((d: RecentDevice) => d.id), now);
this.state.recent = values.filter((d: RecentDevice) => this.presence?.online(d.id, now))
@@ -6,9 +6,11 @@ import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { util } from '@kit.ArkTS';
import { fileIo as fs } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import bridge from 'libcrossdesk.so';
export interface RecentDevice { id: string; name: string; platform: string; remember: boolean; preview: string; }
export class MobileStore {
private static trustLoading?: Promise<string>;
private data: preferences.Preferences;
readonly directory: string;
readonly previewDirectory: string;
@@ -59,6 +61,25 @@ export class MobileStore {
try { await asset.remove(await this.query(key)); }
catch (error) { if ((error as ErrorWithCode).code !== asset.ErrorCode.NOT_FOUND) throw new Error(tr("无法删除已保存的凭据")); }
}
async trustKey(): Promise<string> {
// Serializes pairing export and reconnects, including overlapping page lifetimes.
if (MobileStore.trustLoading) return MobileStore.trustLoading;
const task = this.loadTrustKey(); MobileStore.trustLoading = task;
try { return await task; }
finally { if (MobileStore.trustLoading === task) MobileStore.trustLoading = undefined; }
}
private async loadTrustKey(): Promise<string> {
let seed = await this.secret('device-trust:v1');
if (!seed) {
if (this.flag('deviceTrustInitialized')) throw new Error('Device signing identity missing');
seed = bridge.mobileGenerateTrustKey();
await this.saveSecret('device-trust:v1', seed);
}
// Validate persisted data without replacing a corrupt/unavailable identity.
bridge.mobileTrustInfo(seed, '');
this.put('deviceTrustInitialized', true);
return seed;
}
recent(scope: string): RecentDevice[] {
try {
const entries = JSON.parse(this.text(`recent:${scope}`, '[]')) as RecentDevice[];
@@ -304,6 +304,18 @@ struct Mobile {
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line }).margin({ top: 28 })
Text(this.state.identity ? tr("本机 ID {0}", [String(this.formatId(this.state.identity))]) : this.state.signalText).fontSize(12).fontColor(MobileTheme.secondary)
.textAlign(TextAlign.Center).padding({ left: 16, right: 16, top: 12, bottom: 16 }).width('100%').copyOption(CopyOptions.LocalDevice)
this.SectionTitle(tr("本机配对信息"))
Column() {
this.SettingRow(tr("查看指纹"), '', () => this.controller?.pairingInfo(false))
if (this.state.trustFingerprint) {
Text(this.state.trustFingerprint).fontSize(14).fontFamily('monospace').fontColor(MobileTheme.secondary)
.padding(16).width('100%').copyOption(CopyOptions.LocalDevice)
}
this.Rule()
this.SettingRow(tr("复制配对信息"), '', () => this.controller?.pairingInfo(true))
}.width('100%').backgroundColor(Color.White).borderRadius(26).clip(true).border({ width: 0.5, color: MobileTheme.line })
Text(tr("将配对信息添加到被控端的可信设备列表,并核对指纹。"))
.fontSize(12).fontColor(MobileTheme.secondary).width('100%').padding({ left: 16, right: 16, top: 8, bottom: 12 })
this.SectionTitle(tr("隐私"))
Column() {
this.SettingRow(tr("隐私与授权"), this.state.consent ? tr("已授权") : tr("未授权"), () => this.navigate('privacy'))