npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rand-vec-sig

v1.0.0

Published

Rand-Vec-Sig: self-developed signature + request payload encryption system with dual-mode traffic obfuscation (zero runtime dependencies).

Readme

Rand-Vec-Sig

自研密码学 + 请求报文加密 + 流量混淆的 npm 包。零运行时依赖,不依赖任何第三方密码学库。

实现了《Rand-Vec-Sig 全网混淆签名体系架构设计》的四层结构:

┌──────────────────────────────────────────────────────────────────────┐
│ 业务代码(零改造)                                                     │
│   axios.get('/api/x')      fetch('/api/y')                           │
└──────────────┬──────────────────────────────┬────────────────────────┘
               │ 工程拦截层 client            │
               │  白/黑名单 · 410 刷新重试 · 响应解密 · 会话续期          │
┌──────────────▼──────────────────────────────▼────────────────────────┐
│ 流量伪装层 traffic-obfuscation                                        │
│   种子 → 字段别名表 (_xxxx) · 值可逆掩码 (ChaCha20+IV) · 确定性诱饵     │
├──────────────────────────────────────────────────────────────────────┤
│ 密码学层 core                                                         │
│   2048bit MODP G14 DH → HKDF → encKey/macKey/sigKey/iv               │
│   RVF 自研 Feistel(16 轮, 动态 S 盒) · RVF-CTR + HMAC-SHA256 (AEAD)    │
│   签名 = RVF(sigKey, sha256(RVS1|v|mode|tag|sid|ts|nonce|cpk|alg|..)) │
└──────────────────────────────────────────────────────────────────────┘

1. 快速开始

npm install rand-vec-sig

1.1 生成服务端密钥对

服务端只有一个长期私钥 Rsk,客户端只持有公钥 Rpk。

npx rand-vec-sig-keygen --bits 512 --out ./secrets/rvs
# → ./secrets/rvs.rsk.env    RVS_RSK=...        仅服务端,切勿提交或下发
# → ./secrets/rvs.rpk.env    VITE_RVS_RPK=...   可安全打进前端包

Rsk 必须来自 KMS / Vault / 环境变量。库不会生成、记录或持久化它;createRandVecSigServer 缺少该值时直接抛 CONFIG_INVALID。

1.2 前端

import axios from 'axios';
import { createRandVecSigClient } from 'rand-vec-sig/client';

const rvs = createRandVecSigClient({
  mode: 'static',
  versionSeed: import.meta.env.VITE_RVS_VERSION_SEED, // 每次发版轮换
  serverPublicKey: import.meta.env.VITE_RVS_RPK,      // hex / base64url / BigInt
  skipUrls: [/third-party/, '/api/oss-upload'],        // 三方与直传必须放行
});

rvs.wrapAxios(axios);   // 或 rvs.installFetch() 全局接管 fetch

之后业务代码不需要任何改动:命中白名单的请求自动加密载荷、混淆字段名、附加签名。失败即抛错(默认 failOpen: false,不会静默降级成明文请求)。

1.3 服务端

import express from 'express';
import { createRandVecSigServer, createRedisReplayStore } from 'rand-vec-sig/server';

const rvs = createRandVecSigServer({
  mode: 'static',
  versionSeed: process.env.RVS_VERSION_SEED,   // 与前端一致
  serverPrivateKey: process.env.RVS_RSK,       // 仅来自环境变量
  replayStore: createRedisReplayStore(redis),  // 生产必须换成 Redis
  responseEncryption: true,
});

const app = express();
app.use(express.json());
app.use('/api', rvs.middleware({
  skipPaths: ['/api/health', '/api/oss-callback'],
}));

app.post('/api/order/create', (req, res) => {
  // req.body 已经是验签 + 解密后的业务 JSON
  // req.rvs  = { mode, payloadMode, versionTag, sessionId, nonceHex, durationMs, ... }
  res.json({ code: 0, data: { received: req.body } });
});

Web 标准运行时(Next.js Route Handler、Cloudflare Workers、Deno、Bun)用 rvs.handler(),由 respond 接管响应体,它拿到的 payload 已经验签解密:

// app/api/order/create/route.ts
import { rvs } from '@/lib/rvs';   // 进程内单例

