jose 跨端三方库仓颉适配全流程

本文记录 jose 6.2.10 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在本地终端中通过真实的签名、校验、摘要、JWK thumbprint 和 JWE 往返操作验证库的输出。

项目地址: AtomGit/nutpi_cj/jose

上游仓库: panva/jose

开发工具: 华为云码道

一、背景

1.1 为什么适配 jose

jose 是 JavaScript 生态中用于 JSON Object Signing and Encryption 的通用库,覆盖 JSON Web Token(JWT)、JSON Web Signature(JWS)、JSON Web Encryption(JWE)、JSON Web Key(JWK)和相关编码工具。JavaScript 版本依赖 Web Crypto API 或 Node.js 的运行时能力,而仓颉强调静态类型、显式错误和可检查的模块边界。直接把 JavaScript 文件逐行翻译成仓颉,无法自然表达动态 JSON、密钥输入和不同序列化形式之间的关系。

本项目把 jose 的核心调用模型重新组织为仓颉接口:JSON 使用字符串作为边界,签名结果和解密结果使用明确的类表达,认证失败统一抛出 JoseError。这样熟悉 jose 的开发者可以继续按 JWT、JWS、JWE 和 JWK 的概念迁移调用代码,同时获得仓颉编译器的参数和返回值检查。

1.2 跨端三方库的适配边界

当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:

边界 仓颉实现
包管理 cjpm.toml、cjpm.lock、cjpm build 和静态库输出
编码 RFC 4648 Base64URL 编解码
摘要与认证 纯仓颉 SHA-256、HMAC-SHA-256 和常量时间签名比较
JWS/JWT HS256 Compact、Flattened、General 序列化,JWT 解码和无签名 JWT
JWK oct、RSA、EC JSON key 导入导出和 JWK thumbprint
JWE Compact、Flattened、General 五段结构的本地认证流加解密往返
交付形态 仓颉静态库 libjose_cangjie.a 和接口文件 jose_cangjie.cjo
验证方式 test/main.cj 独立编译测试和 example/example.cj 终端示例

这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。当前仓颉实现以依赖为零、可重复运行和可观察输出为目标;JWS/JWT、Base64URL、SHA-256、HMAC 和 JWK thumbprint 可以直接与外部实现比对。JWE 适配保持 Compact JWE 的五段形状,使用纯仓颉认证流构造完成本地往返验证;需要和外部系统互操作的 AES-GCM 密文时,仍应使用目标平台密码库或原始 jose 实现。

1.3 适配后的能力

当前导出的仓颉 API 共 39 个顶层公开声明(包含重载),覆盖以下能力:

  • 编码与摘要:base64UrlEncode、base64UrlDecode、base64UrlDecodeText、sha256、hmacSha256 和 hmacSha256Text;
  • JWT/JWS:compactSign、compactVerify、jwtSign、jwtVerify、decodeJwt、decodeProtectedHeader、unsecuredJwt,以及 CompactSign、FlattenedSign、GeneralSign 类;
  • JWK:Jwk、importJwk、exportJwk、generateSecret、calculateJwkThumbprint 和 calculateJwkThumbprintUri;
  • JWE:compactEncrypt、compactDecrypt、flattenedEncrypt、flattenedDecrypt、generalEncrypt、generalDecrypt,以及 CompactEncrypt、FlattenedEncrypt、GeneralEncrypt 类;
  • 结果和错误:JwsResult、JweResult、JoseError,所有示例都会打印实际 token、JSON、明文或异常信息。

二、实现路线图

text 复制代码
第 1 阶段:盘点 API       ── 对照 jose 的 JWT/JWS/JWE/JWK 导出,确定仓颉类型和密码学边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和仓库外静态库输出
第 3 阶段:类型映射       ── 用 class、String、Array<UInt8> 和显式异常替换动态输入
第 4 阶段:实现核心流程   ── 完成 Base64URL、SHA-256、HMAC、JWS、JWT、JWK 和 JWE 结构
第 5 阶段:测试与核对     ── 编译测试入口,执行真实签名校验、thumbprint 和加解密往返
第 6 阶段:终端演示       ── 输出所有中间结果、序列化形式、解密明文和篡改 token 错误

每个阶段都有可检查的产物:工程阶段得到 cjpm.toml 和 cjpm.lock,实现阶段得到 .../.cjpm-target/jose_cangjie/release/jose_cangjie,验证阶段得到 ALL JOSE CANGJIE TESTS PASS,最终示例则输出一组由仓颉库实际计算出的令牌、签名、thumbprint 和明文。

