bcrypt.js 跨端三方库仓颉适配全流程

本文记录 bcrypt.js 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在本地终端中通过真实 bcrypt 计算验证盐值生成、哈希、密码比较、轮数和盐提取、72 字节截断检测以及 bcrypt Base64 编解码能力。

项目地址: AtomGit/nutpi_cj/bcrypt.js

上游仓库: dcodeIO/bcrypt.js

开发工具: 华为云码道

一、背景

1.1 为什么适配 bcrypt.js

bcrypt.js 是一个纯 JavaScript 的 bcrypt 实现,兼容常见的 bcrypt 哈希格式,不依赖原生扩展,适用于 Node.js 和浏览器。它提供盐值生成、同步和异步风格的哈希、密码比较、轮数提取、盐提取、密码长度截断检测以及 bcrypt 专用 Base64 编解码。

JavaScript 版本依赖动态参数、字符串和字节数组之间的运行时转换。salt 参数既可以是轮数,也可以是完整盐字符串;随机数回退还需要传入函数。仓颉强调静态类型、显式异常和编译期检查,因此本适配使用函数重载区分 Int64 轮数与 String 盐值,使用 Array 表达二进制数据,并将非法参数统一转换为仓颉异常。

bcrypt 的核心不是简单的摘要函数,而是 Blowfish 密钥编排和按 2 的 cost 次方重复的计算过程。适配必须保留 P-array、S-box、EksBlowfish 和 bcrypt 自定义 Base64 的真实算法,否则只能得到"接口存在"的空壳,无法与原库生成的哈希互相验证。

1.2 跨端三方库的适配边界

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

边界 仓颉实现
包管理 cjpm.toml、cjpm build 和静态库输出
bcrypt 核心 Blowfish P-array、S-box、EksBlowfish 和 64 次加密
输入类型 String 密码、Int64 轮数、String 盐值、Array 字节
随机盐 std.random.Random 和 setRandomFallback
编码 bcrypt 专用 Base64 encodeBase64/decodeBase64
元数据 getRounds、getSalt、truncates
交付形态 仓颉静态库 libbcryptjs_cangjie.a 和接口文件 bcryptjs_cangjie.cjo
验证方式 test/main.cj 静态库测试和 example/example.cj 终端示例

这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。当前 Cangjie API 采用同步调用模型:genSalt、hash 和 compare 保留原库名称,但返回直接结果;JavaScript Promise、回调和进度回调没有伪装成已经支持的仓颉异步接口。只要目标平台提供兼容版本的仓颉 SDK 和 std.random,就可以按同一套源码和 cjpm 配置构建并消费这个库。

1.3 适配后的能力

当前导出的仓颉 API 共 12 个顶层公开名称,覆盖以下能力:

  • setRandomFallback:设置生成盐值时使用的自定义字节生成器;
  • genSaltSync 和 genSalt:按默认或指定 cost 生成 bcrypt 盐值,cost 为 0 时沿用默认 10,其他值限制在 4 到 31;
  • hashSync 和 hash:使用数字 cost 或完整盐值生成 60 字符 bcrypt 哈希;
  • compareSync 和 compare:以常量时间方式比较正确和错误密码;
  • getRounds 和 getSalt:读取哈希中的 cost 和 29 字符盐值前缀;
  • truncates:检查 UTF-8 密码是否超过 bcrypt 的 72 字节输入上限;
  • encodeBase64 和 decodeBase64:处理 bcrypt 使用的非标准 Base64 字母表。

二、实现路线图

text 复制代码
第 1 阶段:盘点 API       ── 对照 bcrypt.js API,确定仓颉重载、异常和算法边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射       ── 用 String、Int64、Array<UInt8> 和函数类型替换动态值
第 4 阶段:实现 bcrypt     ── 完成自定义 Base64、Blowfish 密钥编排和哈希格式
第 5 阶段:测试与核对     ── 用标准已知向量、错误密码和边界输入验证结果
第 6 阶段:终端演示       ── 直接输出盐值、哈希、比较和编码结果,生成效果图

每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 .cjpm-target/bcryptjs_cangjie,验证阶段得到 ALL BCRYPT CANGJIE TESTS PASS,最终示例则输出来自真实仓颉库调用的全部结果。

三、逐步实现过程

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

1.1 识别动态 API

bcrypt.js 的 JavaScript 入口把 salt 定义为 number 或 string,hash 和 compare 还同时提供 Promise 与 callback 形式。仓颉没有必要保留一个运行时判断的无类型参数,因此当前版本把这些调用拆成静态重载:

cangjie 复制代码
let salt = "\$2b\$04\$......................"
let fixedHash = hashSync("password", salt)
let generatedHash = hashSync("password", 4)
let same = compareSync("password", fixedHash)