export const POST = rvs.handler({
  skipPaths: ['/api/health'],
  onReject: (result, request) => metrics.increment(`rvs.reject.${result.code}`, { url: request.url }),
  respond: (context, payload) => ({
    code: 0,
    data: { received: payload, traceId: context.nonceHex.slice(0, 8) },
  }),
});

不传 respond 时 handler() 会把验签后的请求体原样回显,只适合自测端点。需要自己接框架就直接用 rvs.verify():它从不抛异常,拒绝也走 return { ok: false, code, status }。


2. 双模式

| | static | dynamic | |---|---|---| | 种子来源 | 前端构建期注入的 versionSeed | 服务端下发 sessSeed(TTL 900s) | | 别名表粒度 | 同一版本所有用户共享 | 每个会话独立 | | 握手开销 | 0 次 | 0 次(响应头捎带)或 1 次 | | 抗静态逆向 | 轮换 seed 即全体抓包脚本失效 | 单次会话样本无复用价值 | | 适用 | 主体业务、延迟敏感 | 高价值接口、强对抗场景 |

const rvs = createRandVecSigClient({
  mode: 'dynamic',
  versionSeed: import.meta.env.VITE_RVS_VERSION_SEED,
  serverPublicKey: import.meta.env.VITE_RVS_RPK,
  session: {
    provider: async () => (await api.get('/rvs/session')).data,  // SSR / 原生桥接
    // 或 handshake: { url: '/rvs/session' }
    // 或依赖 captureFromResponse(默认开启)从任意响应头自动捕获并续期
  },
});

服务端配 session: { ttlMs: 900_000, issue: 'header' },会话过 TTL 一半即自动续期,新种子通过 x-ky-seed / x-ky-ttl / x-ky-iat 响应头下发,客户端无感切换。

动态模式下 sessSeed 只存在于进程内存(架构红线:不落库)。多实例部署请开启 sticky routing,或依赖「一次 410 重试」自愈。


3. 子路径导出

| 入口 | 内容 | 用途 | |---|---|---| | rand-vec-sig | 全部四层 | 小体量接入 | | rand-vec-sig/core | SHA-256 / HKDF / ChaCha20 / DH / RVF / AEAD / 签名 | 自定义协议、CLI、一致性测试 | | rand-vec-sig/obfuscation | 别名表 / 掩码 / 诱饵 / 混淆引擎 | 已有签名方案,只想隐藏字段名 | | rand-vec-sig/client | Axios / Fetch 拦截、会话、白黑名单 | 前端 | | rand-vec-sig/server | 验签、防重放、中间件、会话存储 | 服务端 |

sideEffects: false,ESM / CJS 双产物,支持 tree-shaking。


4. 报文格式

一次 payloadMode: 'encrypt'(默认)请求的线格式:

{
  "_9k3fa0xz2": "A8f2...",   // 协议版本     (alias of `v`)
  "_p2m1qq87g": "Qz...",     // 时间戳       (alias of `t`)
  "_zz81n0ka4": "Lm...",     // 16B nonce    (alias of `n`)
  "_a0vv9s2k7": "U3bc...",   // 客户端公钥    (alias of `p`)
  "_q7ww2m11c": "5Hd...",    // 密文         (alias of `d`)
  "_k3ll90zm5": "0Pq...",    // MAC          (alias of `m`)
  "_w8ee33rts": "b9...",     // 算法标记      (alias of `a`)
  "_y1uu55nmb": "R2f...",    // 签名         (alias of `s`)
  "_c4zz77plo": "...",       // ↓ 确定性诱饵,服务端可复现并剥离
  "_d6xx22qwe": "...",
  "_e9vv11rty": "..."
}
  • 别名由种子派生(base62 片段 + 防冲突扩展),与业务字段冲突时自动让位。
  • 值用 ChaCha20 + 随机 12B IV 掩码后 base64url 输出,同一明文每次上线都不同:时间戳的十进制形状、nonce 的 hex 形状、协议版本的低熵都被抹掉。
  • 诱饵由 seed + nonce 确定性派生,客户端生成、服务端复现,sign 模式下也能精确剥离。
  • 头部只带标签,从不带种子:x-ky-v: <versionTag>、x-ky-s: <sessionId>。

payloadMode: 'sign' 时业务字段保持可读,仅附加签名与诱饵,服务端返回的 payload 仍是纯业务数据。


5. 配置参考

客户端 createRandVecSigClient

