mcpserver
§ · Nested detail page

TOTP 与 manifest 配置 深度解析

拆解本地 TOTP 计算、敏感配置声明和 API key 不出进程的边界。

§ 00 · IMPORT CHAIN

导入链与契约

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。

src/totp.ts · L1-23TypeScript
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}
§ 01 · CORE IMPL

核心实现,逐段拆

▸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 拿到一个看似正常但完全错误的二次验证码。

src/totp.ts · L6-23TypeScript
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 知道是否应该等待下一轮。

src/totp.ts · L25-43TypeScript
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}
§ 02 · KEY FLOW

关键流程

totp_code(secret,digits,period,algorithm) → decodeBase32 → HMAC(counter) → dynamic truncation → code + secondsLeft

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。

§ 03 · EDGE CASES

边界 / 失败模式

  • src/totp.ts:14 会拒绝非 base32 字符,用户复制错 secret 时不会静默生成错误 code。
  • src/totp.ts:29 支持 sha1、sha256、sha512,但 index.ts 的 zod enum 限制了可用算法。
  • src/totp.ts:42 返回 secondsLeft,给上层 Agent 一个处理临近过期验证码的依据。
Sub-page · 2026-08-12Source · sv-number/mcp-server↑ mcp-server 主页面