news 2026/9/8 23:01:23

ext/crypto Deno 源码导读:cppgc 包裹类、GC 驻留密钥与可播种随机数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ext/crypto Deno 源码导读:cppgc 包裹类、GC 驻留密钥与可播种随机数

ext/crypto Deno 源码导读:cppgc 包裹类、GC 驻留密钥与可播种随机数

【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno

deno_crypto(目录 ext/crypto)是 Deno 中实现 W3C Web Cryptography API 工作草案的扩展 crate,把Crypto/SubtleCrypto/CryptoKey三个接口从历史上"每个操作一个 op + 一个 JS 函数"的形态,重构成了 cppgc 包裹 Rust 类上的原生方法。算法能力面由 Cargo.toml 的依赖声明直接给出:

类别crate
哈希sha1sha2sha3tiny-keccakk12/kmacfeature)
非对称rsap256ecdh+ecdsa)、p384p521ecdsasignaturespkiconst-oid
对称aesaes-gcmaes-kwcbcctrocb3hmac
密钥派生aws-lc-rs(HKDF/PBKDF2)、argon2
后量子fips203(ML-KEM-512/768/1024)、fips205(ML-DSA)
原子算法curve25519-dalekx25519-dalek(另有slhdsa.rs内置实现)

读完本文,你能定位任意一次crypto.subtle.*调用从 JS 到 Rust 的分派路径,并说清楚"密钥为什么不在 JS 里、seed 为什么只影响两条 UUID/随机路径"这三个问题。

① 接线与生命周期:扩展宏的注册面与种子注入

扩展声明在 lib.rs 顶部,是整个 crate 的目录索引,逐面标注如下:

// 来源:ext/crypto/lib.rs 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: Option<u64> }, state = |state, options| { if let Some(seed) = options.maybe_seed { state.put(StdRng::seed_from_u64(seed)); } }, );
  • ops 面只剩 2 个op_crypto_is_seeded只是state.try_borrow::<StdRng>().is_some()的一行判断;op_crypto_random_uuid_batch为 JS 侧批量 UUID 快路径补货(见 H2 ②)。历史上几十个op_crypto_*已随算法体下沉到 cppgc 方法而退役;
  • objects 面是本次重构的主体:三个 WebIDL 接口的类身份(GarbageCollected实现、构造器、方法)全部落在 crypto.rs、subtle_crypto.rs、crypto_key.rs;
  • options/state 面实现了可播种随机数:maybe_seed: Option<u64>若为Some,闭包把StdRng::seed_from_u64(seed)存入OpState,此后getRandomValues/randomUUID走确定性伪随机流;未播种时走thread_rng()(OS 熵源)。这直接服务于快照与集成测试的可复现性;
  • deps 面deno_webidl提供 WebIDL converter 与 brand 检查工具,deno_web提供createFilteredInspectProxy(console inspect 形状修正用)。

运行时接线在runtime/下三处:worker.rs 以deno_crypto::deno_crypto::args(options.seed)透传WorkerOptions.seed,web_worker.rs 用init(options.seed),snapshot.rs 与 snapshot_info.rs 分别用lazy_init()/init(None)(快照期无 seed)。

种子在扩展声明里只是"存进 OpState",真正消费它的是 JS 侧的分派逻辑——下面沿一条调用链看完整闭环。

② 调用路径:从sign()到 Rust 分派内核

crypto.subtle.sign(algorithm, key, data)为样本,链路依次经过:

  1. 00_crypto.js:makeAsyncForwarder("sign", "sign", 3)包出的 async 包装器,把同步异常收敛为 Promise rejection;
  2. subtle_crypto.rs:cppgc 方法sign,op2 生成的 dispatcher 同步调用WebIdlConverter归一参数,再把方法体丢进spawn_blocking
  3. subtle_sign.rs:SubtleSignParams的 converter 解析算法字典,run()校验"密钥算法名 == 参数算法名、usagessign、密钥类型匹配";
  4. lib.rs:sign_key_sync(key, args, data)Algorithm变体做纯 Rust 分派(注释原文:"Called fromcrate::subtle_sign::runinsidespawn_blocking")。