| 字段 | 默认 | 说明 | |---|---|---| | mode | — | 'static' / 'dynamic' | | versionSeed | — | 与服务端一致,且必须是高熵随机串(见 §7) | | serverPublicKey | — | Rpk,hex / base64url / Uint8Array / BigInt | | protocolVersion | 'rvs1' | 变更即为不兼容升级 | | identifyHeaders | x-ky-v/x-ky-s/x-ky-n | 可整体轮换以对抗指纹识别 | | includeUrls / skipUrls | [] | skip 优先;include 非空时只签命中项 | | signMethods | POST/PUT/PATCH/DELETE | 参与签名的方法 | | signContentTypes | application/json, +json | 参与签名的 Content-Type | | profile | { aliasPrefix:'_', aliasLength:9, decoyCount:3, maskValues:true } | 必须与服务端一致 | | payloadMode | 'encrypt' | 或 'sign'(只签不加密) | | deploymentId | — | 部署标识(如 prod-cn)。会混入 HKDF info、AAD 与签名串,用于封堵跨部署重放。前后端必须配同一个值。不配置时派生与签名串与旧版逐位一致(升级不会让既有部署失效,但也不受此保护)。长度 ≤ 64 | | privateKeyBits | 256 | DH 指数位数,红线最低 256 | | ephemeralKey | { strategy:'window', rotateMs:300000, maxRequests:200 } | 或 'request'(每请求新密钥对) | | session | — | provider / handshake / captureFromResponse / issueHeaders / refreshAheadRatio / maxRetries | | retry | { enabled:true, seedExpiredStatus:[410,419], maxRetries:1 } | maxRetries > 1 直接报错 | | responseEncryption | false | 需服务端同开 | | failOpen | false | 默认关闭:加密失败即抛错,不降级明文 | | hooks | — | onSign / onSkip / onError / onSessionChange / onRetry | | now | Date.now | 可注入时钟,便于测试 | | trace | — | 结构化诊断事件,不含密钥与载荷。种子过弱时收到 seed.weak |

服务端 createRandVecSigServer

| 字段 | 默认 | 说明 | |---|---|---| | mode | — | 与客户端一致 | | serverPrivateKey | — | Rsk,hex / base64url / BigInt;必填 | | versionSeed / versionSeeds | — | 静态模式必填;传数组可做无缝轮换窗口。同样是高熵随机串(见 §7) | | versionTagOf | 内置 | 自定义 versionTag 派生,须与客户端一致。内置派生是种子的公开可验证函数,故种子必须不可枚举;也可在此返回与种子无关的随机 tag | | profile | 同客户端默认 | 必须与客户端一致 | | allowedPayloadModes | ['encrypt','sign'] | 可只允许 encrypt | | timestampToleranceMs | 120000 | 时间窗,建议先做 NTP 对齐 | | nonceTtlMs | 300000 | 重放记录 TTL | | replayStore | 内存 LRU | 生产必须换 Redis。内置 MemoryReplayStore 满容量时默认拒绝新请求(fail-closed,抛 REPLAY_STORE_FULL → 503)而非静默淘汰——淘汰活 nonce 等于给重放开门。单进程部署若更看重可用性可显式传 { capacityPolicy: 'evict' },并用 onCapacityExceeded 接告警 | | deploymentId | — | 客户端配了,服务端必须配同一个值,否则全部验签失败 | | session | { ttlMs:900000, issue:'header' } | 动态模式必填 | | identifyHeaders | x-ky-v/x-ky-s/x-ky-n | 与客户端一致(含 nonce,回显头用它) | | strictSubgroupCheck | false | 开启后每请求多一次模幂(子群校验) | | responseEncryption | false | 响应体加密 | | logger | — | (event, detail) => void,永不接收密钥。种子过弱时收到 seed.weak |

中间件 / handler 选项

rvs.middleware() 与 rvs.handler() 共用以下选项:

| 字段 | 默认 | 说明 | |---|---|---| | skipPaths | [] | 完全跳过校验的路径(字符串前缀或正则):健康检查、三方回调、OSS 直传 | | contextProperty | 'rvs' | 验签结果挂到 req 的哪个属性上(日志安全的元信息,不含密钥) | | replaceBody | true | 用解密后的业务数据覆盖 req.body | | onReject | — | 每次拒绝回调,接监控。status === 403 才需要告警,410 是正常自愈信号 | | rejectBody | — | 自定义拒绝响应体(默认 { code, message }) | | respond | — | 仅 handler():接管响应体,(context, payload, request) => unknown |