三、逐步实现过程

第 1 阶段:盘点原库 API 和仓颉工程边界

1.1 识别动态 API

jose 的 JavaScript API 接受字符串、Uint8Array、CryptoKey、KeyObject 和多种 JSON 序列化对象。仓颉没有与这些 JavaScript 动态联合类型完全等价的边界,因此本适配先筛选可以由静态类型稳定表达的核心流程,再为这些流程设计清晰签名。

payload、claims 和 JWK 使用 JSON 字符串;二进制密钥和摘要使用 Array;签名、解密和 JWK 结果分别用 JwsResult、JweResult 和 Jwk 表达;格式错误和认证失败统一通过 JoseError 表达。这样调用方可以在编译期看到参数形状,在运行时读取可观察的错误信息。

1.2 固定仓颉项目结构

以下目录树按照当前 jose 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 jose JavaScript 源码、测试和文档目录保持原位置:

text 复制代码
jose/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│   └── jose.cj
├── test/
│   └── main.cj
└── example/
    ├── cjpm.toml
    ├── cjpm.lock
    └── example.cj

源码集中在 src/jose.cj,按编码、摘要、签名、JWK 和加密流程组织。测试入口位于 test/main.cj,示例工程位于 example/,两者都独立于库源码。根包的 target-dir 是仓库外的 .../.cjpm-target/jose_cangjie;示例使用 src-dir = ".",target-dir 放在仓库外的 .../.../.cjpm-target/jose_cangjie_example,因此构建输出不会被当作源码再次扫描。

第 2 阶段:创建 cjpm 工程

根目录 cjpm.toml 使用静态库输出类型,并固定源码目录、整数溢出策略和仓库外构建目录:

toml 复制代码
[package]
name = "jose_cangjie"
version = "6.2.10"
cjc-version = "1.1.3"
src-dir = "src"
target-dir = "../.cjpm-target/jose_cangjie"
output-type = "static"
compile-option = "--int-overflow wrapping"

示例工程使用本地路径依赖,并把构建目录放在 example 外部:

toml 复制代码
[package]
name = "jose_cangjie_example"
version = "1.0.0"
cjc-version = "1.1.3"
src-dir = "."
target-dir = "../../.cjpm-target/jose_cangjie_example"
output-type = "executable"

[dependencies]
jose_cangjie = { path = ".." }

包版本沿用当前 jose 版本,包名使用 jose_cangjie。构建成功后,最重要的两个库产物是:

text 复制代码
../.cjpm-target/jose_cangjie/release/jose_cangjie/libjose_cangjie.a
../.cjpm-target/jose_cangjie/release/jose_cangjie/jose_cangjie.cjo

其中 .a 用于链接,.cjo 提供编译器需要的接口信息。example 的可执行文件写入仓库外的 .cjpm-target/jose_cangjie_example,验证完成后清理这些目录,不提交构建产物。

第 3 阶段:把 JavaScript 动态输入映射为仓颉类型

3.1 用字符串和结果类表达 JSON 与 token

仓颉边界不强制引入新的 JSON 依赖,调用方可以直接传入 JSON claims:

cangjie 复制代码
let claims = "{\"sub\":\"cangjie-user\",\"role\":\"admin\"}"
let token = jwtSign(claims, "demo-secret")
let verified = jwtVerify(token, "demo-secret")
println(verified)

JWS 操作返回 JwsResult,其中同时保留完整 token、保护头、payload 和签名;JWE 操作返回 JweResult,其中保留 token、保护头和解密后的 plaintext。调用方不需要从打印文本中猜测操作是否成功,而是可以读取明确的字段。

3.2 用 Array 表达编码和摘要数据

Base64URL 同时提供字符串和字节数组入口,适合 claims、签名以及二进制摘要:

cangjie 复制代码
let encoded = base64UrlEncode("Hello, Cangjie!")
let decoded = base64UrlDecodeText(encoded)
let digest = sha256("abc")
println("${encoded} -> ${decoded}")
println(digest)

sha256 和 hmacSha256 使用纯仓颉实现,输出可以使用 Base64URL 形式直接放入 JOSE token。HMAC 校验使用常量时间比较,篡改 token 时会抛出 JoseError,而不是返回一个看似成功的结果。

3.3 用 JoseError 表示失败

JavaScript jose 会区分 token 格式错误、签名校验失败和 JWE 认证失败。仓颉版本把这些失败收敛到可捕获的 JoseError:

cangjie 复制代码
try {
    compactVerify(token, "wrong-secret")
} catch (error: JoseError) {
    println(error.message)
}

这种错误模型可以覆盖错误 token、错误密钥、错误 Base64URL 字符和错误 JWE tag。示例还会主动篡改 token 并打印 signature verification failed,证明异常来自库的实际校验路径。

第 4 阶段:实现源码

4.1 文件 src/jose.cj

源码按 JOSE 生命周期组织:先完成 Base64URL 编解码,再实现 SHA-256 和 HMAC-SHA-256;随后把保护头和 payload 编码为 Compact JWS/JWT,最后复用相同的认证结果生成 Flattened、General 和 JWE 形态。

compactSign 生成 HS256 保护头、Base64URL payload 和签名;compactVerify 重新计算签名并进行常量时间比较。decodeProtectedHeader 和 decodeJwt 只负责解码,不把解码误认为签名验证。JWK thumbprint 按 kty 的规范字段顺序构造 canonical JSON,再计算 SHA-256。

4.2 JWS/JWE 序列化

Flattened 和 General JWS/JWE 通过 JSON 字符串表达,保留 protected、payload、signature、iv、ciphertext、tag 和 recipients 等结构字段。example/example.cj 会分别打印 Compact、Flattened、General 形式,并调用对应的 verify/decrypt 函数读取真实 payload 或 plaintext。

JWE 使用五段 Compact 形状:保护头、空的 encrypted key、iv、ciphertext 和 tag。当前实现使用纯仓颉认证流构造保证本地加密、解密和篡改检测可以重复运行;它不是对平台 AES-GCM 的替代实现,因此文章明确记录互操作边界。

4.3 密钥和平台边界

importJwk、exportJwk 和 generateSecret 为仓颉调用方提供 JWK 边界,calculateJwkThumbprint 和 calculateJwkThumbprintUri 提供可观察的 key identity。当前版本支持 oct、RSA、EC JWK 的 thumbprint 字段选择,但签名示例使用 HS256 字符串密钥;RSA、EC 签名和平台私钥导入需要后续接入目标平台密码库。

第 5 阶段:建立自动化测试

test/main.cj 是一个直接链接仓颉静态库的可执行测试入口,覆盖以下行为:

测试组 示例
编码 Base64URL 字符串往返和 SHA-256 已知向量
认证 HMAC-SHA-256、Compact JWS 签名与校验
JWT claims 签名、校验、保护头解码和 payload 解码
序列化 Flattened/General JWS 的 JSON 结构和 payload 往返
JWK oct JWK thumbprint、thumbprint URI 和导入结果
加密 Compact/Flattened/General JWE 明文往返
错误 篡改 token 后的签名校验异常

断言失败会抛出异常并以非零状态退出;全部通过时输出固定标记:

text 复制代码
ALL JOSE CANGJIE TESTS PASS

示例工程则负责展示完整中间结果,测试程序负责断言库调用,两者共同覆盖编译期 API 和运行时签名、解码、thumbprint 及加解密行为。

第 6 阶段:核对 jose 适配 API

除了行为测试,还扫描 src/jose.cj 的顶层 public 声明,核对文档列出的公开入口:

text 复制代码
JOSE Cangjie public declarations: 39
Missing documented core entries: 0

这些入口包括结果类、错误类、Base64URL、SHA-256、HMAC、JWT/JWS、JWK 和 JWE 操作。这个检查说明当前仓颉适配的公开入口与文档一致,不代表它已经覆盖 JavaScript jose 的所有 Web Crypto、RSA、EC、EdDSA、远程 JWKS 和平台密钥存储语义。

四、完整验证步骤

以下命令均以仓库根目录 jose/ 为当前目录。先确认本机已经安装仓颉 SDK:

bash 复制代码
echo "$CANGJIE_HOME"
cjc --version
cjpm --version

然后构建静态库:

bash 复制代码
cjpm build

编译并运行测试入口:

bash 复制代码
cjc --int-overflow wrapping --static-std --static-libs --set-runtime-rpath \
  --import-path ../.cjpm-target/jose_cangjie/release/jose_cangjie \
  -L ../.cjpm-target/jose_cangjie/release/jose_cangjie \
  -l jose_cangjie test/main.cj -o /tmp/jose_cangjie_test
/tmp/jose_cangjie_test

再构建并运行独立的终端示例工程:

bash 复制代码
cd example
cjpm check
cjpm run

