鸿蒙安全浏览器 qxj_harmony_next_pad_nsp_browser
「风云安全浏览器」(bundleName cn.ac.nsp.qxj.nsp_browser)是 QXJ 方案的终端入口与主交付物: 基于 HarmonyOS NEXT 的 NSP 定制安全浏览器,集成 SM2 密钥协商、SM4-GCM 加密登录、 设备注册、服务器配置管理、WebView 浏览(暗色/主题/UA 切换) 等能力, 采用「一多」架构覆盖 tablet / phone / pc / tv / wearable 五种设备形态。
- targetSdkVersion:6.1.1(24);compatibleSdkVersion:5.0.5(17)
- 三种 product flavor:
appstore/appgallery/default - 本文以
main分支 README 与features/qxj_sdk/README.md为准整理; 多标签相关内容仅适用dev分支
main 与 dev 双版本
两分支并存,共同祖先为 06f5f3d:
| 项 | main(稳定线) | dev(功能线) |
|---|---|---|
| versionName | 4.1.54 | 5.2.89 |
| versionCode | 4001054 | 5002089 |
| 浏览器形态 | 单页模式:一个 Web 组件,Tabs 承载浏览器/设备等业务分区 | 多标签 v2:离线 Web 组件(OffscreenWeb + BuilderNode/NodeController),多标签并存 |
| 独有提交 | 34(dev..main) | 93(main..dev) |
| 额外权限 | 4 项 | 6 项(多 PRINT、READ_WRITE_DOWNLOAD_DIRECTORY) |
dev 主要功能(main 尚未合入):多标签 v2(标签条 / 标签管理页 / 悬浮玻璃条与悬浮球)、 证书面板 v2(完整证书链)、网站信息独立页、页内查找、Chrome 风格长按菜单、下载管理、 菜单增强(强制刷新/清除数据/工具分区)、物理键盘快捷键、Chrome 风格错误页、页面缩放 / 无图模式 / 禁用 JavaScript、会话恢复、地址栏自动补全、后台标签暂停媒体、打印、用户脚本(油猴风格)、 历史左滑删除、PC/TV/穿戴产品骨架、应用内 DevTools、无痕模式、网页截图、广告拦截、PDF 导出。
main 独有内容:单页稳定版、ArkWeb 自签证书确认框、地址栏锁图标查看证书链、 网站访问控制与 JSB 白名单页面、JSB 应用侧白名单与结构化 JSON 桥接口、 tablet debug HdsTabs 悬浮液态玻璃条。两分支近期通过 cherry-pick 同步安全修复。
19 个 feature 分支:address-suggest / cert-v2 / context-menu / downloads / error-page / find-in-page / keyboard-shortcuts / media-pause-background / swipe-delete / user-scripts / menu-enhance / menu-tools / navigation-hardening / page-controls / print-page / security-jsbridge-whitelist / security-log-masking / session-restore / tab-v2。
功能特性全列表
| 特性 | 说明 |
|---|---|
| 加密登录 | 账密/验证码明文经 SM4-GCM 加密后封装自定义二进制帧提交,下行帧解密获取 JWT |
| 设备注册 | SM2 密钥对自动生成,二维码/导出文件两种方式提交管理员绑定 |
| 密钥协商 | SM2 四步握手(INIT/RESP/ACK/TOKEN)协商 SM4 会话密钥,详见 SM2 握手流程 |
| 服务器配置管理 | 多服务器增删改查、扫码绑定公钥、JSON 导入导出,详见 服务器管理 |
| WebView 浏览 | 前进后退、历史记录、搜索引擎切换、网页暗色、UA 切换、图片屏蔽;多标签仅 dev 5.2.x 支持(main 4.1.x 为单页模式),详见 浏览器功能 |
| 界面主题 | 工具栏 10 套主题色 + 浏览器自身界面亮/暗双模式(默认暗色),详见 主题定制 |
| 网站访问控制 | 白名单/黑名单管理,可限制只能访问企业部署的固定站点(默认关闭仅维护列表),详见 网站访问控制 |
技术栈明细
| 领域 | 方案 |
|---|---|
| 平台 | HarmonyOS NEXT,targetSdkVersion 6.1.1(24) / compatible 5.0.5(17)(ArkTS / ArkUI V1 与 V2 混用) |
| 架构 | MVVM(BaseState @Observed + BaseVM + BaseVMEvent),UI 全局态走 AppStorage/@StorageLink |
| 加密 | @nsp/qxj-sdk(C++ SO + AKI 胶水层):SM2 非对称 / SM4-GCM 对称,详见 加密体系 |
| 通信协议 | 自定义二进制帧(QxjFrameUtil,64B 头 + SM4-GCM 密文),与后端 apps/keymgr/utils/packet_parser.py 对齐 |
| HTTP | 原生 @ohos.net.http,自签名证书显式配 CA |
| 定位 | 高德 AMap(@amap/amap_lbs_location) |
| 日志 | hilog 封装(common/util/Logger.ets),全工程统一入口 |
| 扫码 | @pura/picker_utils + ScanKit |
| 持久化 | PersistentStorage(UI 状态)/ AssetUtil(密钥类)/ preferences(配置类) |
更多平台层架构说明见 HarmonyOS 架构。
环境要求与依赖
- 开发工具:DevEco Studio(HarmonyOS NEXT 配套版本),使用工程内置
hvigorw构建 - SDK:targetSdkVersion
6.1.1(24),compatibleSdkVersion5.0.5(17) - products 依赖:
@ohos/common、@ohos/browser、@ohos/device、@nsp/qxj-sdk(均file:本地引用)、@pura/harmony-utils、@pura/picker_utils、高德amap_lbs_common/amap_lbs_location/ map3d - qxj_sdk 依赖:
@ohos/aki ^1.2.21,内含libnspsm2handshake.so、libnspsmapi.so(arm64-v8a / x86_64) - 构建开启 strictMode 与
useNormalizedOHMUrl;混淆规则文件存在但默认enable: false
目录结构
AppScope/ # 应用级配置(app.json5:versionCode/versionName)与图标资源
common/ # 公共模块(HAR,被所有 feature 依赖)
Index.ets # 统一导出入口(@ohos/common)
BuildProfile.ets # DEBUG / BUILD_MODE_NAME 构建开关
src/main/ets/
constant/ # 常量(CommonConstants、DeviceConstants/StorageKey)
model/ # 数据模型(ServerConfig、DeviceModels、GlobalInfoModel)
util/ # 工具类(Logger、HttpUtil、LoginUtil、KeyNegotiationUtil、
# SM2HandshakeUtil、QxjFrameUtil、DeviceKeyUtil、AuthApiUtil、
# BrowserUiModeUtil、JsBridgeHandle、LocationUtil 等)
viewmodel/ # MVVM 基类(BaseState/BaseVM/BaseVMEvent)
features/
browser/ # 浏览器业务模块
src/main/ets/
common/ # BrowserHistory、BrowserThemeUtil、SearchEngineUtil、
# WebDarkModeUtil、WebViewModeUtil(均为静态工具 + AppStorage/prefs)
view/ # BrowserView(主视图)、BrowserMenuView、BrowserHistoryView、
# SearchEngineSettingsView、SettingsMenuDialog、AboutView
device/ # 设备注册/服务器配置模块(标准 MVVM 示例)
src/main/ets/
view/ # DeviceRegisterView、ServerConfigView
viewmodel/ # State(@Observed)/VM/Event,事件驱动 sendEvent
helloworld/ # NDK C++ 示例(nsp_helloworld.so,演示用)
qxj_sdk/ # 加密 SDK C++ 源码(handshake.so 构建)
showcase/ # 组件演示页(非业务,仅调试期使用)
products/ # 无 entry/ 目录,按设备形态出多 entry
tablet/ # 平板 entry(主交付形态,完整页面)
phone/ # 手机 entry(页面同构,约 21 个 @Entry 页)
pc/ tv/ wearable/ # 骨架模块(仅默认页)
{tablet,phone}/src/main/ets/
entryability/EntryAbility.ets # 启动入口(详见下文启动流程)
pages/ # @Entry 页面路由(详见下文页面清单)
hpack/ # 自研打包:Python 构建脚本与签名材料(cer/p12/p7b)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
各模块职责对照:
| 目录 | 类型 | 职责 |
|---|---|---|
AppScope/ | 应用级 | app.json5(版本号)、图标与标签 |
common/ | HAR @ohos/common | 常量、模型、全部核心工具、MVVM 基类 |
features/browser/ | HAR @ohos/browser | 浏览核心:BrowserView、菜单、历史/收藏、主题(10 套)、搜索引擎、站点访问控制 |
features/device/ | HAR @ohos/device | 设备注册与服务器配置(标准 MVVM 范例) |
features/qxj_sdk/ | HAR @nsp/qxj-sdk | 国密 SDK:C++(MIRACL + NAPI/AKI)+ ETS 封装 |
features/helloworld/、features/showcase/ | HAR | 示例 / 演示 |
products/tablet、products/phone | entry HAP | 平板 / 手机入口 |
products/pc、tv、wearable | entry HAP | PC/TV/穿戴骨架 |
hpack/ | 自研打包 | Python 构建脚本与签名材料(cer/p12/p7b) |
页面路由(products/phone 与 products/tablet 的 pages)
| 页面 | 说明 |
|---|---|
MainPage | 主容器,Tabs 承载浏览器/设备等 Tab,处理返回键 |
LoginPage | 加密登录页(账密 + 短信/邮件验证码) |
DeviceRegisterPage | 设备注册(内嵌 DeviceRegisterView) |
ServerConfigPage | 服务器配置管理(内嵌 ServerConfigView) |
ServerEditPage | 新增/编辑服务器,支持扫码填充 JSON |
KeyNegotiationPage | SM2 密钥协商流程页(INIT→RESP→ACK→TOKEN) |
BrowserMenuPage / BrowserHistoryPage | 浏览器菜单 / 历史记录 |
SearchEngineSettingsPage | 搜索引擎设置 |
SiteAccessSettingsPage | 网站访问管理(白名单/黑名单) |
AboutPage | 关于(内嵌 AboutView,版本号运行时读 app.json5) |
DebugInfoPage | 调试信息页,仅 DEBUG 构建可见 |
Sm2HandshakeDemoPage / Showcase*Page | 演示页,非业务 |
签名与构建打包全流程
签名材料与 signingConfigs
签名材料集中放在 hpack/sign/:nsp_browser.cer(证书)、nsp_browser.p12(密钥库)、 nsp_browserRelease.p7b(发布 Profile)、nsp_browser_test_releaseRelease.p7b(测试 Profile), 签名算法 SHA256withECDSA。工程根 build-profile.json5(以 build-profile.json5.example 为模板) 配置三组 signingConfigs 与三个同名 product 一一对应:
| signingConfig / product | 材料来源 | 用途 |
|---|---|---|
appstore | hpack/sign/ 发布证书 + 发布 p7b | 应用商店/正式分发 |
appgallery | hpack/sign/ 发布证书 + 测试 p7b | 华为应用市场渠道 |
default | DevEco 自动签名(~/.ohos/config/ 下的 debug cer/p12/p7b) | 日常开发调试 |
三个 product 均声明 targetSdkVersion 6.1.1(24) / compatibleSdkVersion 5.0.5(17) / runtimeOS HarmonyOS,并开启 strictMode(caseSensitiveCheck + useNormalizedOHMUrl)。 phone/tablet 模块各含 default 与 release 两个 target(通过 buildModeBinder 绑定)。
签名材料安全
build-profile.json5 含加密后的密钥库口令,hpack/sign/ 含私钥材料,均不得外泄; build-profile.json5 在 .gitignore 中,只提交 .example 模板。
hvigorw 手动构建
# 调试构建(完整资源、Web 调试 8888 启用)
hvigorw assembleHap --mode module -p product=phone -p buildMode=debug
# 发布构建(排除 map3d 资源、禁用 Web 调试)
hvigorw assembleHap --mode module -p product=phone -p buildMode=release2
3
4
5
hpack 一键打包(推荐)
hpack/ 目录是基于开源 hpack 工具的自研打包脚本 (config.py 配置 + PackFile.py 打包前/后/失败回调),产物写入 hpack/build/{product}, 并按模板生成分发页 index.html(可选 default/simple/tech/cartoon/tradition/custom 模板), 支持上传对象存储、历史版本按钮与 webhook 通知。
# 打包(发布 → pr,调试 → pd)
hpack pr # release target:排除高德 3D 地图,包体约 7MB
hpack pd # default target(debug):含高德 3D 地图,包体约 55MB2
3
phone/tablet 模块含 default/release 双 target,必须显式指定 module@target 与 buildMode, 否则 hvigorw 会同时编译两个 target 且不生效 release 的地图排除配置。 hpack 内部实际执行的构建命令形如:
hvigorw assembleHap --mode module -p product=appstore -p module=tablet@release -p buildMode=release --no-daemon多设备模块默认打 tablet,可用环境变量 HPACK_MODULE 切换入口模块(可选 phone / tablet / foldable; pc/tv/wearable 为骨架,请直接用 hvigorw 编译):
# PowerShell
$env:HPACK_MODULE='tablet'; hpack pd # 平板 debug 包
$env:HPACK_MODULE='tablet'; hpack pr # 平板 release 包
Remove-Item Env:HPACK_MODULE # 恢复默认2
3
4
版本升级
升级版本只需改 AppScope/app.json5 的 versionCode / versionName,关于页运行时自动读取。
MVVM 架构规范(V1 状态管理)
- State:继承
BaseState,加@Observed;View 中用@State整体持有(替换而非字段赋值触发刷新) - VM:继承
BaseVM<State>,实现sendEvent(event)处理业务并回写 state - Event:实现
BaseVMEvent { type },View 只通过vm.sendEvent(...)触发业务 - 参考实现:
features/device(ServerConfig/DeviceRegister)是标准范例
V1 状态管理注意事项(踩坑记录)
- 禁止在 @Component 内定义 getter:ArkUI V1 经 stateMgmt 转译后,prototype 访问器在部分创建闭包中取不到值,运行时报
Cannot read property xxx of undefined。正确写法:@State theme: BrowserTheme = XxxUtil.getTheme()+@Watch整体替换 - @Builder 参数按值传递:Builder 内如需随状态刷新,直接读
this.xxx状态,不要依赖入参 - PersistentStorage 反序列化丢失类方法:不能用
instanceof/实例方法,用模块级纯函数(如isServerBound(c)) - 日志统一用
Logger:禁止console.log/error(Logger 封装 hilog,带 domain/tag)
状态存储分层
| 数据 | 存储 | 说明 |
|---|---|---|
| access/refresh JWT | PersistentStorage | 登录态 |
| 会话密钥 K_CLIENT / 握手 TOKEN | AssetUtil(设备安全存储) | IS_PERSISTENT=false,卸载即清 |
| 服务器列表/当前索引 | PersistentStorage | SERVER_CONFIG_LIST / CURRENT_SERVER_INDEX |
| 界面暗色/工具栏主题/网页暗色/搜索引擎 | preferences + AppStorage | 启动时 init 回读 |
核心模块说明
启动流程(EntryAbility.ets)
- 初始化各 Util(回读 preferences → AppStorage)
- 冷启动凭据清理
cleanupStaleTokensOnLaunch():检测 K_CLIENT/TOKEN 任一残留即执行performFullLogout(),保证全新会话 - 判定路由:未注册 → 设备注册;已注册未登录 → 登录页;已登录 → MainPage
onForeground:弹出"欢迎回来"提示,welcomeDialogShowing标志防多次进前台叠加弹窗
SM2 四步密钥协商
工具:common/util/KeyNegotiationUtil.ets、SM2HandshakeUtil.ets、QxjFrameUtil.ets。 流程详见 SM2 握手流程。
- HTTP 直接承载自定义二进制帧(
application/octet-stream),统一 64B 帧头 (version/main/sub/total/seq + sender 36B + enc_auth 1B + IV 16B + reserved 3B); - 设备长期 SM2 密钥对(128 hex,native 调用补
04前缀)+ 每次临时密钥对,协商密钥 32B; - K_CLIENT / TOKEN 存关键资产(AssetUtil,非持久,卸载即清);任何失败清理残留;
- 主命令码:
0x00业务帧 /0x01报警帧 /0x02加密完保帧。
加密登录(common/util/LoginUtil.ets)
- 流程:组装明文(账密/验证码/设备ID/位置)→ SM4-GCM 加密 → 封装 64B 上行帧 → POST
/api/v3/user/login/(application/octet-stream)→ 下行加密帧解密 → 获取 access/refresh JWT - 明文线格式
{username,password,verification_code,device_id,position}, 关键常量ENCRYPTED_LOGIN_PATH、LoginPayloadWire(字段名与后端 LoginSerializer 对齐); - 用 K_client(32B 时 SM4 取前 16B)做 SM4-GCM 加密,封包(enc_auth=0x40); 响应成功为 64B 下行加密帧,失败为 JSON 明文回退;
- refresh(
/api/v3/token/refresh/)、logout(/api/v3/user/logout/)为明文 JSON; 登出 = 拉黑 jti + 撤销 sid + 清本地凭据与 K_client。
会话密钥管理(KeyNegotiationUtil + DeviceKeyUtil)
- 协商:SM2 四步握手,全程二进制帧;任一步失败在 catch 中兜底清理会话密钥与 tokens
- K_CLIENT:SM4 会话密钥,握手完成存
AssetUtil - TOKEN:握手临时令牌,同存
AssetUtil - 退出/切服清理:
JsBridgeHandle.performFullLogout()调后端拉黑 refresh token → 清本地 access/refresh → 清AssetUtil中 K_CLIENT + TOKEN;ServerConfigVM.handleSelectServer()切服时对旧服务器执行同一清理
服务器配置(features/device)
- 模型(common/model/ServerConfig.ets):名称 + 4 个 URL +
serverId+serverPubKey - 页面:
ServerConfigPage(内嵌ServerConfigView)+ServerEditPage(独立新增/编辑,支持扫码填 JSON) - 扫码导入:
- 公钥绑定:
DeviceRegisterUtil.parseServerQr→ID#时间戳#公钥(# 分隔) - 配置导入:
DeviceRegisterUtil.parseServerConfigJson→ 单对象或数组 JSON(字段见模型)
- 公钥绑定:
- 持久化:
PersistentStorage.persistProp(SERVER_CONFIG_LIST、CURRENT_SERVER_INDEX)
预置服务器配置与扫码绑定态见 部署与服务器配置。
HTTP 与自签证书(common/util/HttpUtil.ets)
- 基于
@ohos.net.http:postJSON / postJSONWithAuth / postRawFrame / postRawFrameFull; CertValidationError识别证书错误(判定码2300060,消息含 "SSL peer certificate" 兜底);- 用户知情确认后
options.remoteValidation = 'skip'跳过校验——仅 API ≥ 18; API < 18 不设置该字段,弹「无法忽略,请添加 CA 或使用合法证书」; - debug 可用 HTTP,生产强制 HTTPS。
JSBridge(common/util/JsBridgeHandle.ets)
- 注入对象名
jsbridgeHandle,6 个方法(对齐设计文档):login/getAccessToken/refreshAccessToken/sm4GcmEncrypt/sm4GcmDecrypt/logout; - 全部返回结构化 JSON 字符串
{code,message,accessToken?,refreshToken?,data?},Promise 永不 reject; - 安全模型:对象对所有页面注入(内核 permission 白名单存在返回值转换缺陷), 改为每个方法入口的应用侧校验——当前页面 origin 必须属于当前选中服务器的 backend/frontend/businessBackend/businessFrontend 四个 URL,fail-closed,切服务器实时生效;
- login 方法自动补设备 ID 与定位,K_client 缺失时自动静默发起协商,证书失败弹风险框。
浏览器(features/browser)
三层配色互相独立,均持久化:
| 层 | 控制对象 | 工具类 | AppStorage 键 |
|---|---|---|---|
| 工具栏主题 | 地址栏/工具栏背景与文字 | BrowserThemeUtil(10 套色板:默认白/深邃黑/晴空蓝/薄荷绿/暖阳橙/靛青蓝/黛紫/青碧/樱粉/石墨灰) | browser_theme_id |
| 界面暗色模式 | 浏览器自身所有页面(新标签页/菜单/历史/设置/关于/服务器配置/设备注册等) | BrowserUiModeUtil(亮/暗双套 UiPalette,默认 dark) | browser_ui_dark_mode |
| 网页暗色 | WebView 内网页渲染 | WebDarkModeUtil(.darkScheme) | browser_web_dark_mode |
其余视图:BrowserView(WebView + 地址栏 + 进度条 + 新标签页)、BrowserMenuView(菜单/主题/暗色开关)、 BrowserHistoryView(历史 CRUD)、SearchEngineSettingsView(搜索引擎)、 SiteAccessSettingsView(网站访问管理)、SettingsMenuDialog(设置弹窗)。
多标签实现
- main:BrowserView 内单
Web组件 + 新标签页覆盖层; - dev:离线 Web 组件——Web 经 BuilderNode 命令式创建、可脱离组件树存活, 每标签槽位一个 NodeController,切标签「先挂新槽、后卸旧槽」,内核保留后台标签状态; 配套标签条、标签管理页、会话快照持久化、下载管理、无痕分区、用户脚本、广告拦截等。
网站访问控制(SiteAccessUtil)
- 入口:浏览器二级菜单 →「网站访问管理」→
SiteAccessSettingsPage - 拦截点:
BrowserView的Web.onLoadIntercept(返回 true 阻止加载)+ 地址栏提交前置判定,双重拦截避免闪现 - 规则:按域名后缀匹配(
example.cn命中oa.example.cn等全部子域名);开关默认关闭仅维护列表;打开后黑名单必拦,白名单非空时仅放行白名单 - 持久化:
browser_prefs(site_access_enabled/site_allow_list/site_block_list),AppStorage 键browser_site_access_enabled等 - 占位数据:首次启动自动写入示例域名(
oa.example.cn等),企业部署时替换为实际业务站点
调试页(DebugInfoPage.ets)
仅 DEBUG 构建可见(common/BuildProfile.ets 的 DEBUG),展示设备 ID、SM2 公私钥、服务器信息、token 明细等。
qxj_sdk 国密 SDK 详解
features/qxj_sdk 对外发布为 HAR 包 @nsp/qxj-sdk:SM2 四步握手 + SM4-GCM 加解密, 基于 aki 框架封装国密 .so。 它将老项目 qxj-code-app 中分散的 handshake 和 cryptography 两个 native 模块合并为单一 HAR, 底层 .so 仍为原项目提供的 libnspsm2handshake.so + libnspsmapi.so(位于 libs/), 本包仅承担 aki 胶水层和 ArkTS 封装,对外提供:
- SM2 四步握手:客户端/服务端双向身份认证 + 会话密钥协商(含 RESP/TOKEN 帧解析)
- SM4-GCM 加解密:基于协商出的会话密钥做对称加解密
- 会话密钥加解密:对齐老项目 browser 模块用法(随机 IV + 传输帧组装/解析)
与老项目差异:
| 维度 | 老项目(qxj-code-app) | 本包 |
|---|---|---|
| 模块拆分 | handshake + cryptography 两个独立 HAR | 合并为单一 @nsp/qxj-sdk |
| .so 数量 | libhandshake.so / libcryptography.so 两个 | 一个 libhandshake.so(含全部 6 个函数) |
| 参数校验 | C++ std::regex | 手写 hex/ID/时间戳校验,去 regex 链接开销 |
| SM4 调试日志 | 每个函数 20+ 条 OH_LOG_INFO | 仅保留失败路径 OH_LOG_ERROR |
| ArkTS 封装 | SM2Crypto 类硬编码密钥 | QxjHandshakeUtil 默认值仅用于 demo,正式使用必须显式注入 |
| 示例函数 | myadd / wrong_myadd | 已删除 |
安装与引用
1. ohpm 安装 aki(本包依赖 @ohos/aki C++ 模板框架,需在 HAR 模块根目录执行):
cd features/qxj_sdk
ohpm install @ohos/aki2
安装完成后 oh_modules/@ohos/aki 目录会被 CMake 通过以下路径定位: ${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@ohos/aki。
2. 引用本包——在业务模块(HAP 或其他 HAR)的 oh-package.json5 添加:
{
"dependencies": {
"@nsp/qxj-sdk": "file:../features/qxj_sdk"
// 或通过 ohpm-repo 引用正式版本号:
// "@nsp/qxj-sdk": "^1.0.0"
}
}2
3
4
5
6
7
3. 在 ArkTS 中使用:
import { QxjHandshakeUtil, QxjCryptoUtil } from '@nsp/qxj-sdk';SM2 握手接口(QxjHandshakeUtil)
QxjHandshakeUtil 类提供 4 个握手函数,对应协议的 4 个步骤。 客户端实际只用 buildInit() + buildAck(),但本包暴露全部 4 个函数以便端到端联调。
构造函数
new QxjHandshakeUtil(config?: SM2Config)SM2Config 字段(全部可选,缺省值为联调测试用):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
A_pri_hex | string | DEMO_A_PRI | 客户端静态私钥(32B hex) |
A_pub_hex | string | DEMO_A_PUB | 客户端静态公钥(65B hex,04 前缀) |
A_tmp_pri_hex | string | DEMO_A_TMP_PRI | 客户端临时私钥(32B hex) |
A_tmp_pub_hex | string | DEMO_A_TMP_PUB | 客户端临时公钥(65B hex) |
B_pri_hex | string | DEMO_B_PRI | 服务端静态私钥(32B hex) |
B_pub_hex | string | DEMO_B_PUB | 服务端静态公钥(65B hex) |
B_tmp_pri_hex | string | DEMO_B_TMP_PRI | 服务端临时私钥(32B hex) |
B_tmp_pub_hex | string | DEMO_B_TMP_PUB | 服务端临时公钥(65B hex) |
IDA | string | DEMO_IDA | 客户端 ID(UUID 36 字符) |
IDB | string | DEMO_IDB | 服务端 ID(UUID 36 字符) |
uiKeyLen | number | 32 | 协商密钥长度(字节),16=SM4-128 / 32=SM4-256 |
安全警告
默认值 DEMO_* 仅用于联调。生产环境必须通过 SM2Config 显式注入真实密钥。
buildInit(): Sm2InitResult
客户端构造第 1 帧 INIT。返回值字段:
| 字段 | 类型 | 含义 |
|---|---|---|
status | 'success' | 'fail' | 状态 |
RA | string | 客户端临时公钥(128 hex) |
SigA | string | 客户端签名(128 hex) |
ullTms | string | 时间戳(毫秒,十进制字符串) |
aucFrame1Buf | string | 完整 INIT 帧 hex(含 64B 头,总长 280B) |
buildResp(ra, sigA, tms): Sm2RespResult
服务端构造第 2 帧 RESP。客户端把 INIT 帧的 RA/SigA/ullTms 通过 HTTP 发给服务端, 服务端调用此方法生成响应。返回值字段:
| 字段 | 类型 | 含义 |
|---|---|---|
status | string | 状态 |
RB | string | 服务端临时公钥(128 hex) |
SB | string | 服务端确认值(64 hex) |
K_server | string | 服务端协商密钥 |
aucFrame2Buf | string | 完整 RESP 帧 hex |
buildAck(rb, sb): Sm2AckResult
客户端构造第 3 帧 ACK,协商出最终会话密钥 K_client。返回值字段:
| 字段 | 类型 | 含义 |
|---|---|---|
status | string | 状态 |
SA | string | 客户端确认值(64 hex) |
K_client | string | 客户端协商出的会话密钥(hex) |
aucFrame3Buf | string | 完整 ACK 帧 hex |
buildToken(ra, sa, hmacKey): Sm2TokenResult
服务端构造第 4 帧 TOKEN。hmacKey 通常使用 K_client 或服务端主密钥。返回值字段:
| 字段 | 类型 | 含义 |
|---|---|---|
status | string | 状态 |
aucFrame4Buf | string | 完整 TOKEN 帧 hex(含业务 token 数据) |
SM4-GCM 加解密(QxjCryptoUtil)
QxjCryptoUtil 静态类提供 SM4-GCM 加解密。密钥必须 32 字节(64 hex),符合 NSP 变体要求。
QxjCryptoUtil.encrypt(plainHex, keyHex, ivHex, authHex?) // → Sm4GcmEncryptResult
QxjCryptoUtil.decrypt(cipherHex, keyHex, ivHex, authHex?) // → Sm4GcmDecryptResult2
| 参数 | 类型 | 说明 |
|---|---|---|
plainHex / cipherHex | string | 明文/密文 hex |
keyHex | string | 32B 密钥 hex(必须 64 字符) |
ivHex | string | IV hex(任意非空长度,通常 12B) |
authHex | string | 附加认证数据 hex(可空) |
返回值:
| 字段 | 类型 | 加密返回 | 解密返回 |
|---|---|---|---|
status | 'success' | 'fail' | ✓ | ✓ |
cipher / plain | string | 密文 hex | 明文 hex |
mac | string | 16B 认证标签 hex | 16B 认证标签 hex |
reason | string | 失败时含原因 | 失败时含原因 |
兼容性说明:底层
nsp_sm4_gcm调用时 IV 长度硬编码为 0(保持与老项目一致),实际 IV 长度由 .so 内部决定。
会话密钥加解密(QxjSessionCrypto,推荐入口)
QxjSessionCrypto 静态类封装了老项目 browser 模块的实际用法:用协商出的 K_client 加密字符串明文,每次加密生成 16 字节随机 IV,并组装/解析传输帧。
| 方法 | 功能 | 参数 | 返回值 |
|---|---|---|---|
strToHex(str) | 明文字符串 → hex | 明文字符串 | hex 字符串 |
hexToStr(hex) | hex → 明文字符串 | hex 字符串 | 明文字符串 |
generateIvHex(byteLength?) | 生成随机 IV | 字节长度,默认 16B | IV hex(默认 32 hex 字符) |
encryptFrame(sessionKeyHex, plaintext, senderDeviceIdHex) | 加密并组装传输帧 | 会话密钥 hex / 明文字符串 / 发送方设备 ID hex | Sm4GcmFrame |
decryptFrame(sessionKeyHex, frameHex) | 解析传输帧并解密 | 会话密钥 hex / 完整帧 hex | 明文字符串 |
Sm4GcmFrame 返回字段:frame(完整帧 hex)、ivHex、cipherHex、mac。
64B v2 帧结构
传输帧结构(64B v2 帧头,enc_auth 在 IV 之前):
版本(1B=0x01) + 主命令(1B=0x02) + 子命令(2B) + 总长(2B) + 帧序号(2B)
+ 发送方设备ID(36B) + 加密认证模式(1B=0x40) + IV(16B, 随机) + 保留(3B)
+ 密文(SM4-GCM) + MAC(16B)2
3
逐字段明细:
| 偏移 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | version | 1B | 协议版本,0x01 |
| 1 | main_cmd | 1B | 主命令:0x00 业务帧 / 0x01 报警帧 / 0x02 加密完保帧 |
| 2 | sub_cmd | 2B | 子命令 |
| 4 | total_len | 2B | 总长 |
| 6 | seq | 2B | 帧序号 |
| 8 | sender_id | 36B | 发送方设备 ID(ASCII UUID) |
| 44 | enc_auth | 1B | 加密认证模式,业务帧 0x40 |
| 45 | iv | 16B | 随机 IV |
| 61 | reserved | 3B | 保留 |
| 64 | payload | 变长 | SM4-GCM 密文 + 16B MAC(业务帧) |
握手协议帧格式(INIT / RESP / ACK / TOKEN)
SM2 四步握手协议基于 GB/T 35276 国密非对称密钥协商,帧头同为 64B,各步数据域:
INIT 数据域(216B): total_len(2) + ida_len(2) + idb_len(2) + session_key_len(2)
+ timestamp(8) + ida(36) + idb(36) + ra(64) + sign(64)
→ 整帧 280B(total_len 字段 216/214 两种口径均接受)
RESP 数据域(98B): total_len(2) + rb(64) + sb(32)
→ 整帧 162B
ACK 数据域(34B): total_len(2) + sa(32)
→ 整帧 98B
TOKEN 数据域(82B): total_len(2) + ida_len(2) + hmac_len(2)
+ ida(36) + expiration_ms(8,有效时长非时间戳) + hmac(32)
→ 整帧 146B2
3
4
5
6
7
8
9
10
协议交互流程:
客户端 (A) 服务端 (B)
│ │
│ ── INIT (RA + SigA + Tms) →│
│ │ buildResp()
│ ←── RESP (RB + SB) ────────│
│ buildAck() │
│ 协商出 K_client │
│ ── ACK (SA) ──────────────→│
│ │ buildToken()
│ ←── TOKEN (HMAC) ──────────│
│ │
│ 端到端 K_client / K_server 一致2
3
4
5
6
7
8
9
10
11
12
帧解析(parseRespFrame / parseTokenFrame)
QxjHandshakeUtil 另提供两个静态解析方法,用于处理服务端 HTTP 返回的帧:
| 方法 | 功能 | 参数 | 返回值 |
|---|---|---|---|
parseRespFrame(respFrameHex) | 解析服务端 RESP 帧 | RESP 帧 hex | { header, dataLength, RB, SB } |
parseTokenFrame(tokenFrameHex) | 解析服务端 TOKEN 帧 | TOKEN 帧 hex | { header, ..., ida, hmac, token } |
QxjHandshakeUtil.parseRespFrame(respFrameHex) // → { header, dataLength, RB, SB }
QxjHandshakeUtil.parseTokenFrame(tokenFrameHex) // → { header, ..., ida, hmac, token }2
使用示例
会话密钥加解密(老项目 browser 用法)——协商完成后用 K_client 加解密业务字符串, 参考 products/phone/src/main/ets/pages/Sm2HandshakeDemoPage.ets:
import { QxjSessionCrypto } from '@nsp/qxj-sdk';
const sessionKey = '<密钥协商得到的 K_client hex>'; // 32B = 64 hex 字符
const senderDeviceIdHex = QxjSessionCrypto.strToHex('<设备 UUID>');
// 加密:内部生成随机 IV 并组帧
const frame = QxjSessionCrypto.encryptFrame(sessionKey, 'Hello, QXJ!', senderDeviceIdHex);
// frame.frame → 完整传输帧 hex(帧头 + 设备ID + IV + 密文 + MAC)
// 解密:按固定偏移切帧 + SM4-GCM 解密 + 还原字符串
const plaintext = QxjSessionCrypto.decryptFrame(sessionKey, frame.frame);
// plaintext === 'Hello, QXJ!'2
3
4
5
6
7
8
9
10
11
12
真实业务接入(客户端走 HTTP)——业务侧客户端通常只调用 buildInit() + buildAck(), 中间走 HTTP 与后端交互 RESP/TOKEN 帧:
import { QxjHandshakeUtil, QxjSessionCrypto } from '@nsp/qxj-sdk';
const util = new QxjHandshakeUtil({
A_pri_hex: '<真实客户端私钥>',
A_pub_hex: '<真实客户端公钥>',
A_tmp_pri_hex: '<真实临时私钥>',
A_tmp_pub_hex: '<真实临时公钥>',
B_pub_hex: '<服务端公钥>',
IDA: '<设备 UUID>',
IDB: '<服务器 UUID>',
uiKeyLen: 32,
});
// 步骤 1:构造 INIT 帧发给服务端
const init = util.buildInit();
// http.post(..., { frame: init.aucFrame1Buf })
// 步骤 2/3:解析服务端 RESP 帧 → 拿 RB/SB → 本地构造 ACK
const resp = QxjHandshakeUtil.parseRespFrame('<服务端返回的 RESP 帧 hex>');
const ack = util.buildAck(resp.RB, resp.SB);
// ack.K_client 即会话密钥,保存到 AssetStore
// 步骤 4:解析服务端 TOKEN 帧
// const tokenFrame = QxjHandshakeUtil.parseTokenFrame('<TOKEN 帧 hex>');
// 用会话密钥加解密业务数据
const frame = QxjSessionCrypto.encryptFrame(ack.K_client, '业务数据', senderDeviceIdHex);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SDK 目录与 .so 架构支持
features/qxj_sdk/
├── Index.ets # HAR 入口
├── oh-package.json5 # 包定义:@nsp/qxj-sdk
├── build-profile.json5 # native 构建配置
├── module.json5 # HAR 模块声明
├── hvigorfile.ts
├── README.md
├── libs/
│ ├── arm64-v8a/
│ │ ├── libnspsm2handshake.so # 底层 SM2 握手 .so
│ │ └── libnspsmapi.so # 底层 SM2/SM3/SM4 .so
│ └── x86_64/
│ ├── libnspsm2handshake.so
│ └── libnspsmapi.so
└── src/main/
├── module.json5
├── cpp/
│ ├── CMakeLists.txt # aki + libhandshake.so 链接
│ ├── napi_init.cpp # aki 胶水层(6 个 JS 函数)
│ ├── include/libs/ # 国密头文件
│ │ ├── agree.h # 握手帧/参数定义
│ │ ├── Client.h / Server.h # 客户端/服务端编排类
│ │ ├── Utility.h # hex/uuid 工具类
│ │ ├── sm2.h / sm2_agree.h
│ │ ├── sm3.h / sm4.h
│ │ └── miracl.h / mirdef.h # MIRACL 大数库定义
│ ├── src/ # C++ 实现
│ │ ├── Client.cpp / Server.cpp
│ │ ├── sm2_agree.cpp
│ │ └── Utility.cpp
│ └── types/libhandshake/
│ ├── Index.d.ts # TS 类型声明
│ └── oh-package.json5 # .so 虚拟包定义
└── ets/
├── types/Index.ets # 类型导出
└── utils/
├── HandshakeUtil.ets # QxjHandshakeUtil(握手 + 帧解析)
├── CryptoUtil.ets # QxjCryptoUtil(SM4-GCM 原始加解密)
└── SessionCrypto.ets # QxjSessionCrypto(会话密钥 + 组帧)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- .so 架构支持:
arm64-v8a(真机:HarmonyOS 手机/平板/2in1 设备)、x86_64(模拟器)。 如需新增架构,在libs/下新建对应目录,并在build-profile.json5的abiFilters中追加。 - 版本兼容:HarmonyOS 5.0.5(17) / 6.1.1(24);aki ^1.2.21;
useNormalizedOHMUrl: true。 - 修改 native 胶水:修改
napi_init.cpp后需触发 CMake 重新构建(DevEco Studio 通常自动检测), 也可手动rm -rf features/qxj_sdk/.cxx清掉 CMake 缓存。 - 调试日志:
napi_init.cpp顶部宏LOG_DOMAIN 0x3200/LOG_TAG "QXJ_SDK", 用 hilog 过滤QXJ_SDK即可;重构后只在失败路径打OH_LOG_ERROR,正常路径不打日志。
权限说明(products/phone/module.json5)
| 权限 | main | dev | 用途 |
|---|---|---|---|
ohos.permission.INTERNET | ✓ | ✓ | 全部网络通信 |
ohos.permission.STORE_PERSISTENT_DATA | ✓ | ✓ | PersistentStorage 持久化 |
ohos.permission.APPROXIMATELY_LOCATION / LOCATION | ✓ | ✓ | 登录载荷携带位置(高德定位) |
ohos.permission.PRINT | — | ✓ | 打印页面(多标签) |
ohos.permission.READ_WRITE_DOWNLOAD_DIRECTORY | — | ✓ | 公共下载目录(多标签下载) |
开发/调试指南
Web 调试
- 仅调试构建启用:
BrowserView.ets aboutToAppear()中webview.WebviewController.setWebDebuggingAccess(DEBUG) - 发布构建禁用,日志输出
Web debugging disabled (release build) - Chrome DevTools 访问
chrome://inspect/#devices或edge://inspect调试 WebView
真机测试
- PC 和 Pad 连同一局域网
- 前端绑定
0.0.0.0:npm run dev -- --host 0.0.0.0 --port 3006 - 后端绑定
0.0.0.0:python manage.py runserver 0.0.0.0:4607 - Pad 服务器配置中
frontendUrl指向 PC IP,businessFrontendUrl指向业务前端 - 自签名证书需手动确认继续(HarmonyOS http 模块不跟随跨协议重定向,HTTP→HTTPS 报 unsupported protocol)
日志
- 统一入口
Logger.info/warn/error/debug(tag, msg),tag 用文件名常量(如const TAG = '[BrowserView]') - release 构建 debug 级不输出;敏感日志(密钥/token/密码)仅在 DEBUG 构建打印
安全注意事项
- 硬编码凭据:
LoginPage.ets有默认用户名/密码、高德 AMap Key,均为测试便利,发布前必须移除或改为可配置 - 敏感信息日志:DEBUG 构建下
LoginUtil.ets会记录密码明文、sessionKeyHex、SM4 key/IV、token 完整值;release(DEBUG=false)自动跳过/脱敏 - 调试页泄露:
DebugInfoPage.ets仅 DEBUG 可见;若被编译进发布包则私钥完整暴露 - 自签名证书:
HttpUtil.ets显式配 CA(server.crt),否则 http 模块拒绝握手;保护好certs/目录
已知限制
- 模块名不能含连字符,需用下划线(如
qxj_sdk而非qxj-sdk) - HarmonyOS http 模块不跟随跨协议重定向(HTTP→HTTPS),URL 协议需与服务器一致
- 扫码能力依赖
@pura/picker_utils,需在oh-package.json5声明 - 弹窗退后台不销毁,全局弹窗需用标志位防叠加(参见
welcomeDialogShowing)
注意事项
- 旧项目
qxj-code-app只能引用,不能修改 - SDK 所需 SO 文件位于
qxj-code-app/features/handshake/libs - 发布构建需排除 map3d 相关资源(so/rawfile/abc)以减小体积