本文记录 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回显本次操作与目标键。
valueget 返回的原值(或 null);写操作为 null。
modeput 的写入模式(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(Compose MutableState 持久化桥)留盘不进构建;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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(Eclipse Temurin,org.gradle.java.home 指定)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle9.6.1(沿用上游 wrapper;2.4.20 线支持 Gradle 9.x)
kotlinxcoroutines / coroutines-test 1.11.0-0.1.0-07、serialization-json 1.11.0-0.1.0-07(ohos 制品线)
Compose runtimecompileOnly 双线:jvm 1.10.3(上游坐标)/ ohos 1.9.2-0.2.0-04(社区线)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.ksafe
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
ksafe46纯 Kotlin + coroutines + serialization,@Stable 注解 compileOnly compose-runtime;expect 共 11 处整体编译(jvm + 双 ohos),新增 ohosMain + cryptoMain
ksafe-biometrics5纯 Kotlin + coroutines,expect 5 处整体编译(no-op actual,与 JVM-on-Linux 同款退化)
ksafe-compose3org.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,所以引擎自己写,四件事钉死:

  1. 契约冻结:AES-GCM、96 位 IV、128 位 Tag、输出 IV‖密文‖Tag、AAD 原样透传(v3 信封的 store 身份绑定骑在 AAD 上)、密钥缺失抛 KSafeEngineMessage.noKeyFound 精确拼写(孤儿清扫靠它区分"钥匙没了"和"密文坏了")。ohos 写出的密文必须和其它平台的信封逐字节同构——将来 HUKS 桥替换引擎时,旧密文还得能解。
  2. S-box 用 GF(2⁸) 算术构造,不手抄表:求逆 + 仿射变换在初始化时算出来。手抄 256 字节的表错一个字节,加密"能用"但和任何标准实现对不上,肉眼查不出来。
  3. GHASH + CTR 按 NIST SP 800-38D 实现,密钥长度 128/256 跟随 KSafeConfig.aesKeySize,不引入新档位。
  4. cryptoMain 是 jvm + ohos 双编共享源集:同一份 OhosAesGcm.kt,jvm 编进 jvmTest、ohos 编进 KLIB。jvmTest 里跑 NIST 已知答案向量(TC1/2/4/16/17 + JDK javax.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写读回环、重启仍在、清库连密钥一起删
infoprotectionInfoSOFTWARE + notes 码如实上屏
keyInfogetKeyInfo层级 / storage / 代数 / protection
rotaterotateKeys轮换后旧密文仍可解
rawfile诊断对象store 文件前 240 字节 base64——落盘即密文
biometricsKSafeBiometricsavailable=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.tomlKotlin 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.ktNIST TC1/2/4/16/17 + JDK 参考向量 + 篡改/错 AAD 用例
ksafe/src/ohosMain/.../KSafeConcurrent.ohos.ktPOSIX 递归锁 + kotlin.concurrent.atomics 复制写容器
ksafe/src/ohosMain/.../KSafeSecureRandom.ohos.kt/dev/urandom CSPRNG
ksafe/src/ohosMain/.../{KSafeLog,SecurityChecker}.ohos.ktprintln 日志;安全检查全 false(Web 姿势)
ksafe/src/ohosMain/.../OhosFiles.ktPOSIX 文件原语:事务写 / mkdirs / chmod / 读文件
ksafe/src/ohosMain/.../OhosFileKeyStore.ktJSON 密钥文件(Base64 密钥、事务写、缓存失效)
ksafe/src/ohosMain/.../OhosFileStorage.ktKSafePlatformStorage 沙箱 JSON 实现 + StateFlow 镜像
ksafe/src/ohosMain/.../OhosSoftwareEncryption.ktKSafeEncryption 引擎:铸 IV、seal/open、noKeyFound 精确拼写、prewarm 只读不铸
ksafe/src/ohosMain/.../KSafe.ohos.kt工厂(baseDir 契约、AAD 身份、commit mutex、SOFTWARE 如实上报)+ 诊断对象
ksafe-biometrics/src/ohosMain/.../KSafeBiometrics.ohos.ktno-op actual(available=false、直通 true、不种缓存)
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、runBlocking 包裹、MiniJson、10 op 分发、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 三 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事务写 + 快照流(喂热缓存)
NativeKSafeDispatch / KSafeVersionC ABI JSON 导出
N-APIksafeDispatch(req) / ksafeVersion()向 ArkTS 暴露能力
ArkTSdispatchOp(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(上游测试零改动)8080 失败(4 skipped 为上游语义)
:ksafe:jvmTest 新增 KAT9(NIST TC1/2/4/16/17 + JDK 向量 + 篡改/错 AAD/同 IV)0 失败
:ksafe-biometrics:jvmTest(上游零改动)1010 失败(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 功能用例

  1. 启动后关于区出现 ksafeVersion() → 3.3.0-ohos.1(来自真实 Native 调用,非静态文字);
  2. baseDir 自动注入真机沙箱(/data/storage/el2/base/haps/entry/files);
  3. 默认 JSON {"name":"alice","token":"secret-42"} 以加密模式写入,状态行显示 写入成功 key=username mode=encrypted,日志出现 → put 与 ← put ok mode=encrypted(见下节效果图);
  4. 点读取,value 原样返回写入内容,字段无转义损伤;
  5. 杀掉应用重启后再点读取,值仍在——落盘密文持久化成立,不是内存缓存;
  6. 密文检查 Tab:store 文件路径、字节数、前 240 字节 base64——默认 fileName 与 key 的明文不出现在其中,落盘即密文;
  7. 点删除后读取返回 null;清空全库后重启读取为空,密钥文件随库清除;
  8. 轮换密钥返回 rotated 计数,轮换后旧值仍可读(新代数重加密);
  9. 关于区 protectionInfo 显示 intended/effective 均 SOFTWARE、notes 含 ohos_software_keys_no_huks、operational=true;生物识别行显示 no-op 语义;
  10. 每步操作按时间戳滚动留痕,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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :ksafe / :ksafe-biometrics 真正编译成 ohosArm64 / ohosX64;对安全库还要加一层:密文必须与上游契约逐字节同构,NIST KAT 就是那把尺;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. eazytec 代理对 androidx.* 只存元数据不带 jar,Gradle 把模块绑定到元数据仓库后解析必败——依赖仓必须加 includeGroupByRegex("org[.]jetbrains.*") 内容过滤,外国组走 google()/mavenCentral();
  4. compose-runtime 社区 ohos 线(1.9.2-0.2.0-04)没有 jvm 变体:@Stable 的 compileOnly 必须按目标分开声明,commonMain 一刀切必挂一个目标;
  5. Gradle 9.6 移除了 by creating / by getting / by registering 委托,上游脚本写法要整体换成 create() / getByName();
  6. KAT 首跑即抓 bug:S-box 构造的指数表取模((255 - log[i]) % 255)与 GHASH 移位符号扩展(负数字节 toInt() 后 ushr 污染低字节)——"能加密"和"加密对了"之间隔着一组 NIST 向量;
  7. ohos 绑定里 pthread_t 是 CPointer<__pthread>(musl 指针型):可重入锁的属主记账存指针、按指针相等判重入;不要用 rawValue ?: 0L(NativePtr 是 value class,与 Long 做 elvis 会推成 Any);
  8. Kotlin 不允许八进制字面量,0700 / 0600 要写 0x1C0u / 0x180u;
  9. ArkTS 组件状态不能叫 key:与 CustomComponent 内置通用属性 key(value: string) 撞名直接编译失败,改名(如 storeKey)即过,桥协议字段名不受影响;
  10. 鸿蒙线 Dispatchers.IO 是 internal(FFRT 调度下无独立 IO 池),引擎与桥统一 Dispatchers.Default;Main 跨线程派发有已知问题,桥全程 runBlocking 不碰 Main;
  11. turbine 没有 ohos 制品:测试统一 JVM 跑 + 根构建禁用 Ohos*Test*,上游测试文件零改动;
  12. build-profile.json5 的 signingConfigs 缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组,DevEco 勾自动签名后回填;HAP 缺当前 ABI 的 so 报安装失败 9568347;KN 返回字符串必须 DisposeString 归还运行时;
  13. Windows 测试机连跑双模块 --rerun-tasks(forkEvery=1 + 2g 堆)会耗尽原生内存——逐模块跑即可,Gradle/Kotlin 守护进程先 --stop 清理。

8.2 已知问题

  • 未接 HUKS:密钥软件持有(沙箱密钥文件),protectionInfo 如实上报 SOFTWARE(notes 码 ohos_software_keys_no_huks),HARDWARE 系列请求按平台惯例降级;
  • SecurityChecker ohos 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 接开发者模式探测 / /proc ptrace 检测等真信号;
  • 评估 .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 三条经验

  1. 适配存储类先查制品线,适配安全类还要盘点"平台密钥 API 有没有 Kotlin 侧答案":KStore 那一批 kotlinx 制品补齐后 8 行 actual 就收工;KSafe 的 expect 面同样小,真正的鸿沟在加密面——没有平台 crypto 可调时,自建纯 Kotlin 引擎是正解,但必须配套双编源集 + NIST KAT,否则"自己写的加密"没有证据力;
  2. KAT 不是仪式,是能抓住真 bug 的闸门:这次首跑就抓出 S-box 取模越界和 GHASH 符号扩展两个缺陷——后者症状是密文全对、仅 Tag 错,不用标准向量几乎不可能发现。"能跑通"和"密码学正确"之间隔着一组已知答案向量;
  3. 仓库内容过滤是代理 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。

参考文档

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