# otp-cj 仓颉(Cangjie)一次性密码(OTP)库,实现 **HOTP**(RFC 4226)与 **TOTP**(RFC 6238),提供 Base32 密钥编解码(RFC 4648)与 `otpauth://` URI 生成,可直接与 Google Authenticator / Microsoft Authenticator 等常见 OTP App 互通。 组织:`simcu` | 包名:`otp` | 版本:`1.0.0` ## 特性 - **TOTP**:基于时间步长的动态码,默认 6 位 / 30 秒步长,支持自定义位数、步长与时钟偏移窗口 - **HOTP**:基于计数器的一次性密码,默认 6 位 - **Base32**:RFC 4648 无填充变体编解码,解码时忽略大小写、空格、短横线与 `=` - **otpauth:// URI**:生成可用于二维码的认证链接,并附带随机密钥生成工具 - **零三方依赖**:仅依赖仓颉标准库 `stdx.crypto.digest.SHA1` - 密钥支持 **Base32 字符串**(Google Authenticator 惯例)与**原始字节**两种传入方式 ## 安装 在 `cjpm.toml` 中声明依赖(可改用中央仓版本号): ```toml [dependencies] "simcu::otp" = { path = "../../cangjie/otp-cj" } # 或发布到中央仓后:"simcu::otp" = "1.0.0" ``` 代码中引用: ```cangjie import simcu::otp.* ``` ## 快速开始 ### 1. 生成密钥并绑定 OTP ```cangjie // 生成随机 Base32 密钥(默认 20 字节 = 160 bit) let secret = OtpUri.generateSecret() // 例如 "N6C5HNT2F633..." // 生成 otpauth:// 链接,前端渲染成二维码供用户绑定 let uri = OtpUri.build(issuer: "GamePlatform", account: "18600127718", secretBase32: secret) // otpauth://totp/GamePlatform%3A18600127718?secret=N6C5HNT2F633...&issuer=GamePlatform&algorithm=SHA1&digits=6&period=30 ``` ### 2. 生成动态码 ```cangjie // TOTP:默认 6 位 / 30s 步长,基于当前时间 let code = Totp.generate(secret) // "351047" // 自定义位数与步长 let code2 = Totp.generate(secret, digits: 8, period: 60) // 原始字节密钥 let raw = Base32.decode(secret) let code3 = Totp.generateBytes(raw) // HOTP:基于计数器(第 N 次动态码) let hotp = Hotp.generate(raw, counter: 0) // 默认 6 位 let hotp2 = Hotp.generate(raw, counter: 0, digits: 8) ``` ### 3. 校验动态码 ```cangjie // TOTP 校验:默认允许前后各 1 个时间步的时钟偏移 let ok = Totp.verify(secret, code) // Bool // 放宽/收紧偏移窗口 let ok2 = Totp.verify(secret, code, digits: 6, period: 30, window: 2) // 字节密钥版本 let ok3 = Totp.verifyBytes(raw, code) ``` ### 4. Base32 编解码 ```cangjie let raw = Base32.decode("N6C5HNT2F633") // 忽略空格/短横线/大小写 let encoded = Base32.encode(raw) // 往返后还原 "N6C5HNT2F633" ``` ## API 参考 包结构:全部类型位于根包 `simcu::otp`(单一命名空间,无子包) | 类型 | 说明 | |------|------| | `Base32` | RFC 4648 无填充 Base32 编解码;`encode(data)` / `decode(input)`,非法字符抛 `IllegalArgumentException` | | `HmacSha1` | RFC 2104 / 4226 HMAC-SHA1(blockSize=64);`init(key)` / `compute(data)` | | `Hotp` | RFC 4226;`generate(secret, counter[, digits])` | | `Totp` | RFC 6238;`generate/generateAt/generateBytes(..., digits, period)` 与 `verify/verifyBytes(..., digits, period, window)` 多种重载;默认 `period=30`、`window=1` | | `OtpUri` | `generateSecret([bytes])` 随机密钥;`build(issuer, account, secret[, digits, period])` 生成 otpauth:// 链接 | 常用常量: - `Totp.DEFAULT_PERIOD = 30` - `Totp.DEFAULT_T0 = 0` ## 说明 - **HMAC-SHA1 为何自实现**:仓颉标准库 `std.crypto.digest` 的 HMAC 目前仅支持 SHA512,而 HOTP/TOTP 标准基于 HMAC-SHA1,因此本库基于 `stdx.crypto.digest.SHA1` 实现了 RFC 2104 的 HMAC-SHA1。 - **兼容性**:`Totp.generate` / `Totp.verify` 的结果与 Google Authenticator 完全一致,可交叉验证。 - **服务端校验建议**:绑定阶段将 Base32 secret 持久化到账号表(`otp_secret` 字段);校验时用 `Totp.verify(secret, code)`。 ## 运行测试 ```bash cjpm test ``` 测试覆盖:Base32 编解码往返、HOTP/TOTP 与 RFC 官方测试向量的对拍、时钟偏移窗口、otpauth:// URI 生成等。 ## 配合 SimApi 使用 在 simapi 后端中作为账号二次验证通道: ```cangjie // AuthCodeService 示例:绑定 OTP 的账号优先接受 TOTP 动态码 let otpOk = Totp.verify(acc.otpSecret, code) ```