String 参数表示完整 bcrypt 盐值,Int64 参数表示 cost。非法版本、轮数、盐长度和 Base64 字符会抛出 IllegalArgumentException,调用方可以用仓颉异常机制处理错误。

1.2 固定仓颉项目结构

以下目录树按照当前 bcrypt.js 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 JavaScript、TypeScript、npm 测试和命令行脚本保持原位置:

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

源码集中在 src/bcrypt.cj,测试入口位于 test/main.cj,示例工程位于 example/。示例的 src-dir 是当前目录,但 target-dir 放在仓库根目录的 .cjpm-target/bcryptjs_example,因此构建输出不会被当作源码再次扫描,也不会出现"there is no '.cj' file ... target"警告。

第 2 阶段:创建 cjpm 工程

根目录 cjpm.toml 使用静态库输出类型,并把构建目录放到 .cjpm-target:

toml 复制代码
[package]
  cjc-version = "1.1.3"
  name = "bcryptjs_cangjie"
  version = "0.0.0"
  output-type = "static"
  src-dir = "src"
  target-dir = ".cjpm-target/bcryptjs_cangjie"

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

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

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

这样运行 example/cjpm.toml 对应的 cjpm run 时,构建输出不会进入 src-dir = "." 的扫描范围。验证结束后删除 .cjpm-target,构建产物不提交到仓库。

构建成功后,根包最重要的两个产物是:

text 复制代码
.cjpm-target/bcryptjs_cangjie/release/bcryptjs_cangjie/libbcryptjs_cangjie.a
.cjpm-target/bcryptjs_cangjie/release/bcryptjs_cangjie/bcryptjs_cangjie.cjo

其中 .a 用于链接,.cjo 提供编译器需要的接口信息。

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

3.1 密码、盐值和字节数组

密码和哈希在仓颉中使用 String,UTF-8 编码后的密码和盐值使用 Array。数字 cost 使用 Int64,哈希输出和盐值输出继续保持 String:

cangjie 复制代码
let salt = "\$2b\$04\$......................"
let hash = hashSync("password", salt)
println(hash)
println(compareSync("password", hash))

原库的 bcrypt 输出固定为 60 个字符,盐值前缀固定为 29 个字符。Cangjie 实现通过 getSalt 和 getRounds 读取这些结构,而不是让调用方手动切割字符串。

3.2 用函数类型表达随机回退

JavaScript setRandomFallback 接收一个返回随机字节的函数。仓颉版本使用明确函数类型:

cangjie 复制代码
let fallback = { length: Int64 =>
    Array<UInt8>(length, { i => UInt8((i * 17 + 3) % 256) })
}
setRandomFallback(fallback)
let deterministicSalt = genSalt(4)

测试中的回退生成器只用于验证输出稳定性。生产环境必须提供经过安全播种的密码学安全随机源,不能把示例中的确定性函数用于真实密码盐值。

3.3 用重载保持 salt 的语义

salt 的数字和字符串两种语义分别对应不同重载:

cangjie 复制代码
let byRounds = hashSync("password", 4)
let bySalt = hashSync("password", "\$2b\$04\$......................")

genSaltSync、genSalt、hashSync、hash、compareSync 和 compare 都保持清晰的返回类型。同步函数直接返回 String 或 Bool;非法参数通过异常返回,避免使用 null 或无类型结果隐藏失败原因。

第 4 阶段:实现源码

4.1 文件 src/bcrypt.cj

源码按 bcrypt 生命周期组织:先把密码转换为 UTF-8 字节,再解析版本、minor revision、cost 和 16 字节盐值;随后初始化 Blowfish P-array 与 S-box,执行 EksBlowfish,按 2 的 cost 次方交替执行 password key schedule 和 salt key schedule,最后对固定的 OrpheanBeholderScryDoubt 数据执行 64 轮加密并编码为 23 字节结果。

32 位 Blowfish 运算使用 Int64 承载中间值,并通过 add32 截断到 32 位,保证 Cangjie 的溢出检查不会改变 bcrypt 的模 2^32 运算。P-array 有 18 个元素,S-box 有 1024 个元素,常量直接来自原库的 Blowfish 初始化表。

4.2 bcrypt 专用 Base64

bcrypt 的 Base64 字母表不是 MIME Base64,而是:

text 复制代码
./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789

encodeBase64 和 decodeBase64 使用这套字母表处理盐值和最终摘要。固定输入的真实结果为:

text 复制代码
encodeBase64([0, 1, 2, ..., 16], 16)
=> ..CA.uOD/eaGAOmJB.yMBu

decodeBase64("..CA.uOD/eaGAOmJB.yMBv.", 16)
=> 16 bytes, first=0, last=15

这一步决定生成的盐值和哈希是否能与 bcrypt.js 互相识别,不能替换为标准 Base64。