示例会打印真实计算结果,而不是只打印断言通过:

text 复制代码
JWT compact token: eyJhbGciOiJIUzI1NiIs...
Verified JWT claims: {"sub":"cangjie-user",...}
JWK thumbprint: 3WO-bsQ7y_2lOf_qMtm7A4gTfRT83X8INFtT9vMN09k
JWE decrypted plaintext: JWE plaintext from Cangjie
Tampered token result: rejected: signature verification failed
=== every demonstrated operation produced a value ===

示例使用 target-dir = ".../.../.cjpm-target/jose_cangjie_example",位于 src-dir = "." 的扫描范围之外,因此运行 cjpm run 时不会出现把构建输出目录当作源码扫描目录的警告。根包可能针对原仓库 JavaScript 子目录输出没有 .cj 文件的扫描提示,但这些提示不是 target 目录扫描。验证结束后执行 cjpm clean,并确认仓库内没有 .cjpm-target 或其他构建产物。

五、运行效果

下面的效果图来自 example/example.cj 的实际终端运行,包含 Compact JWE、JWE protected header、解密明文、Flattened/General JWE 往返和篡改 token 的错误结果。

这张图可以作为文章或项目说明中的验收证据:终端同时展示了密文 token、保护头、解密后的明文、Flattened/General 序列化结果和签名校验失败信息。所有文本都由仓颉示例程序调用 jose_cangjie 后实时计算得到,因此效果图不是模拟页面,也不是只显示断言通过。

六、关键决策说明

6.1 为什么使用显式结果类和 JSON 字符串边界

仓颉适配的价值在于让调用方在编译期知道 token、payload、signature 和 plaintext 的数据形状。若所有结果都退化成无类型 String,虽然表面上接近 JavaScript,却会失去校验结果和解密结果的字段检查。因此项目使用 JwsResult、JweResult、Jwk 和 JoseError 表达核心状态,JSON 字符串只作为 claims、JWK 和 Flattened/General 序列化的输入边界。

6.2 为什么保留 jose 的概念和方法名称

保留 compactSign、compactVerify、jwtSign、jwtVerify、decodeJwt、calculateJwkThumbprint、compactEncrypt 和 compactDecrypt 等名称可以降低迁移成本。熟悉 jose 的开发者只需要适应仓颉的参数标签、Array 和异常捕获,不必重新记忆一套完全不同的安全令牌 API。对暂时无法一比一映射的 RSA/EC 私钥、远程 JWKS 和平台 AES-GCM,适配通过文档说明边界,而不是伪造已经支持的行为。

6.3 为什么把测试和示例分开

测试程序负责断言,适合自动化执行;示例工程负责展示,适合在终端中观察完整 token、JSON、明文和错误。两者都使用同一个 jose_cangjie 包,因此效果图不是模拟页面,而是库的实际运行输出。仓颉项目布局不会自动把根目录外的 test/main.cj 当作 cjpm test 用例,所以文档提供了直接链接静态库并运行该入口的命令。

七、兼容边界和遗留问题

JavaScript jose 与仓颉以及目标平台的密码学模型不同,以下限制需要在接入时提前了解:

  1. 当前签名适配以 HS256/HMAC-SHA-256 为可互操作示例,RSA、EC、EdDSA 和 ML-DSA 签名需要后续接入目标平台密码库。
  2. 当前 JWE 适配保持 Compact、Flattened、General 的 JOSE 字段形状并完成本地认证流往返,不提供可替代平台 AES-GCM 的互操作实现。
  3. claims 和 JWK 使用 JSON 字符串边界,当前没有提供完整 JSON AST、schema 校验、自动时间 claims 校验和动态 key resolver。
  4. JWK thumbprint 支持 oct、RSA、EC 的规范字段选择;远程 JWKS 获取、缓存、kid 选择和多 key 匹配尚未映射。
  5. 当前没有实现 Web Crypto API、Node.js KeyObject、CryptoKey、PEM/PKCS8/SPKI 导入导出和 X.509 证书处理。
  6. generateSecret 使用可复现 seed 生成示例密钥,适合测试和演示;生产环境应改用目标平台提供的密码学安全随机源。
  7. 当前仓颉包不绑定 UI、网络或系统框架,调用方需要根据平台提供的密码库、随机源和密钥存储能力扩展实现。

这些不是构建错误。只要 cjpm build 成功、测试程序输出通过标记、示例输出每项 token/明文/错误结果,就说明当前版本已经完成本地编译和行为验收;接入生产安全链路时则必须按目标平台补齐真实随机源、AES-GCM 和非对称密钥能力。

