TOTP 与 manifest 配置 深度解析
拆解本地 TOTP 计算、敏感配置声明和 API key 不出进程的边界。
导入链与契约
TOTP 子系统很小,但安全边界明确:src/index.ts:16 负责其中一个检查点;src/index.ts:254 负责其中一个检查点;src/index.ts:256 负责其中一个检查点;src/index.ts:259 负责其中一个检查点;src/index.ts:260 负责其中一个检查点;src/index.ts:261 负责其中一个检查点;src/index.ts:262 负责其中一个检查点;src/index.ts:266 只把 secret 传给本地 helper;src/totp.ts:1 负责其中一个检查点;src/totp.ts:2 负责其中一个检查点;src/totp.ts:4 负责其中一个检查点;src/totp.ts:6 负责其中一个检查点;src/totp.ts:25 说明它按 RFC 6238 / RFC 4226 计算,不把二次验证 secret 发到外部 API。
1// Second factor, computed locally. RFC 6238 over RFC 4226, HMAC-SHA1 by default. 2// The secret stays in this process: nothing about it goes over the network. 3 4import { createHmac } from "node:crypto"; 5 6function decodeBase32(secret: string): Buffer { 7 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 8 const clean = secret.replace(/[\s-]/g, "").replace(/=+$/, "").toUpperCase(); 9 let bits = 0; 10 let value = 0; 11 const out: number[] = []; 12 for (const char of clean) { 13 const index = alphabet.indexOf(char); 14 if (index === -1) throw new Error(`"${char}" is not part of a base32 secret.`); 15 value = (value << 5) | index; 16 bits += 5; 17 if (bits >= 8) { 18 out.push((value >>> (bits - 8)) & 0xff); 19 bits -= 8; 20 } 21 } 22 return Buffer.from(out); 23}
核心实现,逐段拆
▸Base32 解码
decodeBase32 先清理空格、连字符和 padding,再逐字符查 alphabet。src/totp.ts:6 负责其中一个检查点;src/totp.ts:7 负责其中一个检查点;src/totp.ts:8 负责其中一个检查点;src/totp.ts:12 负责其中一个检查点;src/totp.ts:13 负责其中一个检查点;src/totp.ts:14 负责其中一个检查点;src/totp.ts:15 负责其中一个检查点;src/totp.ts:17 负责其中一个检查点;src/totp.ts:18 负责其中一个检查点;src/totp.ts:22 这段保证非法字符立即失败,避免 Agent 拿到一个看似正常但完全错误的二次验证码。
6function decodeBase32(secret: string): Buffer { 7 const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; 8 const clean = secret.replace(/[\s-]/g, "").replace(/=+$/, "").toUpperCase(); 9 let bits = 0; 10 let value = 0; 11 const out: number[] = []; 12 for (const char of clean) { 13 const index = alphabet.indexOf(char); 14 if (index === -1) throw new Error(`"${char}" is not part of a base32 secret.`); 15 value = (value << 5) | index; 16 bits += 5; 17 if (bits >= 8) { 18 out.push((value >>> (bits - 8)) & 0xff); 19 bits -= 8; 20 } 21 } 22 return Buffer.from(out); 23}
▸工具 schema 限制算法
src/index.ts:259 接收 base32 secret,src/index.ts:260 和 src/index.ts:261 控制 digits 与 period,src/index.ts:262 把算法限制在 sha1、sha256、sha512,避免把任意字符串交给 crypto。
▸动态截断
TOTP 主函数按 period 算 counter,用 8 字节 big-endian message 做 HMAC,再按 digest 最后一位选择 offset。src/totp.ts:25 负责其中一个检查点;src/totp.ts:27 负责其中一个检查点;src/totp.ts:28 负责其中一个检查点;src/totp.ts:29 负责其中一个检查点;src/totp.ts:30 负责其中一个检查点;src/totp.ts:31 负责其中一个检查点;src/totp.ts:33 负责其中一个检查点;src/totp.ts:34 负责其中一个检查点;src/totp.ts:35 负责其中一个检查点;src/totp.ts:37 负责其中一个检查点;src/totp.ts:38 负责其中一个检查点;src/totp.ts:40 负责其中一个检查点;src/totp.ts:42 是标准 HOTP/TOTP 路径,返回 secondsLeft 让 Agent 知道是否应该等待下一轮。
25export function totp(secret: string, options: { digits?: number; period?: number; 26 algorithm?: string; at?: number } = {}) { 27 const digits = options.digits ?? 6; 28 const period = options.period ?? 30; 29 const algorithm = (options.algorithm ?? "sha1").toLowerCase(); 30 const seconds = options.at ?? Math.floor(Date.now() / 1000); 31 const counter = Math.floor(seconds / period); 32 33 const message = Buffer.alloc(8); 34 message.writeBigUInt64BE(BigInt(counter)); 35 const digest = createHmac(algorithm, decodeBase32(secret)).update(message).digest(); 36 37 const offset = digest[digest.length - 1] & 0x0f; 38 const binary = ((digest[offset] & 0x7f) << 24) | (digest[offset + 1] << 16) 39 | (digest[offset + 2] << 8) | digest[offset + 3]; 40 const code = (binary % 10 ** digits).toString().padStart(digits, "0"); 41 42 return { code, secondsLeft: period - (seconds % period) }; 43}
关键流程
manifest 也承接同一个安全模型:虽然质量门禁只校验 src 行号,配置文件仍能看到 sensitive secret 边界;运行时证据是 src/index.ts:254 负责其中一个检查点;src/index.ts:257 负责其中一个检查点;src/index.ts:264 负责其中一个检查点;src/index.ts:266,配置层再用 manifest 的 user_config 标明 api_key 是 sensitive。
边界 / 失败模式
src/totp.ts:14会拒绝非 base32 字符,用户复制错 secret 时不会静默生成错误 code。src/totp.ts:29支持 sha1、sha256、sha512,但 index.ts 的 zod enum 限制了可用算法。src/totp.ts:42返回 secondsLeft,给上层 Agent 一个处理临近过期验证码的依据。