4.3 盐值、轮数和截断检测

genSaltSync 会把 cost 限制在 bcrypt 支持的 4 到 31 范围;传入 0 时使用默认 cost 10。truncates 按 UTF-8 字节数判断 72 字节限制,而不是按仓颉字符串的字符数判断:

cangjie 复制代码
truncates(repeatedA(72)) // false
truncates(repeatedA(73)) // true
getRounds(fixedHash)     // 4
getSalt(fixedHash)       // $2b$04$......................

compareSync 先检查哈希长度,再用提取的盐值重新计算密码,最后逐字符累积差异完成比较。

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

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

测试组 示例
已知向量 cost 4、固定盐值和 password 的标准结果
哈希和比较 正确密码为 true,错误密码为 false
API 重载 数字 cost、完整盐值和 hash/compare 别名
元数据 getRounds、getSalt
输入边界 72 字节不截断、73 字节会截断、cost 0 使用默认值
Base64 bcrypt 字母表编码和解码
随机回退 自定义生成器产生稳定盐值

固定盐值的结果与 JavaScript bcrypt.js 相同:

text 复制代码
$2b$04$......................LAtw7/ohmmBAhnXqmkuIz83Rl5Qdjhm

全部测试通过时输出:

text 复制代码
ALL BCRYPT CANGJIE TESTS PASS

第 6 阶段:核对 bcrypt.js 适配 API

对 src/bcrypt.cj 的顶层公开声明进行核对,当前仓颉适配公开名称为 12 个:

text 复制代码
setRandomFallback
genSaltSync
genSalt
hashSync
hash
compareSync
compare
getRounds
getSalt
truncates
encodeBase64
decodeBase64

其中 genSaltSync、genSalt、hashSync 和 hash 存在参数重载,但没有引入未实现的 Promise、callback 或 progressCallback。这个检查说明当前公开入口和文档一致,不代表仓颉版本覆盖 JavaScript bcrypt.js 的全部异步运行时语义。

四、完整验证步骤

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

bash 复制代码
cjc --version
cjpm --version

然后构建静态库:

bash 复制代码
cjpm build

用生成的静态库编译并运行 test/main.cj:

bash 复制代码
cjc test/main.cj \
  --import-path .cjpm-target/bcryptjs_cangjie/release/bcryptjs_cangjie \
  -L .cjpm-target/bcryptjs_cangjie/release/bcryptjs_cangjie \
  -l bcryptjs_cangjie \
  --static \
  -o /tmp/bcryptjs-cangjie-test
/tmp/bcryptjs-cangjie-test

成功时会看到每项 PASS,并以 ALL BCRYPT CANGJIE TESTS PASS 结束。

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

bash 复制代码
cd example
cjpm build
cjpm run

示例使用 target-dir = ".../.cjpm-target/bcryptjs_example",因此不会把构建目录当作源码扫描目录。验证完成后回到仓库根目录清理构建产物:

bash 复制代码
cd ..
find .cjpm-target -type f -delete
find .cjpm-target -type d -depth -empty -delete

五、运行效果

下面的效果图来自 example/example.cj 的实际终端运行,包含 bcrypt.js Cangjie 静态库构建后的真实哈希、比较、元数据、截断检测和 Base64 结果。

终端中的 hashSync、hash、compareSync、compare、getRounds、getSalt、truncates、encodeBase64、decodeBase64、genSaltSync、setRandomFallback 和 genSalt 均由仓颉库实际调用产生。固定盐值的哈希可与 JavaScript bcrypt.js 结果逐字比对,随机盐值的内容会随运行变化,比较结果仍然为 true。

六、关键决策说明

6.1 为什么保留完整 bcrypt 核心

bcrypt 不是普通的单次哈希,cost 直接决定密钥编排重复次数。如果只实现盐值格式或只返回预先写好的示例字符串,无法验证密码存储功能,也无法与既有 bcrypt 数据兼容。因此仓颉版本保留完整 Blowfish 常量、EksBlowfish、cost 循环和 23 字节摘要编码,测试使用固定已知向量校验算法结果。

6.2 为什么保留 bcrypt.js 函数名称

保留 setRandomFallback、genSaltSync、hashSync、compareSync、getRounds、getSalt、truncates、encodeBase64 和 decodeBase64 等名称,可以降低从 JavaScript bcrypt.js 迁移的成本。仓颉调用方只需要适应参数重载、Array 和异常处理,不必重新记忆一套完全不同的密码 API。

genSalt、hash 和 compare 也保留为同步入口,便于在仓颉中使用原库熟悉的名称;Promise、callback 和 progressCallback 在当前适配范围之外,并在文档中明确说明。

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

