拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Deno ext/crypto 深度解析:cppgc 对象化与种子随机数

Deno ext/crypto 深度解析:cppgc 对象化与种子随机数【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno一、从一个不可复现的测试说起给 Deno 写集成测试时很容易撞上这个痛点:代码里调了crypto.getRandomValues()或crypto.randomUUID(),快照对比第二天必然失败——熵来自操作系统,天然不可复现。更隐蔽的问题是错误断言:await crypto.subtle.sign(...)抛出来的到底是TypeError还是DOMException NotSupportedError,在 WPT(标准兼容性测试)里有硬性要求,写错一个名字整组用例全红。要回答随机数熵到底从哪来、密钥字节在内存的哪个位置、错误名称由谁决定,只能读源码。读完本文,你可以把任意一次crypto.subtle.*调用逐层追踪到具体的 Rust 函数,并说清每个设计点为什么不用另一种实现。二、心智地图:先看 ext/crypto 的组件拓扑deno_crypto这个 crate(见 ext/crypto/Cargo.toml 的description Web Cryptography API implementation for Deno)内部不是一个大文件结构,而是按接口层—操作层—算法层三层切分:各文件职责一句话概括:文件职责ext/crypto/lib.rsdeno_core::extension!声明、CryptoError错误枚举、KeyData、三个同步分派内核sign_key_sync/verify_key_sync/derive_bits_sync、fast_uuid_v4_bytesext/crypto/00_crypto.js刻意做薄的 JS shim:单例惰性铸造、privateCustomInspect、structured-clone 回调、async 转发器ext/crypto/crypto.rsCryptocppgc 类:getRandomValues、randomUUID、批量 UUID opext/crypto/subtle_crypto.rsSubtleCryptocppgc 类,16 个 API 全部落在这里ext/crypto/subtle_sign.rs 等subtle_*.rs每个操作的WebIdlConverter 操作级校验 run()ext/crypto/key_store.rsCryptoKeyHandle,密钥字节的 GC 托管容器ext/crypto/shared.rsRawKeyData枚举:密钥素材的类型表达ext/crypto/digest.rs、ext/crypto/ed25519.rs、ext/crypto/mlkem.rs、ext/crypto/mldsa.rs 等原子算法实现,含 XOF/KMAC 与后量子算法算法能力面由依赖声明直接体现:对称侧aes/aes-gcm/aes-kw/cbc/ctr/ocb3/hmac,非对称侧rsa/p256/p384/p521/curve25519-dalek/x25519-dalek/ecdsa,派生侧aws-lc-rs(HKDF/PBKDF2)/argon2,后量子侧fips203(启用ml-kem-512/ml-kem-768/ml-kem-1024)与fips205(ML-DSA),XOF 侧tiny-keccak(启用k12/kmacfeature)。三、一次 sign() 调用的完整旅程选crypto.subtle.sign(RSA-PSS, key, data)追踪,它穿过每一层设施。第 1 层:JS async 转发器。用户拿到的sign并不是 Rust 方法本身,而是 ext/crypto/00_crypto.js 里makeAsyncForwarder(sign, sign, 3)生成的包装。文件头注释解释了动机:a sync error path would propagate to JS as a synchronousthrow. That breaksassertRejectscallers ... WPTspromise_rejects_domwraps the call infn.call(undefined), which then surfaces the throw asTypeError: Failed to execute call...— a wrong shape compared to the specs rejected promise.也就是说,参数转换错误如果以同步throw冒出,错误形状就不符合规范必须 reject 一个 Promise的约定;转发器把成功与失败路径统一压进 Promise。第 2 层:cppgc 方法。真实实现是 ext/crypto/subtle_crypto.rs 中impl SubtleCrypto上的async fn sign,三个参数分别由SubtleSignParams、SubtleKey、BufferSource这三个WebIdlConverter在主线程、v8 栈上完成转换:算法字典解析、BufferSource字节拷贝(满足规范get a copy of the bytes)、CryptoKey槽位快照。第 3 层:converter 的推迟。ext/crypto/subtle_sign.rs 的WebIdlConverter for SubtleSignParams用canonical_sign_name做不区分大小写的名字归一;查不到的名字不抛错,而是塞进SubtleSignParams::Unknown(name)原样带下去。extract_name_and_obj处理字符串或{ name, ... }两种AlgorithmIdentifier形态;read_required_u32手动实现[EnforceRange] unsigned long语义(拒绝 NaN/负数/超u32::MAX),注释里明确说这不是 ECMAScript 的ToUint32截断。第 4 层:离开 v8 栈。方法体只有一行:spawn_blocking(move || run_sign(algorithm, key, data.0)).await?。签名计算可能很重,直接在事件循环线程算会阻塞所有 I/O;而子线程不能碰 V8 isolate,所以上一层的快照(SubtleKey携带从 handle 读出的RawKeyData副本)是必须的前置条件。第 5 层:操作级校验。ext/crypto/subtle_sign.rs 的run()先做跨算法一致性检查:params.canonical_name() ! key.algorithm_name报InvalidAccessError,key.has_usage(sign)不满足同样InvalidAccessError。RsaPss分支取出 key 上铸造时记录的hash,组好SignArg后进入同步内核。第 6 层:同步分派。ext/crypto/lib.rs 的sign_key_sync按Algorithm变体分派。RsaPss分支:Algorithm::RsaPss { let private_key RsaPrivateKey::from_pkcs1_der(key.data)?; let salt_len args.salt_length .ok_or_else(|| CryptoError::MissingArgumentSaltLength)? as usize; let mut rng OsRng; match args.hash.ok_or_else(|| CryptoError::MissingArgumentHash)? { CryptoHash::Sha256 { let signing_key Pss::new_with_salt::Sha256(salt_len); let hashed Sha256::digest(data); signing_key.sign(Some(mut rng), private_key, hashed)? } ... } .to_vec() }注意let mut rng OsRng:PSS 每次签名随机生成盐,熵源就是操作系统。返回值Vecu8经#[arraybuffer]变成ArrayBuffer,Promise resolve;任何Err沿CryptoError的#[class(...)]属性变成对应名称的 DOMException 被 reject。四、设计决策拆解4.1 密钥字节:为什么从 JS WeakMap 搬到 Rust 侧 GC 对象问题。每个CryptoKey背后是敏感字节(HMAC 密钥、PKCS#8 私钥)。历史实现把字节存在00_crypto.js的 JSWeakMap里,每次加密操作都要把字节序列化过 JS/Rust 边界。现有实现。ext/crypto/key_store.rs 的文档注释完整记录了演进: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 ... the key material is freed automatically by V8s garbage collector once the handle is collected ... NoFinalizationRegistryor manual bookkeeping is required.CryptoKeyHandle是unsafe impl GarbageCollected的 V8 GC 对象,内部只有data: RawKeyData(见 ext/crypto/shared.rs)。ext/crypto/lib.rs 中KeyData的注释同样留了痕迹:Previously the key bytes were serialized and passed from JavaScript on every operation.代价与取舍。换来三件事:免序列化(每次操作按 handle 查快照)、生命周期免簿记(GC 回收即释放)、JS 层再也接触不到原始字节(不可读、不可改)。代价是 converter 必须把SubtleKey连同密钥快照一起搬进spawn_blocking闭包,密钥在阻塞线程上会短暂多存一份Box[u8](见KeyData { r#type, data: Box[u8] })。另外FromRawKeyData for KeyData里对SeededPrivate直接unreachable!(),注释说明复合密钥(ML-KEM/ML-DSA)不会走到 sign/verify/derive 这条路。4.2 randomUUID:为什么保留两条随机数路径问题。randomUUID()是热点 API,但测试/快照场景又需要可复现的随机流——这两件事的优化方向相反。现有实现。扩展声明里种下开关(ext/crypto/lib.rs):options { maybe_seed: Optionu64 }, state |state, options| { if let Some(seed) options.maybe_seed { state.put(StdRng::seed_from_u64(seed)); } },有 seed 时OpState里放一个确定性StdRng;JS 侧 ext/crypto/00_crypto.js 在铸造Crypto单例时调op_crypto_is_seeded()记下usesSeededRng,随后randomUUID分两条路:批量路(无 seed):op_crypto_random_uuid_batch()一次向thread_rng()要 128×16 字节熵,经 ext/crypto/lib.rs 的fast_uuid_v4_bytes就地置位版本/变体位(bytes[6] (bytes[6] 0x0f) | 0x40)再用HEX_CHARS查表拼出 4608 字节的连续串,JS 端用StringPrototypeSlice按 36 字节切片消费。同文件里的test_fast_uuid_v4_correctness用uuidcrate 对拍,保证手工格式化与标准库逐字节一致。原生路(有 seed):直接走 cppgc 方法random_uuid,每次单发消费 seededStdRng。代价与取舍。批量路省掉 127 次 op 往返和 V8 字符串构造;但 seed 场景必须保留精确的 RNG 调用顺序——文件头注释原话是seeded runtimes use the native method to preserve exact RNG call order——否则确定性流的消费时序一变,所有快照全漂移。另外批量路还有this ! cryptoSingleton的前置判断:别的对象借用该原型方法时退回原生方法,防止误用别的this却消费了单例的缓存。4.3 错误命名:为什么 Unknown 算法名不能在 converter 层报错问题。未注册的算法名(如sign(MyAlgo, ...))规范上要求NotSupportedError,而WebIdlError这个类型硬编码为#[class(type)],在 converter 层抛出必然变成TypeError。现有实现。ext/crypto/subtle_sign.rs 中read_required_hash的注释把这个约束钉得很死:Does NOT validate the name against the known SHA list -- callers must do that at run-time and surface aDOMException NotSupportedError. (TheWebIdlErrortype is hardcoded#[class(type)], so anyNotSupportedErrorraised here would surface as aTypeError, breaking WPTECDSA verification failure due to bad hash namewhich assertserr.name NotSupportedError.)因此 converter 把陌生名字包装成SubtleSignParams::Unknown(name)下传,run()末尾统一not_supported(format!(Algorithm {name} is not supported))。文件尾部的invalid_access/op_error/not_supported三个 helper 用JsErrorBox::new(DOMException*, msg)精确指定异常类名。代价与取舍。每个subtle_*.rs都带一份自己的错误 helper,且名字合法性被推迟到分派期才知道,converter 层看起来不彻底。但换来的是错误名称与 WPT 逐条对齐——在 WebIDL 转换器和规范错误模型之间,这个 crate 选择了让转换器退让。五、边界与兼容工程这个 crate 里边界不是异常分支,而是被标准测试逐条钉住的契约。错误类映射总表(摘自 ext/crypto/lib.rs 的CryptoError):变体JS 侧异常类触发点MissingArgumentHash/MissingArgumentSaltLengthTypeErrorhash/saltLength缺失HKDFLengthTooLargeDOMExceptionOperationErrorHKDFexpand超限DecryptionErrorDOMExceptionOperationErrorAEAD 认证标签失败,消息 decryption error - integrity check failedArrayBufferViewLengthExceededDOMExceptionQuotaExceededErrorgetRandomValues输入超 65536 字节TypedArrayNotIntegerDOMExceptionTypeMismatchError浮点 TypedArray 等,而非 WebIDL 默认的TypeErrorUnsupportedDigestAlgorithmDOMExceptionNotSupportedError未登记哈希名注意get_random_values(ext/crypto/crypto.rs)的注释专门解释了为什么报TypeMismatchError而不是 WebIDL 默认的TypeError:规范把所有非整型参数(含null、DataView、浮点数组)都归到TypeMismatchError。P-521 的左补零。这是最容易漏掉的曲线相关细节。ext/crypto/lib.rs 的 ECDSA 分支:// P-521 field size is 66 bytes; bits2field requires at least // half that (33 bytes). Left-pad shorter hashes to meet the // minimum. let prehash if prehash.len() 33 { let mut padded vec![0u8; 33 - prehash.len()]; padded.extend_from_slice(prehash); padded } else { prehash };用 SHA-1(20 字节)对 P-521 签名时,若不补零,bits2field会直接失败。sign_key_sync与verify_key_sync各有一份相同处理——签名与验签必须走完全一致的规范化,否则会出现自己签的验不过。常量时间的空判定。ECDH 分支(ext/crypto/lib.rsderive_bits_sync)解码对端公钥时:let pk p256::PublicKey::from_encoded_point(point); // pk is a constant time Option. if pk.is_some().into() { pk.unwrap() }from_encoded_point返回的是常量时间Option,若直接match可能通过分支时序泄露点是否有效。这对非正规编码的公钥输入是有意的防御。XOF 参数防回绕。ext/crypto/digest.rs 的read_optional_u8注释:0x101does not wrap to0x01and slip past the callers range check即domainSeparation这类必须是 u8 且落在[0x01, 0x7F]的字段,先按完整数值读出再拒绝 0xFF的输入,堵住先取低 8 位再查范围的绕过。违规统一报InvalidXofParameters(OperationError)。JS 层的形状契约。ext/crypto/00_crypto.js 的applyWebIdlInterfaceShape把三个构造器的length钉成 0、prototype钉成不可写,注释里写明原因:op2 宏生成的 new-target 信号会让length变成 1,而 Web IDL 要求无暴露构造器的接口对象length为 0。makeAsyncForwarder的第三个参数(如unwrapKey为 7、deriveKey为 5)同理,是喂给 idlharness 的必需参数个数。这些看起来无意义的属性赋值,每一个背后都是一条 WebIDL 断言。六、README 与源码的三处差异ext/crypto/README.md 是较早的文档,引用时注意以下偏差。There are no standalone ops 不成立。当前扩展仍注册了两个 ops:ext/crypto/lib.rs 的扩展宏列了op_crypto_random_uuid_batch与op_crypto_is_seeded,紧随其后的注释解释了留任理由:前者给 JS 批量 UUID 快路径补水,后者在铸造单例时一次性确定 seeded 路径。init(Optionu64)入口已分化为三种。README 只写了init(Optionu64),当前仓库有四处真实调用:deno_crypto::deno_crypto::args(options.seed)(runtime/worker.rs 第 636 行)、init(options.seed)(runtime/web_worker.rs 第 569 行)、init(None)(runtime/snapshot_info.rs 第 33 行)与lazy_init()(runtime/snapshot.rs 第 70 行、runtime/worker.rs 第 1222 行)。种子语义未变,只是注入方式按调用方分路。README 的Object.defineProperty示例是嵌入方视角。Deno 本体运行时通过扩展系统注入该 crate,00_crypto.js的return块导出的除了Crypto/CryptoKey/SubtleCrypto和 gettercrypto,还有两个 Node.jsKeyObject互用函数cryptoKeyExportNodeKeyMaterial/importCryptoKeySync(分别转发到CryptoKey.exportNodeMaterial/CryptoKey.importSync),README 完全没有提及。七、代码阅读路径按依赖顺序推进,每步都能独立编译理解:ext/crypto/lib.rs——扩展宏(3 个 objects、2 个 ops、maybe_seed)与CryptoError全枚举,先建立谁是什么异常类的字典;ext/crypto/00_crypto.js——334 行读完 JS 侧全部簿记,重点看getSubtleSingleton/getCryptoSingleton与makeAsyncForwarder的注册表;ext/crypto/subtle_sign.rs 的run()——一个完整操作的converter → 校验 → 分派样板,其余subtle_*.rs结构相同;ext/crypto/key_store.rs ext/crypto/shared.rs——密钥素材的类型系统与 GC 生命周期。两个可以继续深挖的方向:一是RawKeyData::SeededPrivate { seed, private_key }复合素材在 ext/crypto/mlkem.rs / ext/crypto/mldsa.rs 里如何支撑 FIPS 203/204 的派生与导出限制;二是 ext/crypto/digest.rs 底部手写的TurboShakeNode136sponge 如何用 8192 字节树形归约补齐tiny-keccak只有k12feature 留下的 KangarooTwelve-256 缺口。行为边界以 tests/unit/ 下的 webcrypto 系列单测为锚点,读代码时遇到拿不准的语义,先搜对应 WPT 用例名再下结论。【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门