Deno ext/crypto 深度拆解:cppgc 接口对象 × 密钥句柄 的完整链路

📅 发布时间:2026/9/8 23:41:30
Deno ext/crypto 深度拆解:cppgc 接口对象 × 密钥句柄 的完整链路
Deno ext/crypto 深度拆解cppgc 接口对象 × 密钥句柄 的完整链路【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/denoWebCrypto 有十几个入口方法deno_crypto却只在扩展宏里注册了 2 个 op——这中间的落差就是理解这个 crate 的钥匙。翻开 00_crypto.js全部 JS 只有 334 行而digest、sign、importKey这些你天天调的方法方法体全部住在 V8 垃圾回收GC托管的 Rust 对象里密钥字节甚至不再属于 JavaScript 堆。读下去你会从任意一行crypto.subtle.xxx()调用独立定位到它的 Rust 实现位置并明白那些看起来多此一举的 JS 转发器为什么一个都删不掉。全景一次 API 调用要穿越哪几层先把地图铺开deno_core::extension!声明里objects列表注册了三个 cppgc 包裹类Rust 结构体实现了 V8 的 GC 追踪协议由 V8 分配、追踪引用、自动回收算法逻辑则拆在subtle_*.rs各模块里做纯 Rust 分派。ext/crypto/lib.rs 的扩展声明是整个 crate 的骨架deno_core::extension!(deno_crypto, deps [ deno_webidl, deno_web ], ops [ crypto::op_crypto_random_uuid_batch, op_crypto_is_seeded, ], objects [ crypto::Crypto, subtle_crypto::SubtleCrypto, crypto_key::CryptoKey, ], lazy_loaded_js [ 00_crypto.js ], options { maybe_seed: Optionu64 }, state |state, options| { if let Some(seed) options.maybe_seed { state.put(StdRng::seed_from_u64(seed)); } }, );一次调用的完整流向环节位置发生了什么JS 入口ext/crypto/00_crypto.jsasync 转发器错误变 rejection、Function.length对齐 WebIDL接收层ext/crypto/subtle_crypto.rs#[op2]cppgc 方法converter 同步解析参数spawn_blocking切工作线程分派层subtle_*.rs各模块的run()算法语义校验key type、usage、曲线再进同步内核执行层ext/crypto/lib.rs / ext/crypto/ed25519.rs 等sign_key_sync等纯 Rust 分派调 aws-lc-rs / rsa / tiny-keccak错误回传CryptoError枚举lib.rs#[class(...)]精确绑定 DOMException 类名与消息地图有了接下来看支撑它的四个决策——每个都是规范文本和运行时性能之间讨价还价的产物。决策一为什么用 V8 GC 的 Rust 对象而不是一个 op 一个函数问题出在旧模型上。op 模式下每个 WebCrypto 入口都是一个独立 opJS 函数 → op 参数序列化 → Rust 入口 → 再序列化回来密钥字节每次操作都要完整穿越 V8 边界。17 个入口 × 高频调用这个税太重。方案是把三个接口变成 cppgc 对象方法体直接住在 Rust 的 impl 块里#[op2] impl Crypto { #[required(1)] fn get_random_valuess( self, state: mut OpState, scope: mut v8::PinScopes, _, typed_array: v8::Locals, v8::Value, ) - Resultv8::Locals, v8::Value, CryptoError { // ... } }片段取自 ext/crypto/crypto.rsop2 宏在 impl 块展开方法体同时拿到self、OpState和 V8 句柄converter 在调用点同步解析参数subtle_sign.rs 这类模块只提供纯 Rust 的run()由方法体直接调用——不经过 op 序列化管道。代价呢JS 侧被压缩到 00_crypto.js 的 334 行簿记铸造单例、Deno.privateCustomInspect装饰、Function.length修正、structured-clone 复活回调。为什么单例必须铸造而不是构建快照时建好因为 cppgc 堆在快照构建期还没附着到 V8 isolateCrypto.create(...)只能延迟到运行时第一次读globalThis.crypto时执行。换来的好处同样实在新增算法只需新模块加一个方法分支extension!的ops列表永远不会再膨胀。决策二密钥为什么不住在 JavaScript 堆里你可能会问CryptoKey是个 JS 对象密钥材料放WeakMap里不是最自然吗key_store.rs的文档注释记录了这段演化/// Historically the key material for every CryptoKey lived in a JavaScript /// WeakMap (KEY_STORE in 00_crypto.js) and was serialized and passed to /// every crypto op. Instead, the key material now lives in Rust inside this /// cppgc object, which JavaScript stores as the CryptoKeys handle and passes /// to ops - so the serialized key no longer has to cross the JS/Rust boundary on /// every operation. /// /// Because the handle is a cppgc object, the key material is freed /// automatically by V8s garbage collector once the handle is collected (i.e. /// once no CryptoKey references it). No FinalizationRegistry or manual /// bookkeeping is required. pub struct CryptoKeyHandle { data: RawKeyData, }片段取自 ext/crypto/key_store.rs把CryptoKeyHandle想象成一把带自动续期的仓库钥匙钥匙挂在仓库里仓库随主人CryptoKey实例一起被 GC 回收时锁芯里的材料自动销毁——不需要FinalizationRegistry也不需要手动记账。lib.rs 里KeyData的注释同样直白Previously the key bytes were serialized and passed from JavaScript on every operation.字节本身的形态由 shared.rs 的RawKeyData枚举表达Secret/Private/Public带用途标签HMAC 密钥、PKCS8 私钥、SPKI 公钥Raw是 Ed25519/ML-KEM 公钥这类无标签裸字节SeededPrivate { seed, private_key }则给后量子算法存展开字节 短种子的复合结构——从展开私钥导入时seed为Nonenode_interop.rs 会因此拒绝导出raw-seed/jwk/pkcs8格式。决策三一个 Option 种子如何改变 randomUUID 的行为seed 从扩展options进来有值就向OpState存入确定性StdRngstate.put(StdRng::seed_from_u64(seed))没值则一切随机路径退回操作系统熵源。所有取随机的地方都是同一个模式let maybe_seeded_rng state.try_borrow_mut::StdRng(); if let Some(seeded_rng) maybe_seeded_rng { seeded_rng.fill(bytes); } else { let mut rng thread_rng(); rng.fill(bytes); }片段取自 ext/crypto/crypto.rs 的get_random_values上方还有 65536 字节的配额检查超限抛QuotaExceededError。但 seed 真正有意思的落点在 JS 侧铸造单例时usesSeededRng op_crypto_is_seeded()记下标志randomUUID()就此分叉成两条路function randomUUID() { if (this ! cryptoSingleton || usesSeededRng) { return FunctionPrototypeCall(cppgcRandomUUID, this); } if (uuidBatch UUID_BATCH_SIZE) { uuidBatchData op_crypto_random_uuid_batch(); // 一次取回 128 条 uuidBatch 0; } const start uuidBatch * UUID_STRING_BYTES; return StringPrototypeSlice(uuidBatchData, start, start UUID_STRING_BYTES); }片段取自 ext/crypto/00_crypto.jsUUID_STRING_BYTES 36是 UUIDv4 文本表示长度UUID_BATCH_SIZE 128与 crypto.rs 中的 Rust 常量一致批量路径背后是fast_uuid_v4_bytes16 字节熵就地改版本位与变体位查HEX_CHARS表直接拼 36 字节串一次 op 往返摊薄给 128 次调用。而带种子的运行时刻意放弃快路径——测试和快照场景要求精确复现 RNG 调用顺序确定性比快更重要。这也是那两个残留 op存在的原因op_crypto_random_uuid_batch服务快路径op_crypto_is_seeded向 JS 暴露 seed 状态。执行路径一次 subtle.sign() 在 Rust 里走了多远带着上面的地图走一遍真实链路看每层做了什么变换。JS: crypto.subtle.sign(ECDSA, key, data) → async 转发器00_crypto.jslength 修正为 3 → SubtleCrypto::sign #[op2] 方法体subtle_crypto.rs → converter 同步解析主线程V8 句柄内 → spawn_blocking 工作线程 → subtle_sign::run算法语义校验 分派 → sign_key_synclib.rs纯 Rust 算法执行参数解析SubtleSignParams的WebIdlConverter把ECDSA或{ name, hash }归一化为枚举变体见 subtle_sign.rsSubtleKeyconverter 从CryptoKey快照元数据算法名、usages、密钥类型并取出指向CryptoKeyHandle的句柄——注意密钥字节此刻没有过界过界的只是个指针。分派run()先做语义校验——参数算法名必须与 key 的algorithm.name匹配、usages必须含sign违者InvalidAccessError/OperationError。SubtleSignParams::Ecdsa { hash } { let hash crate::subtle_generate_key::sha_from_name(hash).ok_or_else(|| { not_supported(format!(Unrecognized hash algorithm: {hash})) })?; if key.key_type ! CryptoKeyType::Private { return Err(invalid_access(Key type not supported.to_string())); } // ...从 PKCS#8 解码私钥、prehash、sign_prehash }算法执行sign_key_sync按算法族分派。RSA 从 DER 解私钥ECDSA 走sign_prehash出裸r||s——P-521 分支对短于 33 字节的哈希左补零满足bits2field的最小长度要求33 是 66 字节字段模长的一半HMAC 的 SHA-3 变体走tiny-keccak其余走 aws-lc-rs。结果物化签名字节回到主线程变成ArrayBuffer经 Promise 链交付。converter 为什么必须同步跑在主线程因为它要摸 V8 堆读字典成员而重活放后台线程一次 RSA 签名几十毫秒也不会卡住事件循环。边界与兼容错误类名和 Promise 形态才是生死线 链路通了剩下的坑全在错误长什么样上。第一个大坑converter 层抛不出正确的 DOMException。WebIdlError的类名被硬编码为#[class(type)]converter 一抛错就是 TypeError——但规范在多处场景要求NotSupportedError。所以 digest.rs 里未知算法名不报错而是保留原始拼写let Some(canonical) canonical_digest_name(name_str) else { return Ok(Self::Unknown(name_str)); // 推迟到 run() 抛 NotSupportedError };XOF 参数校验同理推迟——outputLength必须是 8 的倍数、TurboSHAKE 非零、domainSeparation落在[0x01, 0x7F]这些违规最终要落成DOMExceptionOperationError而不是WebIdlError。read_optional_u8还有个防回绕细节先按 u32 读完整值再拒绝 0xFF的输入防止0x101截断成0x01骗过范围检查。这类延迟到 run 阶段才报错的模式在 subtle_sign.rs 的 ECDSA hash 名上重演converter 只取 raw 字符串sha_from_name解析失败才抛NotSupportedError因为 WPT 的 bad hash name 用例硬编码断言了错误名。第二个坑是同步 throw 必须变成 rejected promise。规范保证每个SubtleCrypto方法都返回 Promise但 op2 dispatcher 在异步体启动前就同步调 converter——TypeError: Missing modulusLength这类 converter 层错误会以同步异常形式抵达调用点。WPT 的promise_rejects_dom用fn.call(undefined)包装调用同步 throw 会浮出TypeError: Failed to execute call on SubtleCrypto这种错误形态。于是 JS 层用转发器把 15 个方法全部 async 化顺带对齐 WebIDL 参数个数ObjectDefineProperty(wrapper, length, { __proto__: null, value: arity, // 来自 WebCrypto IDL 的必需参数个数idlharness 会断言 configurable: true, });片段取自 ext/crypto/00_crypto.jsderiveBits还有特殊约束op2 无法在保留最少参数检查的同时声明可选第三参#[required(2)]会经async_op_2管道悄悄丢弃用户第三参所以它由一个三参转发器兜住保证Function.length 2。read_required_u32则实现了[EnforceRange]语义——拒绝 NaN/负数/超 u32 值而不是 ECMAScript 的 ToUint32 静默截断。交叉查阅须知README 与代码的三处偏差上面那些细节README 其实都盖不上。README 的 Surface 一节点名There are no standalone ops但当前extension!宏注册了 2 个——这不是文档笔误而是批量 UUID 快路径与 seed 探测的遗留设计引用时别当全部下沉的铁证。README 里 Rust 侧的init(Optionu64)签名也已演进worker 主路径用args(options.seed)runtime/worker.rsWeb Worker 用init(options.seed)runtime/web_worker.rs快照构建用lazy_init()runtime/snapshot.rsseed 语义不变。README 那段Object.defineProperty(globalThis, ...)示例描述的是嵌入方视角Deno 本体的全局绑定在 runtime/js/98_global_scope_shared.js 完成扩展脚本额外导出了两个 Node.jsKeyObject互用函数。交叉查阅时以extension!宏与 runtime 实际调用点为准README 当作设计意图声明看。收束与导航WebCrypto 的合规成本不在算法而在错误类名、参数校验形态和 Promise 语义这些边角——cppgc 对象把 17 个入口从 ops 列表里清了出去Unknown(name)的延迟报错和 async 壳则守在 JS 与 Rust 的交界处。推荐阅读顺序ext/crypto/lib.rs扩展声明与CryptoError映射→ ext/crypto/00_crypto.js334 行簿记看 JS 还剩什么→ ext/crypto/subtle_sign.rsconverter 与 run 两阶段的完整样板→ ext/crypto/key_store.rs密钥句柄→ tests/unit/webcrypto_test.ts行为边界验证。【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考