开源鸿蒙平台 KMP 三方库「KSafe」适配全流程
本文记录
KSafe接入 OpenHarmony 的完整过程,覆盖加密持久化库盘点、鸿蒙 kotlinx 制品线选型、真实引擎编译、ohosArm64/ohosX64目标、纯 Kotlin AES-GCM 引擎、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
KSafe是基于kotlinx.coroutines+kotlinx.serialization的 Kotlin Multiplatform 加密键值持久化库:给一个 store 名和一个可序列化模型,它提供put/get/delete/clearAll/rotateKeys/getKeyInfo/protectionInfo,默认所有写入经 AES-GCM 加密(128/256 位密钥、96 位 IV、128 位 Tag,线上格式冻结为IV‖密文‖Tag,AAD 按 v3 信封绑定 store 身份),密钥 custody 分层上报(软件 / 沙箱保护 / 硬件隔离)。本次适配与社区早期的"零依赖内核提取"路线(qrose / haze / PreCompose / voyager)以及 KStore 那批"制品线齐全、补 8 行 dispatcher actual 即可"的存储类适配都有本质区别:KSafe 的难点在加密面——上游四个平台各有一套密钥库 / 加密 API(Android Keystore、Apple Keychain+CryptoKit、JVM OS 密钥库、Web IndexedDB+WebCrypto),鸿蒙侧既没有 HUKS 桥也没有任何平台 crypto 库可从 Kotlin/Native 调用。因此本 fork 补齐的是:纯 Kotlin AES-GCM 引擎(cryptoMain共享源集,jvm + ohos 双编,NIST 已知答案向量在jvmTest交叉验证)+ 沙箱文件密钥库 + 文件存储后端 + ohos 工厂,而上游commonMain引擎(46 个文件、信封/缓存/写合并/密钥轮换/孤儿清扫)原样编译成 OpenHarmony KLIB,一个真实的加密持久化引擎运行在真机上,ArkUI 页面作为验收宿主。项目地址: AtomGit/oh-tpc/KSafe.git
上游仓库: ioannisa/ksafe
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
KSafe 为 Kotlin Multiplatform 应用提供"对象 ↔ 加密落盘"这一件事的完整答案:KSafe() 一行开店(平台工厂),put / get / delete / clearAll 全生命周期覆盖,rotateKeys 按代数轮换密钥,getFlow / getStateFlow 让 UI 订阅状态,getOrCreateSecret 派发硬件隔离的对称密钥,protectionInfo 如实上报当前密钥 custody。KMP 项目把这些能力放在 commonMain,由 Android(Keystore + StrongBox)、Apple(Keychain + Secure Enclave,经 Swift CryptoKit 桥 cinterop)、JVM(OS 密钥库 DPAPI / 登录 Keychain / Secret Service,不可达时软件回退)、Web(IndexedDB 里 non-extractable 的 WebCrypto key)共享。OpenHarmony 应用同样需要这套加密持久化能力,但上游构建(Kotlin 2.3.21 / Gradle 9.6.1 / AGP 9.2.1,发 Maven Central)不会生成 OpenHarmony 变体。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共加密引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行——对一个安全库来说尤其如此:"能存能读"只证明没坏,不证明密文和上游其它平台逐字节同构。适配需要同时验证加密引擎、密钥库、存储后端、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链不一致 | 上游 Kotlin 2.3.21 / Gradle 9.6.1 / AGP 9.2.1(com.android.kotlin.multiplatform.library + Apple Swift 桥 Exec 任务)与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| 依赖矩阵 | 引擎依赖 kotlinx-coroutines、kotlinx-serialization-json,都需要 ohos 变体才能解析;测试依赖 turbine 没有制品。 |
| 加密面无人接 | 四平台的 KSafeEncryption 引擎全走平台 API;ohos 上既无 HUKS 绑定也无平台 crypto 库可调,javax.crypto / CryptoKit / WebCrypto 一个都搬不动。 |
| Compose 注解 | commonMain 的 @Stable 注解引用 androidx.compose.runtime(compileOnly),ohos KLIB 编译要它能解析。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:真实引擎与密钥/存储后端的 ohos 编译、根构建替换(cmp_qrose 模式)、纯 Kotlin AES-GCM 填补加密面、以及 C ABI/N-API 桥接层。加密与存储语义完全由上游原版 commonMain 实现,ArkTS 只负责调用 Native、解析 JSON 和展示结果(包括把落盘密文亮给用户看)。
1.2 库提供的能力
Demo 提供一个真实的加密持久化会话,对应桥的全部入口:
- 写入(put):
KSafe.put(key, value)把字符串序列化后经 AES-GCM 加密落盘——每次写入都铸新 96 位 IV,密文按IV‖密文‖Tag进沙箱 JSON 文件,AAD 绑定 store 身份;可选KSafeWriteMode.Plain明文写入对照; - 读取(get):
KSafe.get(key)走内存缓存(首次才碰盘 + 解密),键不存在返回null; - 删除 / 清库(delete / clearAll):删值连带删引擎密钥;清库把数据和密钥一起 wipe;
- 持久化闭环:杀进程重启后再读,值仍在——证明是落盘密文而不是内存缓存;
- 密文检查(rawfile):直接把 store 文件前 240 字节读成 base64 展示——落盘即密文的证据层;
- 密钥轮换(rotate):
rotateKeys()把每条密文在新的密钥代数下重加密,返回 rotated / skipped / failed 计数; - 保护信息(info / keyInfo):
protectionInfo与getKeyInfo的层级、custody、notes 原样上屏; - 版本握手(version):
ksafeVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应统一:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / key | 回显本次操作与目标键。 |
value | get 返回的原值(或 null);写操作为 null。 |
mode | put 的写入模式(encrypted / plain)。 |
引擎侧完整能力(写合并、缓存、v3 信封、密钥代数轮换、损坏清扫、生物识别门控的共享逻辑)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 808 项上游测试 + 9 项 NIST KAT 交叉验证用例,证明 ohos .so 里执行的逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | ksafe 与 ksafe-biometrics 的全部 commonMain 文件原样编译,不提取、不重写;新增 ohos 源码(actual ×6 + AES-GCM 引擎 + 密钥库/存储/工厂)与一个 KAT 测试文件。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);ksafe-compose(Compose 状态桥,鸿蒙 UI 是 ArkUI)与 android / apple / js / wasm 目标留盘不进构建。 |
| 加密纪律 | 线上契约逐字节冻结:AES-GCM、96 位 IV、128 位 Tag、IV‖密文‖Tag;同一份引擎源码 jvm+ohos 双编,NIST KAT 在 JVM 跑的是将进 .so 的那份代码。 |
| 桥接稳定 | 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| UI 完整 | 单页三 Tab 覆盖全部桥接入口:数据存取、密文检查、关于(版本握手 + 保护信息)。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
KSafe核心(:ksafe)不碰 UI,ksafe-compose(ComposeMutableState持久化桥)留盘不进构建;ArkTS Demo 画的是加密持久化闭环 + 落盘密文证据。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与加密面边界、确认 kotlinx ohos 制品线、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec 仓库内容过滤、Kotlin 2.4.20-0.1.0-07 钉版、@Stable 双线 compileOnly
第 3 阶段:加密面建模 ── expect/actual 清单、cryptoMain 双编源集、密钥库/存储/工厂契约、String 载荷与错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── baseDir 注入、三 Tab、密文检查、版本握手
第 6 阶段:示例与验证 ── HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
ksafe/ 上游加密引擎(进 ohos 构建,jvm() + ohosArm64 + ohosX64)
ksafe/src/ohosMain/ 新增:actual ×5 + OhosFileKeyStore / OhosFileStorage / 工厂 / 诊断
ksafe/src/cryptoMain/ 新增:纯 Kotlin AES-GCM(jvm + ohos 双编共享源集)
ksafe/src/jvmTest/.../OhosAesGcmKatTest.kt
新增:NIST 已知答案向量(同一份引擎源码在 JVM 上验证)
ksafe-biometrics/ 上游生物识别门控(进 ohos 构建,no-op actual)
ksafe-compose/ Compose 状态桥,留盘不进 settings
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/ 上游文档站 + 本篇适配全流程
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :ksafe、:ksafe-biometrics 与 :example:nativeApp,rootProject.name = "ohos_ksafe"。上游的 AGP classpath、Apple Swift CryptoKit 桥(xcrun swiftc Exec 任务 + cinterop)、vanniktech 发布插件、binary-compatibility-validator、js/wasm 目标、GitHub Packages 发布段全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(Eclipse Temurin,org.gradle.java.home 指定) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 9.6.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x) |
| kotlinx | coroutines / coroutines-test 1.11.0-0.1.0-07、serialization-json 1.11.0-0.1.0-07(ohos 制品线) |
| Compose runtime | compileOnly 双线:jvm 1.10.3(上游坐标)/ ohos 1.9.2-0.2.0-04(社区线) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.ksafe |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 文件 | 依赖形态 | 处置 |
|---|---|---|---|
ksafe | 46 | 纯 Kotlin + coroutines + serialization,@Stable 注解 compileOnly compose-runtime;expect 共 11 处 | 整体编译(jvm + 双 ohos),新增 ohosMain + cryptoMain |
ksafe-biometrics | 5 | 纯 Kotlin + coroutines,expect 5 处 | 整体编译(no-op actual,与 JVM-on-Linux 同款退化) |
ksafe-compose | 3 | org.jetbrains.compose.runtime 1.10.3(MutableState / remember / LaunchedEffect) | 留盘不进构建(鸿蒙 UI 是 ArkUI,未来 Compose 工具链对齐后再补) |
ksafe 的 expect 声明共 11 个,分布在 4 个文件:KSafeConcurrent.kt(7 个:原子 Flag/Int、并发 Map/Set、runBlockingOnPlatform、decryptFlowContext、可重入 KSafeInitLock)、KSafeLog.kt(2 个)、SecurityChecker.kt(1 个 object)、KSafeSecureRandom.kt(1 个)。另有一个关键发现:公共工厂函数 fun KSafe(...) 不是 expect/actual,而是每个平台一份独立文件(JVM/Android/Apple/Web 各写一份),ohos 必须新写一份工厂文件,不能靠 actual 补齐——这份工厂要向 KSafeCore 供给存储后端、加密引擎、store 身份、密钥别名、保护层级上报等全部接线。
真正的难点不在这些 actual,而在加密面:上游的 KSafeEncryption 接口只有契约(IV/Tag 长度、线上格式、noKeyFound 错误拼写),实现全在各平台源集——JVM javax.crypto、Apple CryptoKit(Swift 桥静态库)、Android Keystore、Web WebCrypto。全库没有一份纯 Kotlin 的 AES-GCM,ohos 侧必须自建引擎。
1.4 页面边界
标题区 KSAFE · OPENHARMONY / 版本握手行(KMP 加密持久化库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换 数据存取 / 密文检查 / 关于
路径区 baseDir 只读展示(aboutToAppear 注入 getContext().filesDir)
fileName 区 store 名输入框(默认 demo,空串 = 默认 store)
键值区 key 输入框 + value 多行文本域(初始 {"name":"alice","token":"secret-42"})
模式区 加密写入 / 明文写入 两个 chip(默认加密)
操作区 写入(深绿) / 读取(蓝) / 删除(红) 三按钮 + 清空全库 / 轮换密钥 次要按钮
状态行 最近一次操作结果(失败红字显示 error 原文)
日志区 带时间戳的操作日志(最新在前,上限 100 条)
密文检查 Tab store 文件路径 / 字节数 / 前 240 字节 base64(等宽小字)——落盘即密文的证据
关于区 版本握手、protectionInfo 六行、keyInfo、生物识别可用性、架构分层、仓库链接
页面启动时先调 ksafeVersion() 做握手,再注入 getContext(this).filesDir 作为所有请求的 baseDir。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的 kotlinx 制品。用 .module 元数据逐个验证:
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / jvmApiElements-published
org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0-0.1.0-07
variants: 同上(jvm + ohos 双线齐全)
org.jetbrains.compose.runtime:runtime:1.9.2-0.2.0-04
variants: ohosArm64 / ohosX64 / uikit*——**没有 jvm 变体**
三个关键结论:其一,版本主体与上游声明逐字相同(coroutines / serialization 都是 1.11.0),只是仓库换线——commonMain 里一行依赖坐标都不用改;其二,ohos 制品同时发布 jvm 变体,jvm() 交叉验证可以用同一个版本号;其三,社区 compose-runtime 线(1.9.2-0.2.0-04)只发布 ohos/uikit 变体、没有 jvm,而上游 @Stable 用的 1.10.3 没有 ohos——所以 compileOnly(compose-runtime) 不能在 commonMain 一刀切,必须按目标分开(见 2.3)。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)
社区此前批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),旧钉法读不了新元数据。工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07。额外好处:上游源码本来就是 Kotlin 2.3.21 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 9.6.1 上游 wrapper 原样沿用(KStore/compottie 先例,2.4.20 线支持 Gradle 9.x),不像 2.2.21 旧线必须降 8.14.1。
settings.gradle.kts 把 eazytec 仓库写在最前,并且这一版多了一层内容过滤——这是 KStore 之后新踩的坑:eazytec 代理对 androidx.* 这类外国组只缓存了 POM 元数据、不带 jar,Gradle 一旦在某仓库解析到元数据就把该模块绑定到它,后续 jar 404 直接失败。解法是 eazytec 双仓只回答 org.jetbrains.*(Kotlin / kotlinx ohos 线 / 社区 compose-runtime 全在这组),androidx / JNA / turbine 走 google() / mavenCentral()——带 -0.1.0-07 后缀的坐标只存在于 eazytec,两条线不可能串:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/") {
content { includeGroupByRegex("org[.]jetbrains.*") }
}
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos") {
content { includeGroupByRegex("org[.]jetbrains.*") }
}
google()
mavenCentral()
}
}
rootProject.name = "ohos_ksafe"
include(":ksafe")
include(":ksafe-biometrics")
include(":example:nativeApp")
pluginManagement 保持 eazytec 双仓置前不加过滤——HarmonyOS Kotlin 插件(含 ohosArm64() / ohosX64() DSL)只能从社区线解析,顺序一错配置期直接 Unresolved reference: ohosArm64。
2.3 编译真实引擎
ksafe/build.gradle.kts 重写为纯 KMP 模块,三处要点:
kotlin {
applyDefaultHierarchyTemplate()
compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") }
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } // 交叉验证用
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {
api(libs.kotlinx.serialization.json) // 1.11.0-0.1.0-07
api(libs.kotlinx.coroutines.core) // 1.11.0-0.1.0-07
}
val common = commonMain.get()
val datastoreMain = create("datastoreMain") { dependsOn(common) } // jvm 的 DataStore 后端
val jvmSharedMain = create("jvmSharedMain") { dependsOn(common) }
val cryptoMain = create("cryptoMain") { dependsOn(common) } // AES-GCM 双编
jvmMain {
dependsOn(datastoreMain); dependsOn(jvmSharedMain); dependsOn(cryptoMain)
dependencies {
implementation(libs.androidx.datastore.preferences)
implementation(libs.jna); implementation(libs.jna.platform)
compileOnly(libs.compose.runtime.jvm) // @Stable,上游坐标 1.10.3
}
}
ohosMain {
dependsOn(cryptoMain)
dependencies { compileOnly(libs.compose.runtime.ohos) } // 社区线 1.9.2-0.2.0-04
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
implementation(libs.turbine) // 仅 JVM 测试解析,见 2.4
}
}
}
其一,上游的中间源集按目标保留:datastoreMain(DataStore 后端 + 日志 actual)和 jvmSharedMain(java.* 并发/CSPRNG actual)只挂给 jvm(),ohos 走自己的 OhosFileStorage;serialization 插件从上游根构建的 allprojects 改为模块级显式声明(KStore 同款教训:丢了它编译期不报错,serializer() 运行期才炸)。其二,@Stable 按目标双线 compileOnly:jvm 用上游 1.10.3(Maven Central),ohos 用社区线 1.9.2-0.2.0-04(eazytec),源码零改动、注解两目标都可解析。其三,generateKSafeBuildConfig 任务原样保留,ksafe.version 单一来源继续驱动 KSafe.VERSION。实测:
.\gradlew.bat :ksafe:compileKotlinOhosArm64 :ksafe:compileKotlinOhosX64
.\gradlew.bat :ksafe-biometrics:compileKotlinOhosArm64 :ksafe-biometrics:compileKotlinOhosX64
# 4 个任务全部 BUILD SUCCESSFUL
顺带一提:Gradle 9.6 已移除 Kotlin DSL 的 by creating / by getting / by registering 委托(上游脚本里大量使用、在 8.x 只是弃用),重写时全部换成 create() / getByName() 现代写法,否则脚本编译直接报 15 个错。
2.4 测试依赖的处置:turbine 没有 ohos 制品
commonTest 的依赖表对 ohos 测试编译同样可见,turbine 解析不到 ohos 变体就会卡死 compileTestKotlinOhosArm64。处置与 KStore 相同:测试统一在 JVM 跑(jvmTest),根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件(含 turbine 用例)零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:加密面建模(本适配的技术核心)
3.1 expect/actual 与引擎契约
eu.anifantakis.lib.ksafe/**(上游引擎 + 各平台源集,原样编译)
↓ KSafeCore / v3 信封 / 写合并 / 缓存 / 轮换
ksafe/src/ohosMain(新增 actual ×5)
↓ 并发(POSIX 递归锁+复制写容器) / CSPRNG(/dev/urandom) / 日志(println) / SecurityChecker(全 false)
ksafe/src/cryptoMain(新增纯 Kotlin AES-GCM,jvm+ohos 双编)
↓ encrypt/decrypt/deleteKey/prewarmKey
OhosSoftwareEncryption + OhosFileKeyStore + OhosFileStorage + KSafe.ohos.kt 工厂
↓ runBlocking
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API ksafeDispatch() / ksafeVersion()
↓
ArkUI Index.ets(单页三 Tab)
五个 actual 的取舍:KSafeConcurrent.ohos.kt 抄 Apple 版(kotlin.concurrent.atomics + 复制写 Map/Set,纯 stdlib),唯一平台依赖 NSRecursiveLock 换成 POSIX pthread_mutex + 属主记账实现可重入(musl 的 pthread_t 本质是线程指针,属主槽存 pthread_self() 指针、按指针相等判重入,释放前清零——外部线程只会正确地阻塞,绝不会错误地重入);KSafeSecureRandom.ohos.kt 读 /dev/urandom(与平台 getrandom 同源);KSafeLog.ohos.kt 用 println;SecurityChecker.ohos.kt 照 Web 姿势全 false(本移植未桥接 HUKS / hilog 真信号,如实上报而不是瞎猜)。
3.2 纯 Kotlin AES-GCM:为什么自写、怎么证明没写错
上游四平台引擎各走各的平台 API,commonMain 只有契约。ohos 上没有可调用的平台 crypto,所以引擎自己写,四件事钉死:
- 契约冻结:AES-GCM、96 位 IV、128 位 Tag、输出
IV‖密文‖Tag、AAD 原样透传(v3 信封的 store 身份绑定骑在 AAD 上)、密钥缺失抛KSafeEngineMessage.noKeyFound精确拼写(孤儿清扫靠它区分"钥匙没了"和"密文坏了")。ohos 写出的密文必须和其它平台的信封逐字节同构——将来 HUKS 桥替换引擎时,旧密文还得能解。 - S-box 用 GF(2⁸) 算术构造,不手抄表:求逆 + 仿射变换在初始化时算出来。手抄 256 字节的表错一个字节,加密"能用"但和任何标准实现对不上,肉眼查不出来。
- GHASH + CTR 按 NIST SP 800-38D 实现,密钥长度 128/256 跟随
KSafeConfig.aesKeySize,不引入新档位。 cryptoMain是 jvm + ohos 双编共享源集:同一份OhosAesGcm.kt,jvm 编进jvmTest、ohos 编进 KLIB。jvmTest里跑 NIST 已知答案向量(TC1/2/4/16/17 + JDKjavax.crypto生成的参考向量):给定 key/IV/明文/AAD,密文+Tag 必须逐字节一致。这是"引擎没漂"的证明——ohos 二进制里跑的加密代码和 JVM 上过了 KAT 的是同一份源码,不是"照着又写了一遍"。
这个设计立刻兑现了价值:KAT 首跑抓出两个真 bug——其一是 S-box 构造里 exp[255 - log[i]] 在 i == 1(log 为 0)时越界,正确写法是 exp[(255 - log[i]) % 255];其二是 GHASH 的 GF(2¹²⁸) 乘法里 V 右移用了 Byte.toInt() ushr 1,负数字节符号扩展后污染低字节(0x81 >> 1 算成 0xC0),密文全对、只有 Tag 错——不靠 NIST 向量这种错几乎不可能发现。修复后 9 项 KAT 全绿,ohos KLIB 与 .so 用修复后的源码重新编译链接。
3.3 密钥库、存储后端与工厂契约
OhosFileKeyStore(钥匙放哪):每个 store 一个 JSON 密钥文件(<base>/<store>.keys.json),Base64 密钥按别名索引,事务写(.tmp+ rename),chmod 0600尽力而为。姿势对齐 JVM 的DataStoreKeyVault软件回退——上游已经定义过"密钥落文件"这种运行模式,ohos 只是把退化路径当主路径并如实上报。OhosFileStorage(密文放哪):KSafePlatformStorage的沙箱 JSON 文件实现,事务写 +MutableStateFlow镜像快照喂给 commonMain 热缓存;字符串单拼写后端,与 Web localStorage 后端同款约束。KSafe.ohos.kt(工厂接线):
fun KSafe(
fileName: String? = null,
lazyLoad: Boolean = false,
memoryPolicy: KSafeMemoryPolicy = KSafeMemoryPolicy.LAZY_PLAIN_TEXT,
config: KSafeConfig = KSafeConfig(),
securityPolicy: KSafeSecurityPolicy = KSafeSecurityPolicy.Default,
plaintextCacheTtl: Duration = 5.seconds,
baseDir: String? = null,
): KSafe
baseDir 显式契约:参数或环境变量 KSAFE_OHOS_BASE_DIR,都没有直接抛 IllegalArgumentException——不猜默认路径,因为 store 身份绑在 store 文件绝对路径上(v3 AAD):把密文挪个目录或从别的 store 抄过来,解密按设计失败(防密文移植)。commitMutex 按 store 文件注册(复制写容器 + putIfAbsent 保证竞态下单例),多实例共享同一 store 时提交串行化。protectionInfo 如实上报 SOFTWARE(intended = effective = SOFTWARE,custody 写明"纯 Kotlin 引擎 + 沙箱密钥文件",notes 码 ohos_software_keys_no_huks)——撒谎才是不兼容。另加公共诊断对象 KSafeOhosDiagnostics(storeFilePath / keyFilePath 路径推导),Demo 的 rawfile op 用它读密文文件,不在这套推导之外手拼路径。
ksafe-biometrics 同期进构建:ohos actual 走 no-op 语义(biometricsAvailable = false,verifyBiometric 直通返回 true 且从不种授权缓存)——这正是 common KDoc 写明的退化契约(JVM-on-Linux、无平台认证器的浏览器同款),未来接 user_auth 时用 commonMain 现成的 beginBiometricAttempt + promptGate 接线即可。
3.4 桥协议与错误通道
KSafeDispatch(json) 一个入口分发 10 个 op,与 KSafe API 一一对应:
| op | 对应 API | 验收点 |
|---|---|---|
version | 桥自身 | 版本握手 |
put / get / delete / clearAll | 同名 API | 写读回环、重启仍在、清库连密钥一起删 |
info | protectionInfo | SOFTWARE + notes 码如实上屏 |
keyInfo | getKeyInfo | 层级 / storage / 代数 / protection |
rotate | rotateKeys | 轮换后旧密文仍可解 |
rawfile | 诊断对象 | store 文件前 240 字节 base64——落盘即密文 |
biometrics | KSafeBiometrics | available=false、直通语义 |
请求 : {"op":"put","baseDir":"/data/storage/el2/base/haps/entry/files","fileName":"demo",
"key":"username","value":"{\"name\":\"alice\"}","plain":false}
响应 : {"ok":true,"op":"put","key":"username","mode":"encrypted"}
{"ok":false,"error":"KSafe: No encryption key found for identifier: demo:username"}
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI,ok:false 统一通道。suspend 调用全部 runBlocking 收进同步 C ABI,协程上下文用 Dispatchers.Default——鸿蒙线 Dispatchers.IO 是 internal(FFRT 调度下无独立 IO 池),且 Main 跨线程派发有已知问题,桥全程不碰 Main。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libksafe_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosksafe.so)
│ KSafeDispatch / KSafeVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制加密语义,所以选择 C ABI + JSON(cmp_qrose 审核模式)。
4.2 Kotlin/Native 导出函数
@CName("KSafeVersion")
fun ksafeVersion(): String = VERSION // 版本握手 "3.3.0-ohos.1"
@CName("KSafeDispatch")
fun ksafeDispatch(request: String): String = try {
/* 10 个 op 分发,runBlocking 收 suspend */
} catch (t: Throwable) {
"""{"ok":false,"error":${q(t.message ?: t.toString())}}"""
}
实例按 baseDir|fileName|appNamespace 缓存(复制写 Map),每次请求复用同一个 KSafe 实例——写合并与热缓存是 per-instance 的,重复开店等于每次都冷启动。JSON 用 MiniJson 扁平解析(顶层平铺字段、引号状态机),不把 kotlinx.serialization 拉进桥。
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"ksafeDispatch", nullptr, NapiKSafeDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"ksafeVersion", nullptr, NapiKSafeVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosksafe_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohosksafe_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),并与 KN 生成的头文件逐符号核对过(KSafeDispatch / KSafeVersion / DisposeString 拼写一致)。
4.4 CMake 和类型声明
add_library(ohosksafe SHARED IMPORTED)
set_target_properties(ohosksafe PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(ksafe_napi SHARED napi_init.cpp)
target_link_libraries(ksafe_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosksafe)
OHOS_ARCH 为空或 libohosksafe.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const ksafeDispatch: (request: string) => string;
export const ksafeVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 注入路径和触发操作
aboutToAppear(): void {
this.baseDir = getContext(this).filesDir; // 沙箱目录,所有请求的 baseDir
this.version = ksafeVersion(); // 版本握手:来自真实 Native 调用
this.dispatchOp('version');
this.refreshAbout();
}
private dispatchOp(op: string): void {
const req: DispatchRequest = {
op, baseDir: this.baseDir, fileName: this.fileName,
key: this.storeKey, value: this.value, plain: this.plain,
};
const res = JSON.parse(ksafeDispatch(JSON.stringify(req))) as DispatchResp;
this.apply(res); // @State 刷新状态行 + 时间戳日志
}
写入前不做本地校验——值就是字符串,非法 fileName / 保留键由引擎拒绝并经 ok:false 回显原文。
5.2 页面交互
数据存取 Tab 自上而下:baseDir 只读行、fileName 输入框、key 输入框、value 文本域、加密/明文双 chip、三色操作按钮(写入深绿 / 读取蓝 / 删除红)、清空全库与轮换密钥次要按钮、状态行与操作日志。每条日志带 [HH:mm:ss] 时间戳,→ 行显示请求、← 行显示响应摘要,最新在前、上限 100 条;每次写入/删除/轮换后自动触发一次 get 回读闭环。密文检查 Tab 展示 store 文件路径、字节数与前 240 字节 base64(等宽小字)——默认 fileName 与 key 的明文不应出现在其中。关于 Tab 展示版本握手行、protectionInfo 六行(intendedLevel / effectiveLevel / custody / operational / notes / deviceKeyStorages)、当前 key 的 keyInfo、生物识别可用性、ArkTS → NAPI → C ABI → KSafe 引擎 → 纯 Kotlin AES-GCM → 沙箱 JSON 文件 分层说明与仓库 / 社区链接。
5.3 对比度、错误边界和一个 ArkTS 踩坑
主操作深绿白字,读取蓝、删除红,语义与破坏性对齐;ok:false 时错误原文进状态行(红字)与日志。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志区。实测踩到一个 ArkTS 编译坑:组件状态最初叫 @State key,与 ArkUI CustomComponent 的内置通用属性 key(value: string) 撞名,CompileArkTS 直接报 “Property ‘key’ … is not assignable to the same property in base type ‘CustomComponent’”——改名 storeKey 即过(请求接口字段名不受影响,桥协议不动)。getContext 的弃用警告不影响功能,后续可换 getUIContext().getHostContext()。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosksafe)+ copyKnSo*
│ └── src/ohosMain/kotlin/eu/anifantakis/ksafe/bridge/
│ └── Bridge.kt @CName × 2 + MiniJson(继承自 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.ksafe、versionName 3.3.0-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosksafe_api.h
├── src/main/cpp/types/libksafe_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 三 Tab 验收页面
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :ksafe:compileKotlinOhosArm64 :ksafe:compileKotlinOhosX64 :ksafe-biometrics:compileKotlinOhosArm64
.\gradlew.bat :ksafe:jvmTest :ksafe-biometrics:jvmTest
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
linkDebugShared* 完成后 copyKnSo* 自动把 libohosksafe.so + 对应 triple 的 libc++_shared.so 回填进 entry/libs/{arm64-v8a,x86_64}。HAP 用 DevEco Studio 打开 example/harmonyApp 构建(注意不要打开仓库根目录),签名在 File → Project Structure → Signing Configs 勾选自动生成,证书写入本机不进仓库(build-profile.json5 的 signingConfigs 保持空数组)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.ksafe -m entry
四、完整代码对照
4.1 整体架构
eu.anifantakis.lib.ksafe/**(上游引擎原样编译)
│ KSafeCore(1323 行)/ v3 信封 / 写合并 / 热缓存 / 轮换 / 孤儿清扫
ksafe/src/ohosMain(本 fork 新增)
│ actual ×5(并发 / CSPRNG / 日志 / 安全检查)
│ OhosFileKeyStore(沙箱密钥文件) / OhosFileStorage(事务写存储)
│ OhosSoftwareEncryption(引擎接线) / KSafe.ohos.kt 工厂 + KSafeOhosDiagnostics
ksafe/src/cryptoMain(本 fork 新增,jvm + ohos 双编)
│ OhosAesGcm:GF(2⁸) 算术构造 S-box、GHASH + CTR、12 字节 IV、128 位 Tag
example/nativeApp Bridge.kt + MiniJson
│ KSafeDispatch / KSafeVersion
libksafe_napi.so
│ ksafeDispatch / ksafeVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(单页三 Tab)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec 双仓置前(依赖仓带 org.jetbrains.* 内容过滤)、ohos_ksafe、ksafe-compose 移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + ohos kotlinx 制品线钉版 + compose-runtime 双线坐标 |
build.gradle.kts(根) | 版本 3.3.0-ohos.1、全局禁用 Ohos*Test* 任务 |
ksafe/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64;datastoreMain/jvmSharedMain 留给 jvm、cryptoMain 双编、serialization 插件模块级补回 |
ksafe-biometrics/build.gradle.kts | 纯 KMP:jvm + 双 ohos(JNA 只挂 jvmMain) |
ksafe/src/cryptoMain/.../OhosAesGcm.kt | 纯 Kotlin AES-GCM(S-box 算术构造、GHASH、CTR、seal/open) |
ksafe/src/jvmTest/.../OhosAesGcmKatTest.kt | NIST TC1/2/4/16/17 + JDK 参考向量 + 篡改/错 AAD 用例 |
ksafe/src/ohosMain/.../KSafeConcurrent.ohos.kt | POSIX 递归锁 + kotlin.concurrent.atomics 复制写容器 |
ksafe/src/ohosMain/.../KSafeSecureRandom.ohos.kt | /dev/urandom CSPRNG |
ksafe/src/ohosMain/.../{KSafeLog,SecurityChecker}.ohos.kt | println 日志;安全检查全 false(Web 姿势) |
ksafe/src/ohosMain/.../OhosFiles.kt | POSIX 文件原语:事务写 / mkdirs / chmod / 读文件 |
ksafe/src/ohosMain/.../OhosFileKeyStore.kt | JSON 密钥文件(Base64 密钥、事务写、缓存失效) |
ksafe/src/ohosMain/.../OhosFileStorage.kt | KSafePlatformStorage 沙箱 JSON 实现 + StateFlow 镜像 |
ksafe/src/ohosMain/.../OhosSoftwareEncryption.kt | KSafeEncryption 引擎:铸 IV、seal/open、noKeyFound 精确拼写、prewarm 只读不铸 |
ksafe/src/ohosMain/.../KSafe.ohos.kt | 工厂(baseDir 契约、AAD 身份、commit mutex、SOFTWARE 如实上报)+ 诊断对象 |
ksafe-biometrics/src/ohosMain/.../KSafeBiometrics.ohos.kt | no-op actual(available=false、直通 true、不种缓存) |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、runBlocking 包裹、MiniJson、10 op 分发、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 三 Tab 验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(24 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | KSafe(fileName, config, baseDir) | ohos 工厂开店(baseDir = 沙箱目录) |
| 引擎 | put / get / delete / clearAll | 写 / 读 / 删 / 清库(连带引擎密钥),suspend |
| 引擎 | rotateKeys() | 按新密钥代数重加密全库,返回 rotated/skipped/failed |
| 引擎 | getKeyInfo / protectionInfo | 单键与实例的密钥 custody 上报 |
| 引擎 | KSafeWriteMode.Plain / Encrypted | 明文 / 加密写入模式 |
| 加密 | OhosAesGcm.seal / open | 纯 Kotlin AES-GCM(12 字节 IV、128 位 Tag、IV‖ct‖tag) |
| 后端 | OhosFileKeyStore.ensureKey / findKey | 铸钥匙(写文件)/ 只读查钥匙(缺失即失败,绝不静默重铸) |
| 后端 | OhosFileStorage.applyBatch / snapshotFlow | 事务写 + 快照流(喂热缓存) |
| Native | KSafeDispatch / KSafeVersion | C ABI JSON 导出 |
| N-API | ksafeDispatch(req) / ksafeVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatchOp(op) | 触发操作并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const res = JSON.parse(ksafeDispatch(JSON.stringify({
op: 'put', baseDir: context.filesDir, fileName: 'demo',
key: 'username', value: '{"name":"alice","token":"secret-42"}', plain: false,
})));
// res.ok === true && res.mode === 'encrypted'
// 杀进程重启后 op:'get' 返回 value 原文 —— 落盘的是密文,读回的是明文
const raw = JSON.parse(ksafeDispatch(JSON.stringify({
op: 'rawfile', baseDir: context.filesDir, fileName: 'demo',
})));
// raw.headBase64 是 store 文件前 240 字节 base64 —— 肉眼只见密文
val store = KSafe(fileName = "demo", config = KSafeConfig(), baseDir = baseDir)
runBlocking { store.put("username", json, String.serializer(), store.defaultWriteMode) }
val value = runBlocking { store.get("username", null, String.serializer().nullable) }
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译整库 + 自建加密面,而不是提取内核或在 ArkTS 重写加密
上游 commonMain 引擎(信封、写合并、缓存、轮换、清扫)与四个平台引擎的契约边界非常干净:只要给一个 KSafeEncryption 实现 + 一个 KSafePlatformStorage 实现 + 一份工厂接线,整库就能在 ohos 上跑。提取内核等于把加密持久化引擎手抄一遍,引入漂移风险;在 ArkTS 侧用系统 API 重写加密则彻底丢掉了 KMP 复用。正确的切口是"引擎原样编译、加密面自建"。
决策 2:纯 Kotlin AES-GCM 放 cryptoMain 双编,而不是 cinterop OpenSSL / HUKS
ohos 的 Kotlin/Native 目标没有现成的平台 crypto 绑定,走 cinterop 链系统库(OpenSSL so / HUKS ndk)会把 fork 绑死在特定 SDK 版本和符号集上,且 HUKS 是异步会话模型,与引擎的阻塞契约对不齐。纯 Kotlin 实现零外部依赖、契约逐字节可控,代价是性能——对键值持久化的载荷规模(百字节级)完全够用。关键是配套证明:双编源集让 jvmTest 的 NIST KAT 验证的就是 .so 里那份代码,没有这层,"自己写的加密"四个字毫无证据力。
决策 3:@Stable 按目标双线 compileOnly,源码零改动
commonMain 的 @Stable 注解引用 compose-runtime。社区 ohos 线 1.9.2-0.2.0-04 没有 jvm 变体、上游 1.10.3 没有 ohos 变体,单一坐标无法同时服务两个目标。按目标分开 compileOnly(jvm 1.10.3 / ohos 1.9.2-0.2.0-04)后源码一字不动,非 Compose 消费者也不被拖进 compose-runtime 依赖。
决策 4:SOFTWARE 层级如实上报,不冒充硬件
上游 protectionInfo 的设计意图就是让调用方知道密钥的真实 custody。ohos 引擎是软件密钥(沙箱密钥文件、无 HUKS),fork 就报 SOFTWARE + ohos_software_keys_no_huks notes 码,custody 文案写明实现方式。等 HUKS 桥落地,protectionInfo 自然升级——撒谎才是不兼容。
决策 5:jvmTest 交叉验证 + 测试零改动 + KAT 常驻
引擎逻辑在 commonMain,JVM 与 ohos 编的是同一份代码;808 项上游 jvmTest(含 DataStore 后端、并发压力、损坏恢复全链路)零改动全绿,是"引擎没漂"的闸门。turbine 没有 ohos 制品的处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"。9 项 AES-GCM KAT 常驻 jvmTest——cryptoMain 双编让它们盯住的就是设备上那份加密代码。
六、测试与验证
6.1 测试环境
本次验证使用 Windows x64 宿主机(JDK 21 Eclipse Temurin、HarmonyOS Kotlin 2.4.20-0.1.0-07、Gradle 9.6.1)完成 KLIB 编译、JVM 交叉验证与双 ABI 链接;HAP 构建、自动签名、设备安装与真机运行在 DevEco Studio 侧完成,验收设备为鸿蒙真机(arm64)。
6.2 JVM 交叉验证
.\gradlew.bat :ksafe:jvmTest :ksafe-biometrics:jvmTest
| 套件 | 用例数 | 结果 |
|---|---|---|
:ksafe:jvmTest(上游测试零改动) | 808 | 0 失败(4 skipped 为上游语义) |
:ksafe:jvmTest 新增 KAT | 9(NIST TC1/2/4/16/17 + JDK 向量 + 篡改/错 AAD/同 IV) | 0 失败 |
:ksafe-biometrics:jvmTest(上游零改动) | 101 | 0 失败(4 skipped) |
KAT 的过程价值:首跑抓出两个真实缺陷——S-box 构造 exp[255 - log[i]] 越界(i == 1 时应取模)与 GHASH V 移位符号扩展(Byte.toInt() ushr 1 污染低字节,症状是密文全对、仅 Tag 错)。修复后全绿,ohos 产物随即用修复版源码重编重链。依赖换线没有 API 漂移:coroutines / serialization 1.11.0 换成 -0.1.0-07 线后版本主体不变,上游测试照跑全绿。
6.3 HAP 与符号验证
:ksafe:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:ksafe-biometrics:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: KSafeDispatch / KSafeVersion 均为全局 T 符号
copyKnSo*: libohosksafe.so + libc++_shared.so 回填 entry/libs/{arm64-v8a,x86_64}
DevEco CompileArkTS / BuildNativeWithCmake / PackageHap: finished(修复 @State key 撞名后)
install bundle successfully
start ability successfully
libohosksafe.so debug 版 arm64 约 10.8 MB、x86_64 约 10.2 MB(含 Kotlin/Native 运行时与 coroutines/serialization,HAP 内 strip 后显著缩小)。手写 libohosksafe_api.h 与 KN 生成头逐符号核对一致(libohosksafe_ExportedSymbols / DisposeString / 两个业务符号)。
6.4 功能用例
- 启动后关于区出现
ksafeVersion() → 3.3.0-ohos.1(来自真实 Native 调用,非静态文字); baseDir自动注入真机沙箱(/data/storage/el2/base/haps/entry/files);- 默认 JSON
{"name":"alice","token":"secret-42"}以加密模式写入,状态行显示写入成功 key=username mode=encrypted,日志出现→ put与← put ok mode=encrypted(见下节效果图); - 点读取,
value原样返回写入内容,字段无转义损伤; - 杀掉应用重启后再点读取,值仍在——落盘密文持久化成立,不是内存缓存;
- 密文检查 Tab:store 文件路径、字节数、前 240 字节 base64——默认 fileName 与 key 的明文不出现在其中,落盘即密文;
- 点删除后读取返回
null;清空全库后重启读取为空,密钥文件随库清除; - 轮换密钥返回 rotated 计数,轮换后旧值仍可读(新代数重加密);
- 关于区
protectionInfo显示 intended/effective 均SOFTWARE、notes 含ohos_software_keys_no_huks、operational=true;生物识别行显示 no-op 语义; - 每步操作按时间戳滚动留痕,
ok:false时错误原文红字进状态行。
6.5 验证结论
加密引擎、密钥库、存储后端、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的加密持久化会话:版本握手、加密写入、读取回显、密文检查与保护信息如实上报(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/KSafe.git
cd KSafe
export JAVA_HOME="/path/to/jdk-21"
./gradlew :ksafe:compileKotlinOhosArm64 :ksafe:compileKotlinOhosX64 # klib
./gradlew :ksafe-biometrics:compileKotlinOhosArm64 :ksafe-biometrics:compileKotlinOhosX64
./gradlew :ksafe:jvmTest :ksafe-biometrics:jvmTest # 918 项交叉验证(808+9+101)
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp → 自动签名 → 构建 HAP
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.ksafe -m entry
hdc shell hilog | grep KSafe
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:ksafe/:ksafe-biometrics真正编译成ohosArm64/ohosX64;对安全库还要加一层:密文必须与上游契约逐字节同构,NIST KAT 就是那把尺; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement最前,否则配置期直接Unresolved reference: ohosArm64;- eazytec 代理对
androidx.*只存元数据不带 jar,Gradle 把模块绑定到元数据仓库后解析必败——依赖仓必须加includeGroupByRegex("org[.]jetbrains.*")内容过滤,外国组走 google()/mavenCentral(); - compose-runtime 社区 ohos 线(1.9.2-0.2.0-04)没有 jvm 变体:
@Stable的 compileOnly 必须按目标分开声明,commonMain 一刀切必挂一个目标; - Gradle 9.6 移除了
by creating/by getting/by registering委托,上游脚本写法要整体换成create()/getByName(); - KAT 首跑即抓 bug:S-box 构造的指数表取模(
(255 - log[i]) % 255)与 GHASH 移位符号扩展(负数字节toInt()后ushr污染低字节)——"能加密"和"加密对了"之间隔着一组 NIST 向量; - ohos 绑定里
pthread_t是CPointer<__pthread>(musl 指针型):可重入锁的属主记账存指针、按指针相等判重入;不要用rawValue ?: 0L(NativePtr是 value class,与 Long 做 elvis 会推成Any); - Kotlin 不允许八进制字面量,
0700/0600要写0x1C0u/0x180u; - ArkTS 组件状态不能叫
key:与CustomComponent内置通用属性key(value: string)撞名直接编译失败,改名(如storeKey)即过,桥协议字段名不受影响; - 鸿蒙线
Dispatchers.IO是 internal(FFRT 调度下无独立 IO 池),引擎与桥统一Dispatchers.Default;Main跨线程派发有已知问题,桥全程runBlocking不碰 Main; - turbine 没有 ohos 制品:测试统一 JVM 跑 + 根构建禁用
Ohos*Test*,上游测试文件零改动; build-profile.json5的signingConfigs缺material字段会被 hvigor schema 拒绝——仓库内留空数组,DevEco 勾自动签名后回填;HAP 缺当前 ABI 的 so 报安装失败9568347;KN 返回字符串必须DisposeString归还运行时;- Windows 测试机连跑双模块
--rerun-tasks(forkEvery=1 + 2g 堆)会耗尽原生内存——逐模块跑即可,Gradle/Kotlin 守护进程先--stop清理。
8.2 已知问题
- 未接 HUKS:密钥软件持有(沙箱密钥文件),
protectionInfo如实上报SOFTWARE(notes 码ohos_software_keys_no_huks),HARDWARE 系列请求按平台惯例降级; SecurityCheckerohos actual 全 false(同 Web 姿势),开发者模式 / ptrace 真信号待接;ksafe-compose留盘不进构建(依赖 Compose 运行时 API,鸿蒙 UI 是 ArkUI),源文件保留待 Compose 工具链对齐;- 生物识别 no-op:
biometricsAvailable=false、verifyBiometric直通返回 true 且从不种授权缓存(common KDoc 契约),user_auth桥是未来工作; - android / js / wasmJs / apple 目标剥离(源文件留在树里),发布、API 校验一并剥离,本 fork 不发 Maven Central;
- store 身份绑 store 文件绝对路径(v3 AAD):
baseDir变了旧密文读不回——这是防密文移植的按设计失败,迁移场景要显式搬数据; - ArkTS 页面的
getContext在新 SDK 有 deprecation 警告(功能正常,后续可换getUIContext().getHostContext())。
8.3 未来优化方向
- 接 HUKS:密钥进系统级密钥库,
protectionInfo上报真实层级,密文契约不变、引擎可平滑替换(v3 信封与 AAD 绑定都在 commonMain); - 生物识别接
user_auth(指纹 / 人脸),用 commonMain 现成的beginBiometricAttempt+ promptGate 接线,替换 no-op actual; SecurityChecker接开发者模式探测 //procptrace 检测等真信号;- 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、
llvm-nm符号检查、NIST KAT 与 HAP 构建; ksafe-compose待社区 Compose for OpenHarmony 工具链与 Kotlin 线对齐后进构建。
九、总结
9.1 核心难点回顾
上游 ksafe + ksafe-biometrics(commonMain 引擎)
→ 盘点加密面:四平台各有密钥库 API,ohos 一个都调不到
→ 验证 ohos kotlinx 制品线(coroutines / serialization 1.11.0-0.1.0-07)
→ 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1 沿用)
→ 根构建替换(cmp_qrose 模式,仓库内容过滤,serialization 插件模块级补回)
→ ohosArm64 / ohosX64 KLIB + actual ×5 + 工厂/密钥库/存储
→ 纯 Kotlin AES-GCM(cryptoMain 双编)+ NIST KAT 常驻 jvmTest
→ C ABI(@CName × 2)+ 10 op JSON 桥
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机三 Tab 验收(写入 / 读取 / 重启持久化 / 密文检查 / 保护信息)
9.2 封装层次
- 引擎层:上游原版
KSafeCore、v3 信封、写合并、热缓存、密钥代数轮换与孤儿清扫; - 加密层:本 fork 新增
OhosAesGcm(纯 Kotlin AES-GCM)+OhosSoftwareEncryption引擎接线 +OhosFileKeyStore沙箱密钥库; - 存储层:
OhosFileStorage事务写 JSON 文件 + StateFlow 快照镜像; - Native 层:双 ABI 动态库经 2 个 C ABI 输出 JSON;
- N-API 层:参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:页面状态、JSON 分发、日志与密文证据展示;
- DevEco 层:CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 适配存储类先查制品线,适配安全类还要盘点"平台密钥 API 有没有 Kotlin 侧答案":KStore 那一批 kotlinx 制品补齐后 8 行 actual 就收工;KSafe 的 expect 面同样小,真正的鸿沟在加密面——没有平台 crypto 可调时,自建纯 Kotlin 引擎是正解,但必须配套双编源集 + NIST KAT,否则"自己写的加密"没有证据力;
- KAT 不是仪式,是能抓住真 bug 的闸门:这次首跑就抓出 S-box 取模越界和 GHASH 符号扩展两个缺陷——后者症状是密文全对、仅 Tag 错,不用标准向量几乎不可能发现。"能跑通"和"密码学正确"之间隔着一组已知答案向量;
- 仓库内容过滤是代理 Maven 的必备姿势:社区代理对外国组只存元数据不带 jar 时,Gradle 会把模块绑死在元数据仓库;把社区仓过滤到自家组(
org.jetbrains.*)、其余走官方仓,版本号本身(带-0.1.0-07后缀)天然防串线。
9.4 适配成果
当前适配已完成:
- KSafe
3.3.0(上游)真实引擎的双 ABI Kotlin/Native 编译(上游commonMain与各平台源文件零改动,新增 ohosMain / cryptoMain 源码与一份 ohos 工厂); - HarmonyOS kotlinx 制品线在加密持久化库上的完整落地(coroutines / serialization
1.11.0-0.1.0-07,compose-runtime@Stable按目标双线 compileOnly); - 纯 Kotlin AES-GCM 引擎(12 字节 IV、128 位 Tag、
IV‖密文‖Tag契约冻结)+ 沙箱文件密钥库 / 存储后端 / ohos 工厂 / 公共诊断对象; - 2 个 C ABI 符号和 C++ N-API 包装层(
llvm-nm验证全局 T 符号,手写头与生成头逐符号一致); - 918 项 JVM 交叉验证全绿(上游 808 + biometrics 101 零改动,NIST KAT 9 项常驻)+ 双 ABI 三件套 so、HAP 构建、签名、设备安装和真机验证;
- DevEco Stage ArkUI 三 Tab 验收页面(加密写入、读取回显、重启持久化、密文检查、版本握手、保护信息、操作日志);
- AtomGit 项目文档、OpenHarmony README(中英双份)和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐


所有评论(0)