JS 层因此被刻意做薄到只剩"簿记",文件头注释逐条列明:

  1. privateCustomInspect装饰:给三个原型挂Deno.privateCustomInspect符号,让Deno.inspect(cryptoKey)只显示type/extractable/algorithm/usages四个 WebIDL 形状属性;
  2. 惰性铸造单例:cppgc 堆在快照构建期未附着到 V8 isolate,故Crypto.create(getSubtleSingleton())SubtleCrypto.create()必须延迟到运行时首次读取globalThis.crypto时执行,同时打上webidl.brand
  3. structured-clone 复活回调core.registerCloneableResource("CryptoKey", (data) => CryptoKey.fromCloneData(data)),使CryptoKey可跨 Worker 克隆;
  4. Function.length与 async 化修正deriveBits用三参转发器保证length === 2;其余 15 个SubtleCrypto方法经makeAsyncForwarder(name, method, arity)包装,arity逐一照抄 WebIDL 必需参数数(verify4、deriveKey/importKey5、unwrapKey7、decapsulateKey6 等)。注释解释动机:converter 在 async 体执行前同步抛错,WPT 的promise_rejects_dom会以fn.call(undefined)触发TypeError: Failed to execute 'call',与规范要求的 rejected promise 形状不符,必须全部过一层async

分派按"每个操作族一个文件"拆分,与lib.rsmod列表一一对应:

操作族参数归一 + 校验 + 调度算法内核落点
digestdigest.rsaws_lc_rs::digestsha3、内置 KT256 sponge
sign / verifysubtle_sign.rs / subtle_verify.rslib.rssign_key_sync/verify_key_sync
encrypt / decryptsubtle_encrypt.rs / subtle_decrypt.rsencrypt.rs / decrypt.rs
deriveBits / deriveKeysubtle_derive_bits.rs / subtle_derive_key.rslib.rsderive_bits_sync
import / export / generate / wrap / getPublicKey同名subtle_*.rsimport_key.rs、export_key.rs、generate_key.rs 等
封装/解封装subtle_encapsulate.rs、subtle_encapsulate_key.rsmlkem.rs(ML-KEM)、slhdsa.rs
原子算法由上表文件直接调用ed25519.rs、x25519.rs、x448.rs、mldsa.rs

