
Deno deno_crypto 深度解析Web Crypto API 的 Rust 实现与可播种随机数【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/denoconst buf new Uint8Array(16); crypto.getRandomValues(buf);这行代码在浏览器里跑了几十年。但在 Deno 仓库里它落到 ext/crypto 这个 crate——deno_crypto一个纯 Rust 实现的 Web Crypto API。你调用crypto.subtle.digest(SHA-256, data)时字节流先被拷进 Rust进 worker 线程池跑完哈希再回来而crypto.randomUUID()背后还藏着一个Optionu64种子它能把整个 RNG 切成确定性回放模式。本文顺着这三个问题往下挖接口怎么变成 Rust 对象、seed 怎么改写随机数路径、密钥字节为什么从 JavaScriptWeakMap搬进了 Rust 侧的垃圾回收对象。三个 cppgc 包裹类WebCrypto 接口在 Rust 侧的落点打开 lib.rsdeno_core::extension!宏声明里真正决定 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 }, ... );可以看到Crypto、SubtleCrypto、CryptoKey三个接口全部注册在objects列表里。cppgc 是 V8 的 C 垃圾回收机制Rust 结构体实现GarbageCollectedtrait 后就能活在 V8 堆上随 GC 自动生灭。类身份、原型链、每个方法的实现都住在 Rust 里JS 侧脚本只保留薄薄一层簿记。单例为什么必须惰性铸造快照构建时 cppgc 堆尚未附着到 V8 isolate所以Crypto/SubtleCrypto实例不能进快照必须延迟到运行期第一次读globalThis.crypto时经由Crypto.create(getSubtleSingleton())与SubtleCrypto.create()两个静态方法分配。crypto.rs 的Crypto只存一个字段——subtle: v8::Globalv8::Valuesubtlegetter 每次返回同一身份满足 Web IDL 的 SameObject 要求而new Crypto()直接报IllegalConstructor因为规范不允许。JS 层保留的簿记只有四类给三个原型挂privateCustomInspect让Deno.inspect输出符合 WebIDL 形状惰性铸造单例注册 structured-clone 复活回调让CryptoKey能跨 Worker 结构化克隆修正Function.length。最后这条有点绕op2 宏没法声明可选参数还不破坏宏层的最少参数检查于是deriveBits(algorithm, baseKey, length)包了一层三参转发器用声明参数个数凑出规范要求的length 2。所有SubtleCrypto方法则统一过makeAsyncForwarder包成 async——converter 在 async 体之前同步执行同步 throw 会变成调用点异常而不是 rejected PromiseWPT 的promise_rejects_dom正是用fn.call(undefined)这种姿势去接 rejection形态不符就红。种子注入一个 Option 劈出的两条随机数路径扩展声明里的options { maybe_seed: Optionu64 }配合state闭包把种子语义钉死在初始化一刻state |state, options| { if let Some(seed) options.maybe_seed { state.put(StdRng::seed_from_u64(seed)); } },seed 为什么不直接做成 op 参数、每次调用时传进来因为 OpState 是 deno_core 挂在每个 isolate 上的 Rust 侧注册表任何拿到state: mut OpState的方法都能直接借用它——StdRng放进去整个运行期的所有随机数路径就都看得见它。没有 seed 时随机数来自操作系统熵源有 seed 时则是可复现的确定性流seed 相当于给 RNG 按了回放键这是测试与快照场景要的。批量缓存与确定性回放谁快谁稳worker.rs 的WorkerOptions携带pub seed: Optionu64web_worker.rs 的 Web Worker 路径走deno_crypto::deno_crypto::init(options.seed)。JS 侧在铸造Crypto单例时调用op_crypto_is_seeded()记下usesSeededRng标志randomUUID的实现由此分叉function randomUUID() { if (this ! cryptoSingleton || usesSeededRng) { return FunctionPrototypeCall(cppgcRandomUUID, this); } if (uuidBatch UUID_BATCH_SIZE) { uuidBatchData op_crypto_random_uuid_batch(); uuidBatch 0; } const start uuidBatch * UUID_STRING_BYTES; return StringPrototypeSlice( uuidBatchData, start, start UUID_STRING_BYTES, ); }普通路径一次取回 128 条完整 UUID 字符串缓存在 JS后续 128 次调用纯 JS 切片不跨边界。批量 op 背后是fast_uuid_v4_bytes对每 16 字节就地设置 UUIDv4 的版本位与变体位再用HEX_CHARS查表拼出 36 字节字符串省掉整条格式化路径同文件里还挂着与uuidcrate 对拍的正确性测试。seed 路径则每次调用都走原生方法——批量会打乱 RNG 的调用序列而确定性测试要钉住的恰恰是这个序列。密钥的归宿WeakMap 退场之后cppgc 接住了什么早期实现里每个CryptoKey的密钥字节存在 JS 侧WeakMapKEY_STORE里每次签名、加密、派生都要把字节序列化一遍跨 JS/Rust 边界。2048 位 RSA 的 PKCS#8 私钥意味着每次操作都拖几 KB 序列化。key_store.rs 把这个位置整体搬到了 Rustpub struct CryptoKeyHandle { data: RawKeyData, } unsafe impl GarbageCollected for CryptoKeyHandle { fn trace(self, _visitor: mut v8::cppgc::Visitor) {} ... }trace是空实现——handle 里只有纯字节没有任何指向 V8 堆的引用。GC 追踪它handle 被回收即再没有CryptoKey引用它时密钥字节随之释放不需要FinalizationRegistry或任何手动簿记。crypto_key.rs 的CryptoKey结构体持有这个 handle 的v8::Global引用引用语义还允许密钥对的两半共享同一份底层材料。素材本身的类型由 shared.rs 的RawKeyData表达pub enum RawKeyData { Secret(Box[u8]), Private(Box[u8]), Public(Box[u8]), Raw(Box[u8]), SeededPrivate { seed: OptionBox[u8], private_key: Box[u8], }, }Secret/Private/Public是带用途标签的字节Raw原样存储、不带标签Ed25519/X25519/X448/ML-KEM 公钥走这里SeededPrivate是 FIPS 203/204 算法的复合素材private_key是展开后的密钥字节seed是派生用的短种子。从展开私钥字节导入时seed为None导出raw-seed/jwk/pkcs8格式会被正确拒绝——边界就编码在这个Option里。操作内部流动的KeyData则是从RawKeyData拷出的临时素材type 字节FromRawKeyData for KeyData对SeededPrivate直接unreachable!()复合素材从不流向 sign/verify/derive。三条同步内核sign、verify、deriveBits 在 Rust 里走了多远lib.rs 里的sign_key_sync/verify_key_sync/derive_bits_sync是同步内核subtle_sign.rs 的run在spawn_blocking里调用它们——签名这类 CPU 密集工作被挪出主线程。sign_key_sync按Algorithm变体分派完整枚举在 key.rsRSASSA-PKCS1-v1_5、RSA-PSS、RSA-OAEP、ECDSA、AES-GCM、HMAC、PBKDF2、HKDF直到后量子的 ML-KEM / ML-DSA 都在内RSASSA-PKCS1-v1_5 从 PKCS#1 解码私钥按hash变体构造签名密钥缺hash报MissingArgumentHashRSA-PSS 额外要求salt_length用OsRng随机盐再Pss::new_with_saltECDSA 按named_curve从 PKCS#8 解码后sign_prehash产出 rawr||s签名HMAC 的 SHA3 变体走tiny-keccak路径其余走aws-lc-rs。P-521 分支有个不那么显眼的细节。这条曲线的阶是 521 位66 字节bits2field要求 prehash 至少 33 字节而 SHA-1 只有 20 字节let prehash if prehash.len() 33 { let mut padded vec![0u8; 33 - prehash.len()]; padded.extend_from_slice(prehash); padded } else { prehash };缺这一步P-521 SHA-1 的组合会直接解码失败。verify_key_sync结构对称但多一条 WebCrypto 允许的路径KeyType::Private的 ECDSA 验证先从 PKCS#8 推导出VerifyingKey即允许用私钥对象验证验证失败统一返回false而非抛错符合规范。derive_bits_sync里salt为Some对应 PBKDF2/HKDF、None对应 ECDHPBKDF2 断言length是 8 的倍数后派生length/8字节HKDF 先extract出 PRK 再expand超限报HKDFLengthTooLargeECDH 对三条曲线都用elliptic_curve::ecdh::diffie_hellman公共方既可以是 SPKI 编码点常量时间Option判空也可以是 PKCS#8 私钥自动.public_key()返回共享点的 raw x 坐标。错误模型与 WPT 兼容策略XOF 参数校验为什么分两层WebCrypto 规范要求错误以特定 DOMException 名抛出lib.rs 的CryptoError枚举用#[class(...)]属性把每个变体绑到具体 JS 异常类#[class(DOMExceptionOperationError)] #[error(The length provided for HKDF is too large)] HKDFLengthTooLarge, #[class(DOMExceptionOperationError)] #[error(Invalid XOF parameters)] InvalidXofParameters, #[class(DOMExceptionQuotaExceededError)] #[error(The ArrayBufferViews byte length ({0}) exceeds ...)] ArrayBufferViewLengthExceeded(usize),缺hash或saltLength的变体用#[class(type)]映射到TypeError。错误名的选择不是实现者的自由digest.rs 的DigestAlgorithm注释说得很清楚converter 层直接抛错会得到 WebIDLTypeError而 WPTdigest.https.any.html的子测试硬编码了NotSupportedError所以未知算法名被保留为DigestAlgorithm::Unknown推迟到run()再抛规范要求的错误类。缺name成员则相反按 WebIDL 必需成员语义在 converter 层报TypeError——WPT 的digest({}, ...)空对象子测试钉死了这一侧。XOF 参数谁在 converter 报错谁在 run 报错cSHAKE / TurboSHAKE / KangarooTwelve 这七个 XOF 算法的参数字典由SubtleDigestXof枚举承接校验分两层。converter 层只读成员缺outputLength报TypeError数值合法性推迟到run_xofif !output_length.is_multiple_of(8) { return Err(CryptoError::InvalidXofParameters); } if (is_turbo || is_kangaroo) output_length 0 { return Err(CryptoError::InvalidXofParameters); } if let SubtleDigestXof::TurboShake128 { domain_separation, .. } | SubtleDigestXof::TurboShake256 { domain_separation, .. } algorithm let Some(d) domain_separation !(0x01..0x7F).contains(d) { return Err(CryptoError::InvalidXofParameters); }read_optional_u8读domainSeparation时先按 u32 读完整值再拒绝 0xFF防止0x101回绕成0x01绕过范围检查。BufferSource转换器把ArrayBuffer/ArrayBufferView物化成Vecu8保证数据跨spawn_blocking的.await安全并显式拒绝SharedArrayBuffer及其视图。KangarooTwelve-256 的实现更特殊tiny-keccak只带k12featureKT256 用文件内手写的TurboShakeNode136sponge状态 25×u64、rate 136 字节按 8192 字节分块做树形归约补上。README 与源码之间接口演进的三处漂移README.md 是较早时期的文档与当前仓库有三处差异引用它时心里要有数。初始化签名已经换了写法。README 说提供deno_crypto::deno_crypto::init(Optionu64)当前仓库里 worker 路径是deno_crypto::deno_crypto::init(options.seed)快照路径改用lazy_init()选项本身也从函数参数演进成了扩展宏里的options.maybe_seed。seed 语义没变有 seed 时OpState里放一个确定性StdRng。没有独立 ops的断言留了两个例外。README Surface 一节说所有入口都是 cppgc 方法、没有 standalone ops当前扩展声明里还留着op_crypto_random_uuid_batch与op_crypto_is_seeded——前者服务批量 UUID 快路径后者向 JS 暴露 seed 状态。JS 挂载代码已内化。README 的Object.defineProperty(globalThis, ...)示例是嵌入方视角Deno 本体的全局绑定现在由 runtime/js/98_global_scope_shared.js 完成扩展脚本额外导出两个 Node.jsKeyObject互用函数cryptoKeyExportNodeKeyMaterial/importCryptoKeySync。算法能力面则由 Cargo.toml 的依赖声明体现哈希是sha1/sha2/sha3/tiny-keccak非对称是rsa/p256/p384/p521/curve25519-dalek/x25519-dalek/ecdsa对称是aes/aes-gcm/aes-kw/cbc/ctr/ocb3/hmac派生是aws-lc-rs/argon2后量子是fips203ML-KEM与fips205ML-DSA。阅读路线顺着数据流走最快先读 lib.rs——扩展声明、CryptoError映射与三条同步内核都在一屏之内再跳 00_crypto.js 看单例铸造、Function.length修正与两条 UUID 路径如何咬合然后 digest.rs 看 converter 与run_xof的双层校验怎么被 WPT 钉死最后配合 tests/unit/webcrypto_test.ts 验证行为边界。【免费下载链接】denoA modern runtime for JavaScript and TypeScript.项目地址: https://gitcode.com/GitHub_Trending/de/deno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考