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 |
|---|---|
| 哈希 | sha1、sha2、sha3、tiny-keccak(k12/kmacfeature) |
| 非对称 | rsa、p256(ecdh+ecdsa)、p384、p521、ecdsa、signature、spki、const-oid |
| 对称 | aes、aes-gcm、aes-kw、cbc、ctr、ocb3、hmac |
| 密钥派生 | aws-lc-rs(HKDF/PBKDF2)、argon2 |
| 后量子 | fips203(ML-KEM-512/768/1024)、fips205(ML-DSA) |
| 原子算法 | curve25519-dalek、x25519-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)为样本,链路依次经过:
- 00_crypto.js:
makeAsyncForwarder("sign", "sign", 3)包出的 async 包装器,把同步异常收敛为 Promise rejection; - subtle_crypto.rs:cppgc 方法
sign,op2 生成的 dispatcher 同步调用WebIdlConverter归一参数,再把方法体丢进spawn_blocking; - subtle_sign.rs:
SubtleSignParams的 converter 解析算法字典,run()校验"密钥算法名 == 参数算法名、usages含sign、密钥类型匹配"; - lib.rs:
sign_key_sync(key, args, data)按Algorithm变体做纯 Rust 分派(注释原文:"Called fromcrate::subtle_sign::runinsidespawn_blocking")。
JS 层因此被刻意做薄到只剩"簿记",文件头注释逐条列明:
privateCustomInspect装饰:给三个原型挂Deno.privateCustomInspect符号,让Deno.inspect(cryptoKey)只显示type/extractable/algorithm/usages四个 WebIDL 形状属性;- 惰性铸造单例:cppgc 堆在快照构建期未附着到 V8 isolate,故
Crypto.create(getSubtleSingleton())与SubtleCrypto.create()必须延迟到运行时首次读取globalThis.crypto时执行,同时打上webidl.brand; - structured-clone 复活回调:
core.registerCloneableResource("CryptoKey", (data) => CryptoKey.fromCloneData(data)),使CryptoKey可跨 Worker 克隆; 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.rs的mod列表一一对应:
| 操作族 | 参数归一 + 校验 + 调度 | 算法内核落点 |
|---|---|---|
| digest | digest.rs | aws_lc_rs::digest、sha3、内置 KT256 sponge |
| sign / verify | subtle_sign.rs / subtle_verify.rs | lib.rssign_key_sync/verify_key_sync |
| encrypt / decrypt | subtle_encrypt.rs / subtle_decrypt.rs | encrypt.rs / decrypt.rs |
| deriveBits / deriveKey | subtle_derive_bits.rs / subtle_derive_key.rs | lib.rsderive_bits_sync |
| import / export / generate / wrap / getPublicKey | 同名subtle_*.rs | import_key.rs、export_key.rs、generate_key.rs 等 |
| 封装/解封装 | subtle_encapsulate.rs、subtle_encapsulate_key.rs | mlkem.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 调用顺序。usesSeededRng由getCryptoSingleton()铸造单例时调用op_crypto_is_seeded()记下——这就是 seed 从 RustOpState反哺 JS 分派的闭环。
分派内核消费的第一样东西是密钥,而密钥如今不再住在 JS 里。
③ 状态与数据的驻留位置:密钥字节为什么搬进 GC 对象
四类关键状态的落点:
| 状态 | 驻留位置 | 生命周期由谁管 |
|---|---|---|
播种 RNG(StdRng) | OpState(state闭包注入) | runtime 存活期 |
| UUID 批量缓存、单例、seed 标志 | 00_crypto.js 模块作用域 | isolate 存活期 |
SubtleCrypto实例引用 | Cryptocppgc 对象的v8::Global字段 | V8 堆 |
| 密钥字节 | CryptoKeyHandle(cppgc 对象) | V8 GC 回收 handle 时释放 |
krypto 侧注释 把演进动机写得很直白:
Historically the key material for every
CryptoKeylived 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为派生用短种子;注释指出seed为None时(从展开私钥导入),导出raw-seed/jwk/pkcs8格式会被正确拒绝。格式与类型标签同样定义在 lib.rs:KeyFormat { Raw, Pkcs8, Spki }、KeyType { Secret, Private, Public }。
④ 规范对齐与错误模型:错误在哪个阶段抛
WebCrypto 要求错误以特定 DOMException 名称出现。lib.rs 的CryptoError枚举用#[class(...)]把每个变体精确绑到 JS 异常类:
| 变体 | JS 异常类 | 消息 |
|---|---|---|
MissingArgumentHash | TypeError | Missing argument hash |
MissingArgumentSaltLength | TypeError | Missing argument saltLength |
HKDFLengthTooLarge | DOMExceptionOperationError | The length provided for HKDF is too large |
DecryptionError | DOMExceptionOperationError | decryption error - integrity check failed(AEAD 认证标签失败) |
InvalidXofParameters | DOMExceptionOperationError | Invalid XOF parameters |
ArrayBufferViewLengthExceeded(usize) | DOMExceptionQuotaExceededError | 上限 65536 字节熵 |
TypedArrayNotInteger | DOMExceptionTypeMismatchError | The provided value is not an integer-type TypedArray |
UnsupportedDigestAlgorithm(String) | DOMExceptionNotSupportedError | Algorithm '{0}' is not supported |
IllegalConstructor(shared.rsSharedError) | TypeError | Illegal constructor,附code: ERR_ILLEGAL_CONSTRUCTOR |
错误推迟到哪个阶段抛是被 WPT 逐条钉死的设计。digest.rs 的DigestAlgorithm注释:
Unrecognized algorithm names are kept as
DigestAlgorithm::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非零,违规统一报InvalidXofParameters;BufferSourceconverter 则把字节物化为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 是较早时期的文档,引用时注意三处演进:
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()(快照)接线。种子的语义未变,变的只是扩展宏生成的构造函数名;- "无独立 ops" 已基本成立但留有两个例外。README "Surface" 一节断言 "There are no standalone ops",而当前
ops列表仍注册op_crypto_random_uuid_batch与op_crypto_is_seeded两个 op,分别服务 H2 ② 所述的 UUID 批量快路径与 seed 状态查询; - JS 挂载代码已内化。README 的
Object.defineProperty(globalThis, "crypto", ...)示例是嵌入方视角;Deno 本体的全局绑定改由 runtime/js/98_global_scope_shared.js 完成,00_crypto.js 导出的是Crypto、gettercrypto、CryptoKey、SubtleCrypto及 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),仅供参考