sign_key_sync内的曲线细节值得留意:ECDSA P-521 分支会把短于 33 字节的哈希左补零,注释为 "P-521 field size is 66 bytes; bits2field requires at least half that (33 bytes)";verify_key_sync则允许KeyType::Private的密钥参与验证(先从 PKCS#8 推导出VerifyingKey),且验证失败统一返回false而非抛错。randomUUID的双路径也在这层:

// 来源:ext/crypto/00_crypto.js function randomUUID() { if (this !== cryptoSingleton || usesSeededRng) { return FunctionPrototypeCall(cppgcRandomUUID, this); } if (uuidBatch === UUID_BATCH_SIZE) { uuidBatchData = op_crypto_random_uuid_batch(); uuidBatch = 0; } // ...按 UUID_STRING_BYTES=36 切片返回下一条 }

普通路径批量取回 128 条完整 UUID 字符串(Rust 侧 crypto.rs 的op_crypto_random_uuid_batch用查表HEX_CHARS拼 36 字节,避免格式化开销),后续调用纯在 JS 内切片;播种运行时则每条都走原生方法,以保留与 OS 熵源不同的精确 RNG 调用顺序。usesSeededRnggetCryptoSingleton()铸造单例时调用op_crypto_is_seeded()记下——这就是 seed 从 RustOpState反哺 JS 分派的闭环。

分派内核消费的第一样东西是密钥,而密钥如今不再住在 JS 里。

③ 状态与数据的驻留位置:密钥字节为什么搬进 GC 对象

四类关键状态的落点:

状态驻留位置生命周期由谁管
播种 RNG(StdRngOpStatestate闭包注入)runtime 存活期
UUID 批量缓存、单例、seed 标志00_crypto.js 模块作用域isolate 存活期
SubtleCrypto实例引用Cryptocppgc 对象的v8::Global字段V8 堆
密钥字节CryptoKeyHandle(cppgc 对象)V8 GC 回收 handle 时释放

krypto 侧注释 把演进动机写得很直白:

Historically the key material for everyCryptoKeylived in a JavaScriptWeakMap(KEY_STOREin00_crypto.js) and was serialized and passed to every crypto op. Instead, the key material now lives in Rust inside this cppgc object... NoFinalizationRegistryor manual bookkeeping is required.

(转述:历史上密钥在 JSWeakMap里,每次操作都要序列化跨边界;现在密钥字节活在 Rust 侧的 cppgc 对象中,V8 一旦回收 handle 就自动释放,省掉FinalizationRegistry手工簿记。)lib.rs 的KeyData注释同义重申:"Previously the key bytes were serialized and passed from JavaScript on every operation." 动机是纯粹的边界成本:RSA/EC 密钥动辄数百字节,旧路径每次sign都要走一遍 JS→Rust 的字节序列化,新路径只传 handle,Rust 内部直接(&key.raw).into()拿到Box<[u8]>

密钥素材的形状由 shared.rs 的枚举表达:

// 来源:ext/crypto/shared.rs pub enum RawKeyData { Secret(Box<[u8]>), Private(Box<[u8]>), Public(Box<[u8]>), Raw(Box<[u8]>), SeededPrivate { seed: Option<Box<[u8]>>, private_key: Box<[u8]> }, }

前四个变体按secret/private/public用途标签区分(HMAC 密钥、PKCS#8 私钥、SPKI 公钥等),Raw存放不带标签的原样字节(Ed25519/ML-KEM 公钥),SeededPrivate是 FIPS 203/204 复合素材:private_key为展开后的密钥字节,seed为派生用短种子;注释指出seedNone时(从展开私钥导入),导出raw-seed/jwk/pkcs8格式会被正确拒绝。格式与类型标签同样定义在 lib.rs:KeyFormat { Raw, Pkcs8, Spki }KeyType { Secret, Private, Public }

④ 规范对齐与错误模型:错误在哪个阶段抛

WebCrypto 要求错误以特定 DOMException 名称出现。lib.rs 的CryptoError枚举用#[class(...)]把每个变体精确绑到 JS 异常类:

变体JS 异常类消息
MissingArgumentHashTypeErrorMissing argument hash
MissingArgumentSaltLengthTypeErrorMissing argument saltLength
HKDFLengthTooLargeDOMExceptionOperationErrorThe length provided for HKDF is too large
DecryptionErrorDOMExceptionOperationErrordecryption error - integrity check failed(AEAD 认证标签失败)
InvalidXofParametersDOMExceptionOperationErrorInvalid XOF parameters
ArrayBufferViewLengthExceeded(usize)DOMExceptionQuotaExceededError上限 65536 字节熵
TypedArrayNotIntegerDOMExceptionTypeMismatchErrorThe provided value is not an integer-type TypedArray
UnsupportedDigestAlgorithm(String)DOMExceptionNotSupportedErrorAlgorithm '{0}' is not supported
IllegalConstructor(shared.rsSharedErrorTypeErrorIllegal constructor,附code: ERR_ILLEGAL_CONSTRUCTOR

错误推迟到哪个阶段抛是被 WPT 逐条钉死的设计。digest.rs 的DigestAlgorithm注释:

Unrecognized algorithm names are kept asDigestAlgorithm::Unknownso the dispatch inrun()can throw the WebCrypto-spec-mandatedNotSupportedErrorDOMException(not the WebIDLTypeErrorthat a converter-level error would produce).

(转述:未知算法名不在 converter 层报TypeError,而是保留为Unknown变体,推迟到run()抛出规范要求的NotSupportedError;WPTdigest.https.any.html的子测试硬编码了该错误名。)同样的推迟模式出现在 subtle_sign.rs 的read_required_hash:只把 hash 字典成员强转为原始.name字符串而不校验合法性,由run()sha_from_name失败后抛NotSupportedError,对应 "ECDSA bad hash name" 的 WPT 断言err.name === "NotSupportedError"

边界防护从 JS 迁移到 Rust 时逐条保留。read_optional_u8的注释给出了范围回绕防护的原文:

Read the full u32 and reject values outside[0, 0xFF]so a stray0x101does not wrap to0x01and slip past the caller's [1, 0x7F] domain-separation range check (TurboSHAKE edge).

(转述:先按 u32 读完整值再拒绝>0xFF,防止0x101回绕成0x01绕过 TurboSHAKEdomainSeparation[0x01, 0x7F]范围检查。)run_xof进一步校验outputLength为 8 的倍数、TurboSHAKE/KangarooTwelve 的outputLength非零,违规统一报InvalidXofParametersBufferSourceconverter 则把字节物化为Vec<u8>(保证跨.await安全)并显式拒绝SharedArrayBuffer及其视图,与 WebIDLBufferSource[AllowShared]的契约一致。

行为边界由三层测试钉死:tests/unit/webcrypto_test.ts 与 tests/unit/webcrypto_mldsa_test.ts 覆盖算法面,tests/wpt/ 下的 WPT 套件覆盖规范形状(错误名、Function.length、SAB 拒绝),Rust 侧 lib.rs 内还有与uuidcrate 对拍的test_fast_uuid_v4_correctness

⑤ 文档与源码的已知偏差 ⚠️

README.md 是较早时期的文档,引用时注意三处演进:

  1. init(Option<u64>)已演进为args(seed)。README 写的是"提供deno_crypto::deno_crypto::init(Option<u64>)";当前宏声明的字段是options.maybe_seed,运行时侧分别以args(options.seed)(主 worker)、init(options.seed)(Web Worker)、lazy_init()(快照)接线。种子的语义未变,变的只是扩展宏生成的构造函数名;
  2. "无独立 ops" 已基本成立但留有两个例外。README "Surface" 一节断言 "There are no standalone ops",而当前ops列表仍注册op_crypto_random_uuid_batchop_crypto_is_seeded两个 op,分别服务 H2 ② 所述的 UUID 批量快路径与 seed 状态查询;
  3. JS 挂载代码已内化。README 的Object.defineProperty(globalThis, "crypto", ...)示例是嵌入方视角;Deno 本体的全局绑定改由 runtime/js/98_global_scope_shared.js 完成,00_crypto.js 导出的是Crypto、gettercryptoCryptoKeySubtleCrypto及 Node.jsKeyObject互用函数cryptoKeyExportNodeKeyMaterial/importCryptoKeySync

⑥ 阅读路线图

建议按依赖顺序走四站:lib.rs(扩展声明、CryptoError映射表、sign_key_sync/verify_key_sync/derive_bits_sync三个同步分派内核)→ 00_crypto.js(单例铸造、async 转发器、UUID 双路径,全文仅 300 余行)→ crypto.rs / subtle_sign.rs(一个 getter 型 cppgc 类 + 一个操作族的 converter/run样板)→ 目标算法的subtle_*.rs与原子模块(digest.rs 的 XOF 校验、mldsa.rs 的后量子路径)。验证行为边界时配合 tests/unit/webcrypto_test.ts(算法面)、tests/wpt/(规范形状)以及lib.rs内的test_fast_uuid_v4_correctness(Rust 单测)。调试某个subtle.*调用时,先断点subtle_*.rs里的run()入口——converter 已把参数归一化完毕,此处再往下就是纯 Rust 分派,不再有 JS 边界。

【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:00:42

Spring Boot Banner定制实战:从release包下载到项目接入全指南

简介&#xff1a;面向 Android 开发者的 Banner 组件库发布包&#xff0c;版本 1.4.10&#xff0c;核心解决广告位、推荐位等内容的自动轮播与循环展示需求&#xff0c;适用于新闻客户端、电商首页、运营活动页等常见场景。库内已封装自动播放间隔配置、多页面切换动画、无限循…

作者头像 李华
网站建设 2026/9/8 22:57:00

浏览器会话跨机迁移:从Cookie到Playwright的共享登录态方案

简介&#xff1a;一套面向Web开发与网络安全学习者的共享浏览器工程方案&#xff0c;重点解决异地设备间Session克隆与会话无缝迁移问题。方案深入讲解HTTP会话机制&#xff0c;围绕Session ID捕获、加密传输、请求头注入与实时同步等核心步骤&#xff0c;覆盖Cookie管理、HTTP…

作者头像 李华
网站建设 2026/9/8 22:56:30

Vue3+TypeScript+Uniapp构建医疗小程序全流程实战

简介&#xff1a;面向使用 Vue3、TypeScript 与 Uniapp 开发跨端小程序的开发者&#xff0c;这份完整医疗挂号小程序案例覆盖首页、预约挂号、时段选择、个人中心、视频信息等核心业务模块&#xff0c;并配有类型声明、请求封装、页面路由与构建配置等工程化内容。资源共 30 个…

作者头像 李华