middleware() 靠 next() 组合,不需要 respond;handler() 是终结式的,所以需要。


6. 错误码

| 状态 | 错误码 | 含义 | |---|---|---| | 410 | SEED_EXPIRED / SESSION_EXPIRED / SESSION_UNKNOWN | 种子过期、未知,或请求在途时种子刚好轮换 → 客户端刷新后重试一次 | | 403 | SIGNATURE_INVALID / MAC_INVALID / NONCE_REPLAYED / TIMESTAMP_OUT_OF_WINDOW / MISSING_FIELDS / KEY_INVALID | 密码学校验失败 | | 400 | PAYLOAD_MALFORMED | body 非法 JSON 对象 / 字段格式错误 | | 503 | REPLAY_STORE_FULL | 重放存储达容量上限。这是可用性信号而非安全问题:存储拒绝记录而不是淘汰活 nonce。运维动作:调大 maxEntries、缩短 nonceTtlMs、或换 Redis。响应文案保持模糊,详情只进日志与 onCapacityExceeded | | 500 | CONFIG_INVALID / INTERNAL_ERROR / CRYPTO_UNAVAILABLE | 配置或运行时故障(响应体不含内部错误详情,只进日志) |

客户端遇到 410 会刷新种子并只重试一次(单飞,避免惊群),这是协议自愈的唯一路径。

动态模式下的种子轮换不会打断在途请求:续期会给同一个 sessionId 换一把新种子,而别名表与掩码密钥都随种子变化。服务端会把上一代种子保留一个宽限窗口(SESSION_PREVIOUS_GRACE_MS,默认 300s)并依次尝试两代;两者都认不出时返回 410 SEED_EXPIRED 而不是 403,客户端刷新一次即可恢复。若实现了自定义 SessionStore,请一并实现可选的 getPrevious() —— 不实现也能跑,只是并发突发跨越续期点时退化成「410 重试一次」。


7. 部署红线

  1. 禁止降低 DH 位数:generateDhKeyPair 对 <256bit 的指数直接抛错。
  2. 禁止关闭流量混淆裸传:profile.maskValues 与字段别名始终生效,sign 模式也保留诱饵与签名。
  3. 禁止客户端自编 seed:动态模式下种子只能来自服务端下发的 SessionSeed。
  4. 禁止 seed / 会话密钥落盘或落库:MemorySessionStore 仅进程内存,TTL 到期即销毁;重放记录只写 nonce 的哈希。
  5. 禁止 Rsk 写死在代码或前端产物中:服务端启动时从环境变量 / KMS 读取。
  6. 同一份 versionSeed 不要跨部署复用。若确实要复用(测试 / 预发 / 生产),必须在客户端与服务端同时配置相同的 deploymentId,否则一个部署的请求可以在另一个部署上验签通过。未配置 deploymentId 时行为与旧版逐位一致 —— 兼容,但不受保护。
  7. 重放存储不要开 capacityPolicy: 'evict',除非明确接受「攻击者可以刷掉受害者的 nonce 从而重放被捕获请求」这一后果。默认的 fail-closed 会把容量问题暴露成 503 与 onCapacityExceeded 回调,这是应该看到的信号。生产环境请直接用 createRedisReplayStore。
  8. 禁止 retry.maxRetries > 1:过期种子必须刷新,而不是重试循环。
  9. 禁止关闭认证:failOpen 默认 false,这是刻意设计。

版本种子必须高熵

versionTag 是种子的确定性、公开可验证的函数:

versionTag = HMAC( sha256('rvs/v1/tag/version/rvs1'), versionSeed )[0..8]

HMAC 的密钥是写死在代码里的公开常量,所以这个式子等价于「种子的公开指纹」—— 抓到一次请求就能在本地对候选种子做字典爆破并逐字节比对,无需与服务器交互。实测以 web-prod-v2.0.0 为真实种子、6480 个候选串穷举,13.4ms 即还原出种子。

低熵种子不会让「加密」失守(报文仍由逐请求 DH 派生密钥保护,签名也伪造不了),但会让「伪装」这一整层归零:别名表、值掩码、诱饵全部可复现,payloadMode: 'sign' 下业务明文直接裸奔。