测试程序负责固定向量和边界断言,适合自动化执行;示例工程负责展示真实盐值、哈希字符串和比较结果,适合在终端观察并生成截图。两者都使用同一个 bcryptjs_cangjie 静态库,因此效果图不是模拟页面,而是库的实际运行输出。

七、兼容边界和遗留问题

JavaScript bcrypt.js 与仓颉运行时模型不同,以下限制需要在接入时提前了解:

  1. JavaScript 的 Promise、callback 和 progressCallback 异步 API 尚未映射;当前 genSalt、hash 和 compare 是同步返回结果的仓颉入口。
  2. 密码先转换为 UTF-8 字节,bcrypt 的有效输入上限仍是 72 字节;多字节字符可能比字符数量占用更多字节,应使用 truncates 检查。
  3. Cangjie 版本使用 Int64 表示 cost,合法范围为 4 到 31;cost 越高,计算时间和资源消耗呈指数增加,示例使用 4 只为快速演示。
  4. 默认随机盐使用 std.random.Random;setRandomFallback 只适合接入调用方提供的密码学安全随机源,示例中的确定性回退函数不能用于生产环境。
  5. 支持 bcrypt.js 兼容的 2 a 2a 2a、 2 b 2b 2b 和 2 y 2y 2y 盐版本解析;不支持的版本、错误长度和非法 Base64 输入会抛出 IllegalArgumentException。
  6. 本库只负责密码哈希和比较,不提供密码数据库、用户认证流程、密钥管理、登录限流或 HAP/ArkTS 集成能力。

这些不是构建错误。只要 cjpm build 成功、固定向量测试输出 ALL BCRYPT CANGJIE TESTS PASS、示例中生成哈希后的 compare generated hash 为 true,就说明当前版本已经完成本地编译和行为验收。

八、接入方式

在另一个仓颉项目中,可以将本仓库作为本地依赖或 AtomGit 依赖使用。远程依赖配置可以写成:

toml 复制代码
[dependencies]
  bcryptjs_cangjie = { git = "https://atomgit.com/nutpi_cj/bcrypt.js.git", branch = "main" }

本地验证时,也可以使用:

toml 复制代码
[dependencies]
  bcryptjs_cangjie = { path = ".." }

调用代码只需要导入:

cangjie 复制代码
import bcryptjs_cangjie.*

main(): Int64 {
    let salt = "\$2b\$04\$......................"
    let hash = hashSync("password", salt)
    println(hash)
    println(compareSync("password", hash))
    println(getRounds(hash))
    return 0
}

如果需要生成真实随机盐值,直接调用 genSaltSync(10) 或 genSalt(10);如果目标平台需要接管随机数来源,可以先调用 setRandomFallback。跨端项目可以复用相同的盐值、哈希和比较 API,认证存储、密钥管理和 UI 展示由宿主应用负责。

九、总结

本次适配完成了 bcrypt.js 到仓颉静态类型模型的迁移:密码和哈希使用 String,二进制数据使用 Array,cost 使用 Int64,随机回退使用带类型的函数值,Blowfish P-array、S-box、EksBlowfish、bcrypt Base64 和 60 字符哈希格式均由 src/bcrypt.cj 真实实现。

验证链路包括 cjpm build、静态库链接测试、标准 bcrypt 已知向量、正确和错误密码比较、元数据提取、UTF-8 截断判断、Base64 编解码以及独立示例输出。最终效果图来自真实的仓颉可执行程序,能够直观看到三方库生成的盐值、哈希和验证结果。对于 Promise、callback、progressCallback 和宿主认证系统等无法一比一映射的部分,项目通过静态签名和文档边界保持行为可解释,方便后续扩展。

适配过程 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 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
m0_639906941 小时前
Linux 操作系统入门:目录结构、服务排查常用命令、Shell 脚本基础
linux·运维·后端
今天重构了吗2 小时前
Linux 上 rm -rf 删错了能找回吗
运维·服务器
KaiwuDB2 小时前
KaiwuDB 分布式集群扩缩容,从原理到完整操作踩坑总结
运维·数据库·分布式
codezzzsleep2 小时前
学术汇报 PPT 自动化生成实战指南
运维·自动化·powerpoint
古法老登瞎折腾2 小时前
大数据智能运维实战(九):让 AI 查日志——找到异常,也保留上下文
java·大数据·运维·人工智能·spring ai·mcp
水饺编程3 小时前
第2章:Linux 基础,exit 命令
linux·c语言·汇编
木子欢儿3 小时前
Debian 13 + vsftpd 搭建FTP服务
linux·服务器·前端·网络·debian
讯捷蓝达3 小时前
IBM V3500存储 双控故障、数据怎么恢复?
运维
qq_谁赞成_谁反对3 小时前
甲方IT的成长之路--SG TCP网络优化工具--20261026
运维·服务器·udp·tcp