八、接入方式

在另一个仓颉项目中,可以将本仓库作为本地依赖或发布包使用。最直接的本地验证方式是先在本仓库根目录生成仓颉静态库,再把相同的 --import-path 和 -L 参数传给调用方的 cjc 命令。调用代码只需要导入:

cangjie 复制代码
import jose_cangjie.*

main(): Int64 {
    let claims = "{\"sub\":\"service-user\",\"scope\":\"read\"}"
    let token = jwtSign(claims, "service-secret")
    println(token)
    println(jwtVerify(token, "service-secret"))
    return 0
}

如果使用 cjpm 管理依赖,则将仓颉包加入项目依赖配置,再由业务工程统一构建。跨端项目可以复用相同的 JWT/JWS/JWK 调用、错误处理和测试思路;具体的随机源、AES-GCM、RSA/EC 签名、平台密钥存储和远程 JWKS 由宿主平台负责。

九、总结

本次适配完成了 jose 核心令牌模型到仓颉静态类型模型的迁移:Base64URL 使用字节数组和字符串重载,SHA-256/HMAC 使用纯仓颉实现,JWT/JWS 使用明确的签名结果,JWK 使用导入、导出和 thumbprint 操作,JWE 使用 Compact、Flattened、General 结构完成本地认证流往返。

验证链路包括 cjpm build、测试入口编译、真实 token 生成、保护头和 claims 解码、签名校验、JWK thumbprint、JWE 明文恢复和篡改错误检查。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库在 token、JSON、明文和异常场景中的输出。对于暂时无法一比一映射 JavaScript jose 的平台密码学能力,项目通过显式 API 和文档边界保持行为可解释,方便后续继续接入真实随机源、AES-GCM 和非对称密钥实现。

适配过程 FAQ

以下问题来自仓颉工程创建、cjpm 依赖、与 HarmonyOS 设备联调的实际流程。

Q1:cjpm 依赖解析失败或拉取超时?

  • 检查 cjpm.toml 中的依赖坐标与版本号;
  • 配置可用的仓颉包仓库镜像或代理;
  • 删除 target、cjpm.lock 后重新执行 cjpm update / cjpm build。

Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?

  • 设置 CANGJIE_HOME 指向仓颉 SDK 根目录;
  • 确认 cjc --version、cjpm --version 与工程要求的版本一致;
  • 清理构建缓存后重新生成。

Q3:与 HarmonyOS 设备联调时安装失败?

  • 用 hdc list targets 确认设备在线;
  • 确认 HAP 已签名,bundleName 与签名 profile 一致;
  • 设备需加入调试 profile 的 deviceIds。

Q4:动态库或 FFI 符号找不到?

  • 检查导出符号名称、ABI 与打包路径;
  • 确认 .so 已随 HAP 一起打包到 libs/<abi>/;
  • 用 llvm-readelf -d 检查依赖库是否齐全。

Q5:测试用例在设备上失败,但本机通过?

  • 对比设备与主机的架构、SDK 版本和环境变量;
  • 用 hdc hilog 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
CappuccinoRose3 小时前
滚动交互体系
前端·javascript·交互·html5·1024程序员节
时空节拍AI数字人3 小时前
博物馆数字人讲解系统设计:知识库 schema、展项联动协议与时序控制
java·服务器·前端·人工智能·ai·语音识别
吴建旭 智宅焕3 小时前
智能家居全国交付中前端工具与后端基础设施的系统集成架构:报价入口与交付兜底的闭环设计
前端·架构·智能家居
开开心心就好4 小时前
电脑小说阅读器,支持朗读和自定义皮肤
服务器·开发语言·javascript·算法·pdf·c#·音视频
夏天要喝冰可乐4 小时前
Codex 写 Three.js 大屏实录:800 行代码,和一条画反的弧线
前端·aigc
欣欣之王来了4 小时前
v-bind语法糖::属性名的简写方式
前端·学习·架构·项目·vue3教程
2601_954005624 小时前
# ricon SVG 编辑器:工业图元的“军火工厂“
前端·物联网·数学建模·架构·编辑器·制造
codeniu4 小时前
一个人,5天,从想法到上线:我用 TRAE 跑通全链路开发的完整实录
前端
shaibdoio4 小时前
RPA自动化获客怎么落地?瞬维AI评论区工程实践
前端·自动化·rpa·长沙瞬维ai