正确做法(推荐同时做):

  1. 种子用真随机值 openssl rand -hex 24,随发版轮换,轮换后旧抓包脚本即刻失效;
  2. 用动态模式,别名表按会话生成、TTL 900s,攻击者收集不到稳定样本;
  3. 若必须用可读种子,用 versionTagOf 返回一个与种子无关的随机不透明 tag,客户端构建期注入同样的 tag。

库在构造时会通过 logger / trace 发 seed.weak 事件提示(默认不打印到 console)。估算口径是 字符串长度 × log2(字符集大小),低于 100bit 即告警。它只能看形状,不能替你确认你是否真的掷了骰子,请当 linter 用。


8. 性能

参考机型 Intel i5-8257U(低功耗移动 CPU,数值偏保守),node scripts/bench.mjs,300 次迭代。下表是同机多次运行的实测区间(该机 p50 对负载敏感,单次数字不可当真):

| 操作 | p50 | p95 | |---|---|---| | client.sign 1kB(共享临时密钥) | 1.43 ~ 1.77 ms | 2.82 ~ 4.93 ms | | client.sign 1kB(每请求新密钥对) | 4.89 ~ 6.81 ms | 5.84 ~ 12.07 ms | | client.sign 16kB | 3.59 ~ 5.09 ms | 5.69 ~ 11.04 ms | | client.sign 1kB(payloadMode:'sign') | 0.74 ~ 1.20 ms | 1.58 ~ 5.37 ms | | server.verify 1kB(DH+KDF+签名+AEAD) | 3.12 ~ 4.87 ms | 4.54 ~ 25.55 ms | | 全链路 sign + verify 1kB | 4.55 ~ 7.29 ms | 6.18 ~ 15.52 ms | | RVF-CTR + HMAC 1kB(seal) | 0.35 ~ 0.45 ms | 0.66 ~ 3.48 ms |

在文档设定的 5~12ms 客户端开销预算内(中位数口径)。基准脚本以中位数为门禁,RVS_BENCH_STRICT=1 可同时约束 p95。

首包不是稳态:模块 import 约 5ms,第一次 client.sign 实测 49~75ms,其中约 45ms 是 V8 首次编译 BigInt 模幂与 RVF 位运算路径的 JIT 成本。它每个进程只发生一次,之后按 ephemeralKey 窗口复用共享秘密,不要算进单请求延迟预算。

报文越长,逐字节加密的成本越显眼:RVF-CTR 约 0.44 µs/字节。大 body 场景优先用 payloadMode:'sign'(只签不加密)。单请求明文上限是 MAX_PAYLOAD_BYTES = 4MiB,sign 与 encrypt 共用同一上限。

代码混淆的实测代价

混淆档位不是免费的,而这一层的代价在本库上特别极端 —— 它全是位运算热点循环(RVF 轮函数、S-box 生成、SHA-256、BigInt 模幂),正好是 controlFlowFlattening 与 deadCodeInjection 惩罚最重的代码形态。实测(同机型,client.sign 1kB,p50):

| 档位 | client.sign p50 | 基准总耗时 | dist 体积 | 结论 | |---|---|---|---|---| | 不混淆 | 4.36 ms | 7.1 s | 817 kB | 基线 | | light(默认) | 4.49 ms | 8.6 s | 611 kB | +3% 延迟,体积反而 -25% | | balanced | 493 ms | 94 s | 1991 kB | 慢 113 倍,体积 3.3 倍,不可用 | | hardened | 同 balanced | 同 balanced | 同 balanced | 不可用 |

light 已经去掉了可读的协议结构(无字面量字符串表、标识符全部改名),分析者无法再从产物里读出签名串布局,而这几乎不消耗运行时。

balanced / hardened 只在「接口非延迟敏感 + 流量低 + 威胁模型里确实有愿意手工逆向的人」时才值得开启,且开完必须重跑 npm run bench —— 它会直接撞穿 12ms 门禁,判 FAILED 是预期行为。这条红线针对代码混淆;流量伪装层(字段别名、值掩码、诱饵)是强制项,没有开关可以关掉。

密码学强度与风险边界见 SECURITY.md。


9. 验证与构建

npm run typecheck      # tsc --noEmit,0 error
npm test               # 单元 + 全链路测试
npm run test:vectors   # 固定字节向量校验(重构密码热路径的护栏)
npm run test:guards    # 变异测试:把每个修复还原成缺陷,断言指定测试变红
npm run build          # ESM + CJS + .d.ts
npm run rebuild        # clean + build,改了 entry 映射后才需要
npm run build:obf      # 追加代码混淆(默认 light,types 不动)
npm run test:dist      # 对构建产物做端到端冒烟(CJS + ESM 双通道)
npm run test:examples  # 跑 examples/ 里自带断言的示例
npm run bench          # 性能基准 + 预算门禁
npm run verify:all     # typecheck → test → test:vectors → test:guards → build → test:dist → test:examples
npm run verify:release # verify:all + bench

prepublishOnly = verify:all + 混淆 + test:dist,所以 npm publish 不可能发出未混淆或未验证的产物。

test:vectors 存了 21 项固定字节输出(PRNG 流、S 盒、分组加解密、签名变换、CTR keystream、modPow)。性质测试(往返、置换性、雪崩)在实现静默改变时依然会通过,只有固定向量能把「我做了等价重构」变成可证伪的陈述。仅在有意改变密码行为时才跑 test:vectors:update,并须在 CHANGELOG 说明原因。

test:guards 把每个修复还原成缺陷形态,断言指定的那条测试变红,再用 finally 还原源文件。锚点必须恰好匹配一次(重构后代码移位会报 STALE,而不是静默地什么都没测),并且必须由指定的那条测试失败(否则报 RED-BUT-WRONG-TEST)。

npm run build 不删除任何文件:它走 scripts/build.mjs 调 tsup 编程式 API 并传 config: false,跳过配置文件发现环节,从而不再生成(也就不再 unlink)tsup 自己的临时配置。这是刻意的 —— 在开启批量删除守卫的加固 shell / CI 沙箱里,unlink 会被拒绝并直接判定构建失败,而删除那个临时文件对构建结果毫无影响。代价是旧产物不会自动清理,改了 scripts/tsup.options.mjs 的 entry 映射后请跑 npm run rebuild。

scripts/ 与 tests/ 不随包发布,因此上面这些命令只在源码仓库内可用。使用者只需要 rand-vec-sig-keygen 这一个随包发布的 CLI。

可运行的示例在 examples/,见 examples/README.md。其中 01 / 03 / 04 自带安全断言(网线无明文、重放被拒、篡改被拒、410 恰好重试一次),失败即以非 0 退出码结束,可以直接当冒烟测试用。


10. 常见问题

为什么 sign 模式也需要服务端? 签名要覆盖「含诱饵的完整报文体」,而签名字段本身又在报文里。协议的处理方式是:客户端对不含签名字段的报文求 sha256 作为 binding;诱饵名从 seed + nonce 确定性派生,服务端解出 nonce 后复现同一列表并剥离,因此业务字段与噪音可以精确分离。

解密后的 body 字段顺序为什么变了? 载荷使用 stableStringify 规范化后加密(键排序),服务端解出的对象是规范序。语义完全等价,但不要对 JSON 字符串做逐字节比较。

三方接口 / OSS 直传被拦截了? 放进 skipUrls(字符串前缀、子串、正则或对象均可)。空正则 /(?:)/ 会被拒绝 —— 它会一次性禁用全部签名。

我的 Node 环境没有 WebCrypto? randomBytes 依次尝试 globalThis.crypto.getRandomValues → 注入源 → process.getBuiltinModule('node:crypto'),全部不可用时抛 CRYPTO_UNAVAILABLE。可用 setRandomSource(fn) 注入密码学安全的随机源(切勿传 Math.random())。

rvs.handler() 为什么把请求体回显回来了? 因为没传 respond。handler() 需要知道「验签通过之后回什么」,不传时它只能把验签后的请求体原样回显。传 respond: (context, payload) => ... 即可接管。

如何轮换? 版本种子:前端发版时改 versionSeed,服务端 versionSeeds: [newSeed, oldSeed] 保留旧值一个排空窗口,再移除 —— 那一刻所有旧抓包脚本同时失效。Rsk:需要重新生成并同步客户端 Rpk,属于客户端不兼容变更,按发布流程处理。


11. 许可

MIT。

部署前请阅读 SECURITY.md,特别是关于自研密码学风险的部分。