feat(js): implement WebCrypto SubtleCrypto secret-key algorithms

crypto.subtle only implemented digest. sign returned a fixed 32-byte
buffer, verify always returned true, encrypt and decrypt threw, and
generateKey, importKey and deriveBits handed back placeholder data.
Pages that use WebCrypto (anti-bot probes, PKCE and other auth flows,
client-side crypto) silently got wrong results.

Implement the secret-key half of WebCrypto on RustCrypto, following the
existing op_subtle_digest pattern:

- HMAC sign and verify (SHA-1/256/384/512)
- AES-GCM, AES-CBC and AES-CTR encrypt and decrypt
- PBKDF2 and HKDF deriveBits and deriveKey
- raw and symmetric JWK (oct) importKey and exportKey, generateKey,
  wrapKey and unwrapKey
- getRandomValues and randomUUID now draw from the OS CSPRNG instead of
  Math.random, so they are uniform across typed-array widths and no
  longer a fingerprinting tell

Unsupported algorithms (RSA, ECDSA, ECDH) and key formats (pkcs8, spki)
throw NotSupportedError instead of returning fake data. The crypto runs
in stateless Rust ops; the bootstrap.js shim only marshals bytes and
normalizes algorithm parameters, so the native fast paths are untouched
and there is no cost unless a page calls crypto.

Verified against RFC 4231 (HMAC), the NIST GCM vectors, NIST SP800-38A
(AES-CTR), RFC 6070 (PBKDF2) and RFC 5869 (HKDF), plus roundtrip and
tag/padding tamper-rejection checks.
This commit is contained in:
MrBeanDev committed 2026-07-01 12:11:55 +05:30
1 parent d5d9b7788f
commit ed75730e2d
4 files changed
+705 -27

No files matched your search

+11
View File
@@ -30,3 +30,14 @@ deno_error = "0.6"
base64 = { workspace = true }
sha1 = "0.10"
sha2 = "0.10"
# WebCrypto (crypto.subtle) primitives. RustCrypto, pure Rust, no CMake/OpenSSL,
# and they unify on the digest 0.10 / cipher 0.4 stack already in the tree.
hmac = "0.12"
aes = "0.8"
aes-gcm = "0.10"
cbc = { version = "0.1", features = ["alloc"] }
ctr = "0.9"
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
hkdf = "0.12"
# CSPRNG for getRandomValues / randomUUID (replaces Math.random).
getrandom = "0.2"
+292 -27
View File
@@ -4835,8 +4835,33 @@ Object.defineProperty(Document.prototype, 'fonts', {
configurable: true,
});
globalThis.Crypto = class Crypto {
getRandomValues(arr) { for(let i=0;i<arr.length;i++) arr[i]=Math.floor(Math.random()*256); return arr; }
randomUUID() { return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,c=>{const r=Math.random()*16|0;return(c==="x"?r:(r&3|8)).toString(16);}); }
// Fill an integer TypedArray from the OS CSPRNG. Filling the underlying bytes
// (not per-element Math.random) keeps the distribution uniform across every
// typed-array width and is actually cryptographically random.
getRandomValues(arr) {
if (!ArrayBuffer.isView(arr) || arr instanceof DataView ||
arr instanceof Float32Array || arr instanceof Float64Array ||
(typeof Float16Array !== 'undefined' && arr instanceof Float16Array)) {
throw new DOMException("The provided ArrayBufferView is not an integer-typed array", "TypeMismatchError");
}
if (arr.byteLength > 65536) {
throw new DOMException("The requested length exceeds 65536 bytes", "QuotaExceededError");
}
const bytes = Deno.core.ops.op_random_bytes(arr.byteLength);
new Uint8Array(arr.buffer, arr.byteOffset, arr.byteLength).set(bytes);
return arr;
}
randomUUID() {
const b = Deno.core.ops.op_random_bytes(16);
b[6] = (b[6] & 0x0f) | 0x40; // version 4
b[8] = (b[8] & 0x3f) | 0x80; // variant 10xx
let s = "";
for (let i = 0; i < 16; i++) {
s += (b[i] + 0x100).toString(16).slice(1);
if (i === 3 || i === 5 || i === 7 || i === 9) s += "-";
}
return s;
}
};
globalThis.crypto = globalThis.crypto || new globalThis.Crypto();
globalThis.structuredClone = globalThis.structuredClone || ((v) => JSON.parse(JSON.stringify(v)));
@@ -6608,32 +6633,272 @@ if (typeof TransformStream === 'undefined') {
if (!globalThis.crypto) globalThis.crypto = {};
if (!globalThis.crypto.subtle) {
globalThis.crypto.subtle = {
async digest(algorithm, data) {
const name = (typeof algorithm === 'string' ? algorithm : algorithm?.name || '').toUpperCase().replace('_', '-');
if (name !== 'SHA-1' && name !== 'SHA-256' && name !== 'SHA-384' && name !== 'SHA-512' &&
name !== 'SHA-512/224' && name !== 'SHA-512/256') {
throw new DOMException('Unrecognized algorithm name', 'NotSupportedError');
}
let bytes;
if (data instanceof ArrayBuffer) bytes = new Uint8Array(data);
else if (ArrayBuffer.isView(data)) bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
else bytes = new Uint8Array(data || []);
const out = Deno.core.ops.op_subtle_digest(name, bytes);
return new Uint8Array(out).buffer;
},
async encrypt() { throw new DOMException('NotSupportedError'); },
async decrypt() { throw new DOMException('NotSupportedError'); },
async sign() { return new ArrayBuffer(32); },
async verify() { return true; },
async generateKey() { return { type: 'secret', algorithm: {}, extractable: false, usages: [] }; },
async importKey() { return { type: 'secret', algorithm: {}, extractable: false, usages: [] }; },
async exportKey() { return new ArrayBuffer(32); },
async deriveBits() { return new ArrayBuffer(32); },
async deriveKey() { return { type: 'secret', algorithm: {}, extractable: false, usages: [] }; },
async wrapKey() { return new ArrayBuffer(32); },
async unwrapKey() { return { type: 'secret', algorithm: {}, extractable: false, usages: [] }; },
// Real WebCrypto for the secret-key algorithms sites actually use: HMAC,
// AES-GCM/CBC/CTR, PBKDF2 and HKDF, plus raw/JWK-oct key handling. The crypto
// itself runs in Rust ops (RustCrypto); this shim only marshals bytes and
// normalizes algorithm parameters. Public-key algorithms (RSA*, ECDSA, ECDH)
// and non-symmetric key formats (pkcs8/spki) are not implemented and throw
// NotSupportedError rather than returning fake data.
const keyMaterial = new WeakMap();
class CryptoKey {
constructor() { throw new TypeError("Illegal constructor"); }
get [Symbol.toStringTag]() { return "CryptoKey"; }
}
function makeKey(type, extractable, algorithm, usages, bytes) {
const k = Object.create(CryptoKey.prototype);
Object.defineProperty(k, "type", { value: type, enumerable: true });
Object.defineProperty(k, "extractable", { value: !!extractable, enumerable: true });
Object.defineProperty(k, "algorithm", { value: algorithm, enumerable: true });
Object.defineProperty(k, "usages", { value: Object.freeze((usages || []).slice()), enumerable: true });
keyMaterial.set(k, bytes);
return k;
}
function keyBytes(key) {
if (!(key instanceof CryptoKey) || !keyMaterial.has(key)) {
throw new DOMException("Argument is not a valid CryptoKey", "InvalidAccessError");
}
return keyMaterial.get(key);
}
const toBytes = (data) => {
if (data instanceof ArrayBuffer) return new Uint8Array(data);
if (ArrayBuffer.isView(data)) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
return new Uint8Array(data || []);
};
const bufferOf = (u8) => new Uint8Array(u8).buffer;
const ALGO_CANON = {
"AES-CTR": "AES-CTR", "AES-CBC": "AES-CBC", "AES-GCM": "AES-GCM", "AES-KW": "AES-KW",
"HMAC": "HMAC", "PBKDF2": "PBKDF2", "HKDF": "HKDF",
"RSASSA-PKCS1-V1_5": "RSASSA-PKCS1-v1_5", "RSA-PSS": "RSA-PSS", "RSA-OAEP": "RSA-OAEP",
"ECDSA": "ECDSA", "ECDH": "ECDH",
};
function normalizeAlgo(algorithm) {
const a = typeof algorithm === "string" ? { name: algorithm } : (algorithm || {});
const upper = String(a.name || "").toUpperCase();
const name = ALGO_CANON[upper] || upper;
return Object.assign({}, a, { name });
}
// SubtleCrypto hashes for HMAC/PBKDF2/HKDF and digest (SHA-1/256/384/512).
function normalizeHash(h) {
const n = (typeof h === "string" ? h : (h && h.name) || "").toUpperCase().replace("_", "-");
if (n === "SHA-1" || n === "SHA-256" || n === "SHA-384" || n === "SHA-512") return n;
throw new DOMException("Unsupported hash algorithm: " + (h && (h.name || h)), "NotSupportedError");
}
const hashBlockSize = (hash) => (hash === "SHA-384" || hash === "SHA-512" ? 128 : 64);
function b64urlToBytes(s) {
s = String(s).replace(/-/g, "+").replace(/_/g, "/");
while (s.length % 4) s += "=";
const bin = atob(s);
const out = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
return out;
}
function bytesToB64url(bytes) {
let bin = "";
for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
// Run an op, converting a Rust-side failure (bad GCM tag, bad CBC padding)
// into the OperationError the WebCrypto spec requires. DOMExceptions we raise
// ourselves pass through unchanged.
function runOp(fn) {
try { return fn(); }
catch (e) {
if (e instanceof DOMException) throw e;
throw new DOMException(String((e && e.message) || e), "OperationError");
}
}
function keyAlgorithmFor(alg, bytes) {
switch (alg.name) {
case "HMAC":
return { name: "HMAC", hash: { name: normalizeHash(alg.hash) }, length: bytes.length * 8 };
case "AES-CTR": case "AES-CBC": case "AES-GCM": case "AES-KW":
if (bytes.length !== 16 && bytes.length !== 24 && bytes.length !== 32) {
throw new DOMException("AES key data must be 128, 192, or 256 bits", "DataError");
}
return { name: alg.name, length: bytes.length * 8 };
case "PBKDF2": return { name: "PBKDF2" };
case "HKDF": return { name: "HKDF" };
default:
throw new DOMException("Unsupported key algorithm: " + alg.name, "NotSupportedError");
}
}
const subtle = {
async digest(algorithm, data) {
const name = (typeof algorithm === "string" ? algorithm : algorithm && algorithm.name || "").toUpperCase().replace("_", "-");
if (name !== "SHA-1" && name !== "SHA-256" && name !== "SHA-384" && name !== "SHA-512" &&
name !== "SHA-512/224" && name !== "SHA-512/256") {
throw new DOMException("Unrecognized algorithm name", "NotSupportedError");
}
return bufferOf(Deno.core.ops.op_subtle_digest(name, toBytes(data)));
},
async importKey(format, keyData, algorithm, extractable, keyUsages) {
const alg = normalizeAlgo(algorithm);
let bytes;
if (format === "raw") {
bytes = toBytes(keyData);
} else if (format === "jwk") {
if (!keyData || keyData.kty !== "oct" || typeof keyData.k !== "string") {
throw new DOMException("Only symmetric 'oct' JWK keys are supported", "NotSupportedError");
}
bytes = b64urlToBytes(keyData.k);
} else {
throw new DOMException("Only 'raw' and symmetric 'jwk' key formats are supported", "NotSupportedError");
}
return makeKey("secret", extractable, keyAlgorithmFor(alg, bytes), keyUsages, bytes);
},
async exportKey(format, key) {
const bytes = keyBytes(key);
if (!key.extractable) throw new DOMException("Key is not extractable", "InvalidAccessError");
if (format === "raw") return bufferOf(bytes);
if (format === "jwk") {
const jwk = { kty: "oct", k: bytesToB64url(bytes), ext: key.extractable, key_ops: key.usages.slice() };
if (key.algorithm.name && key.algorithm.name.indexOf("AES-") === 0) {
jwk.alg = "A" + (bytes.length * 8) + key.algorithm.name.slice(4);
} else if (key.algorithm.name === "HMAC") {
jwk.alg = "HS" + key.algorithm.hash.name.slice(4);
}
return jwk;
}
throw new DOMException("Only 'raw' and 'jwk' export is supported", "NotSupportedError");
},
async generateKey(algorithm, extractable, keyUsages) {
const alg = normalizeAlgo(algorithm);
if (alg.name === "HMAC") {
const hash = normalizeHash(alg.hash);
const len = alg.length ? Math.ceil(alg.length / 8) : hashBlockSize(hash);
const bytes = Deno.core.ops.op_random_bytes(len);
return makeKey("secret", extractable, { name: "HMAC", hash: { name: hash }, length: len * 8 }, keyUsages, bytes);
}
if (alg.name === "AES-CTR" || alg.name === "AES-CBC" || alg.name === "AES-GCM" || alg.name === "AES-KW") {
if (alg.length !== 128 && alg.length !== 192 && alg.length !== 256) {
throw new DOMException("AES key length must be 128, 192, or 256 bits", "OperationError");
}
const bytes = Deno.core.ops.op_random_bytes(alg.length / 8);
return makeKey("secret", extractable, { name: alg.name, length: alg.length }, keyUsages, bytes);
}
throw new DOMException("generateKey does not support " + alg.name, "NotSupportedError");
},
async sign(algorithm, key, data) {
const alg = normalizeAlgo(algorithm);
const bytes = keyBytes(key);
if (alg.name === "HMAC") {
const hash = key.algorithm && key.algorithm.hash ? key.algorithm.hash.name : normalizeHash(alg.hash);
return bufferOf(runOp(() => Deno.core.ops.op_subtle_hmac(hash, bytes, toBytes(data))));
}
throw new DOMException("sign does not support " + alg.name, "NotSupportedError");
},
async verify(algorithm, key, signature, data) {
const alg = normalizeAlgo(algorithm);
const bytes = keyBytes(key);
if (alg.name === "HMAC") {
const hash = key.algorithm && key.algorithm.hash ? key.algorithm.hash.name : normalizeHash(alg.hash);
const mac = runOp(() => Deno.core.ops.op_subtle_hmac(hash, bytes, toBytes(data)));
const sig = toBytes(signature);
if (sig.length !== mac.length) return false;
let diff = 0;
for (let i = 0; i < mac.length; i++) diff |= mac[i] ^ sig[i];
return diff === 0;
}
throw new DOMException("verify does not support " + alg.name, "NotSupportedError");
},
async encrypt(algorithm, key, data) { return aesCipher(true, algorithm, key, data); },
async decrypt(algorithm, key, data) { return aesCipher(false, algorithm, key, data); },
async deriveBits(algorithm, baseKey, length) {
const alg = normalizeAlgo(algorithm);
const bytes = keyBytes(baseKey);
const lenBytes = Math.ceil((length || 0) / 8);
if (alg.name === "PBKDF2") {
const hash = normalizeHash(alg.hash);
const salt = toBytes(alg.salt);
const iterations = alg.iterations >>> 0;
return bufferOf(runOp(() => Deno.core.ops.op_subtle_pbkdf2(hash, bytes, salt, iterations, lenBytes)));
}
if (alg.name === "HKDF") {
const hash = normalizeHash(alg.hash);
const salt = alg.salt != null ? toBytes(alg.salt) : new Uint8Array(0);
const info = alg.info != null ? toBytes(alg.info) : new Uint8Array(0);
return bufferOf(runOp(() => Deno.core.ops.op_subtle_hkdf(hash, bytes, salt, info, lenBytes)));
}
throw new DOMException("deriveBits does not support " + alg.name, "NotSupportedError");
},
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
const dAlg = normalizeAlgo(derivedKeyAlgorithm);
let bits;
if (dAlg.name === "HMAC") {
bits = dAlg.length || hashBlockSize(normalizeHash(dAlg.hash)) * 8;
} else if (dAlg.name === "AES-CTR" || dAlg.name === "AES-CBC" || dAlg.name === "AES-GCM" || dAlg.name === "AES-KW") {
bits = dAlg.length;
if (bits !== 128 && bits !== 192 && bits !== 256) {
throw new DOMException("AES key length must be 128, 192, or 256 bits", "OperationError");
}
} else {
throw new DOMException("deriveKey does not support deriving " + dAlg.name, "NotSupportedError");
}
const derivedBits = await this.deriveBits(algorithm, baseKey, bits);
return this.importKey("raw", derivedBits, derivedKeyAlgorithm, extractable, keyUsages);
},
async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
const exported = await this.exportKey(format, key);
const bytes = format === "jwk"
? new TextEncoder().encode(JSON.stringify(exported))
: new Uint8Array(exported);
return this.encrypt(wrapAlgorithm, wrappingKey, bytes);
},
async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
const decrypted = await this.decrypt(unwrapAlgorithm, unwrappingKey, wrappedKey);
const keyData = format === "jwk"
? JSON.parse(new TextDecoder().decode(new Uint8Array(decrypted)))
: decrypted;
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
},
};
function aesCipher(encrypt, algorithm, key, data) {
const alg = normalizeAlgo(algorithm);
const bytes = keyBytes(key);
const input = toBytes(data);
if (alg.name === "AES-GCM") {
const iv = toBytes(alg.iv);
const aad = alg.additionalData != null ? toBytes(alg.additionalData) : new Uint8Array(0);
const tagLength = alg.tagLength == null ? 128 : alg.tagLength;
if (tagLength !== 128) {
throw new DOMException("Only a 128-bit AES-GCM tag length is supported", "NotSupportedError");
}
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_gcm(encrypt, bytes, iv, aad, input)));
}
if (alg.name === "AES-CBC") {
const iv = toBytes(alg.iv);
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_cbc(encrypt, bytes, iv, input)));
}
if (alg.name === "AES-CTR") {
const counter = toBytes(alg.counter);
const length = alg.length >>> 0;
return bufferOf(runOp(() => Deno.core.ops.op_subtle_aes_ctr(bytes, counter, length, input)));
}
throw new DOMException((encrypt ? "encrypt" : "decrypt") + " does not support " + alg.name, "NotSupportedError");
}
globalThis.CryptoKey = CryptoKey;
globalThis.SubtleCrypto = function SubtleCrypto() { throw new TypeError("Illegal constructor"); };
Object.setPrototypeOf(subtle, globalThis.SubtleCrypto.prototype);
globalThis.crypto.subtle = subtle;
}
if (typeof DOMRect === 'undefined') {
+241
View File
@@ -1383,6 +1383,240 @@ fn op_subtle_digest(#[string] algorithm: &str, #[buffer] data: &[u8]) -> Vec<u8>
}
}
// ---------------------------------------------------------------------------
// WebCrypto (crypto.subtle) secret-key primitives.
//
// These ops are stateless. The JS shim in bootstrap.js owns the CryptoKey
// objects and their raw key bytes; it hands the bytes plus normalized algorithm
// parameters to these ops for each operation. Only secret-key algorithms live
// here (HMAC, AES-GCM/CBC/CTR, PBKDF2, HKDF); public-key algorithms are rejected
// in the shim. A fallible op returns a JsErrorBox that the shim turns into the
// appropriate DOMException (OperationError for a bad tag or padding, etc.).
// ---------------------------------------------------------------------------
fn crypto_err(msg: impl std::fmt::Display) -> deno_error::JsErrorBox {
deno_error::JsErrorBox::generic(msg.to_string())
}
/// HMAC sign. `hash` is a normalized SubtleCrypto hash name; any key length is
/// accepted (HMAC pads or hashes the key per RFC 2104). Returns the MAC bytes;
/// the shim does the constant-time-insensitive compare for `verify`.
#[op2]
#[buffer]
fn op_subtle_hmac(
#[string] hash: &str,
#[buffer] key: &[u8],
#[buffer] data: &[u8],
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use hmac::{Hmac, Mac};
macro_rules! run {
($d:ty) => {{
let mut mac = Hmac::<$d>::new_from_slice(key).map_err(crypto_err)?;
mac.update(data);
mac.finalize().into_bytes().to_vec()
}};
}
Ok(match hash {
"SHA-1" => run!(sha1::Sha1),
"SHA-256" => run!(sha2::Sha256),
"SHA-384" => run!(sha2::Sha384),
"SHA-512" => run!(sha2::Sha512),
_ => return Err(crypto_err("unsupported HMAC hash")),
})
}
/// AES-GCM encrypt/decrypt. WebCrypto's ciphertext carries the auth tag
/// appended, which is exactly RustCrypto's combined form, so this maps 1:1.
/// Restricted to a 96-bit IV and 128-bit tag (the WebCrypto defaults and the
/// overwhelming majority of real usage); the shim rejects other tag lengths.
#[op2]
#[buffer]
fn op_subtle_aes_gcm(
encrypt: bool,
#[buffer] key: &[u8],
#[buffer] iv: &[u8],
#[buffer] aad: &[u8],
#[buffer] data: &[u8],
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use aes_gcm::aead::{Aead, KeyInit, Payload};
use aes_gcm::aes::{Aes192, Aes256};
use aes_gcm::{AesGcm, Nonce};
type Aes192Gcm = AesGcm<Aes192, aes_gcm::aead::consts::U12>;
type Aes256Gcm = AesGcm<Aes256, aes_gcm::aead::consts::U12>;
if iv.len() != 12 {
return Err(crypto_err("AES-GCM requires a 96-bit (12-byte) IV"));
}
let nonce = Nonce::from_slice(iv);
macro_rules! run {
($ty:ty) => {{
let cipher = <$ty>::new_from_slice(key).map_err(crypto_err)?;
if encrypt {
cipher
.encrypt(nonce, Payload { msg: data, aad })
.map_err(|_| crypto_err("AES-GCM encryption failed"))?
} else {
cipher
.decrypt(nonce, Payload { msg: data, aad })
.map_err(|_| crypto_err("AES-GCM decryption failed: authentication tag mismatch"))?
}
}};
}
Ok(match key.len() {
16 => run!(aes_gcm::Aes128Gcm),
24 => run!(Aes192Gcm),
32 => run!(Aes256Gcm),
_ => return Err(crypto_err("AES-GCM key must be 128, 192, or 256 bits")),
})
}
/// AES-CBC encrypt/decrypt with PKCS#7 padding (the only padding WebCrypto
/// AES-CBC uses) and a 16-byte IV.
#[op2]
#[buffer]
fn op_subtle_aes_cbc(
encrypt: bool,
#[buffer] key: &[u8],
#[buffer] iv: &[u8],
#[buffer] data: &[u8],
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use cbc::cipher::block_padding::Pkcs7;
use cbc::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use cbc::{Decryptor, Encryptor};
if iv.len() != 16 {
return Err(crypto_err("AES-CBC requires a 16-byte IV"));
}
macro_rules! run {
($cipher:ty) => {{
if encrypt {
Encryptor::<$cipher>::new_from_slices(key, iv)
.map_err(crypto_err)?
.encrypt_padded_vec_mut::<Pkcs7>(data)
} else {
Decryptor::<$cipher>::new_from_slices(key, iv)
.map_err(crypto_err)?
.decrypt_padded_vec_mut::<Pkcs7>(data)
.map_err(|_| crypto_err("AES-CBC decryption failed: invalid padding"))?
}
}};
}
Ok(match key.len() {
16 => run!(aes::Aes128),
24 => run!(aes::Aes192),
32 => run!(aes::Aes256),
_ => return Err(crypto_err("AES-CBC key must be 128, 192, or 256 bits")),
})
}
/// AES-CTR. Encrypt and decrypt are the same keystream XOR. `counter_length` is
/// the WebCrypto counter width in bits; it selects the RustCrypto CTR flavor so
/// only the low `counter_length` bits of the 16-byte block increment.
#[op2]
#[buffer]
fn op_subtle_aes_ctr(
#[buffer] key: &[u8],
#[buffer] counter: &[u8],
counter_length: u32,
#[buffer] data: &[u8],
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use ctr::cipher::{KeyIvInit, StreamCipher};
if counter.len() != 16 {
return Err(crypto_err("AES-CTR requires a 16-byte counter block"));
}
let mut buf = data.to_vec();
macro_rules! run {
($ty:ty) => {{
<$ty>::new_from_slices(key, counter)
.map_err(crypto_err)?
.apply_keystream(&mut buf);
}};
}
macro_rules! by_key {
($flavor:ident) => {
match key.len() {
16 => run!(ctr::$flavor<aes::Aes128>),
24 => run!(ctr::$flavor<aes::Aes192>),
32 => run!(ctr::$flavor<aes::Aes256>),
_ => return Err(crypto_err("AES-CTR key must be 128, 192, or 256 bits")),
}
};
}
match counter_length {
128 => by_key!(Ctr128BE),
64 => by_key!(Ctr64BE),
32 => by_key!(Ctr32BE),
_ => return Err(crypto_err("AES-CTR supports counter lengths of 32, 64, or 128 bits")),
}
Ok(buf)
}
/// PBKDF2 key derivation. `length` is the derived-bits output in bytes.
#[op2]
#[buffer]
fn op_subtle_pbkdf2(
#[string] hash: &str,
#[buffer] password: &[u8],
#[buffer] salt: &[u8],
iterations: u32,
length: u32,
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use pbkdf2::pbkdf2_hmac;
let mut dk = vec![0u8; length as usize];
match hash {
"SHA-1" => pbkdf2_hmac::<sha1::Sha1>(password, salt, iterations, &mut dk),
"SHA-256" => pbkdf2_hmac::<sha2::Sha256>(password, salt, iterations, &mut dk),
"SHA-384" => pbkdf2_hmac::<sha2::Sha384>(password, salt, iterations, &mut dk),
"SHA-512" => pbkdf2_hmac::<sha2::Sha512>(password, salt, iterations, &mut dk),
_ => return Err(crypto_err("unsupported PBKDF2 hash")),
}
Ok(dk)
}
/// HKDF key derivation. `length` is the output length in bytes. An empty salt
/// behaves as RFC 5869 specifies (HMAC zero-pads it to the block size, which is
/// what browsers do).
#[op2]
#[buffer]
fn op_subtle_hkdf(
#[string] hash: &str,
#[buffer] ikm: &[u8],
#[buffer] salt: &[u8],
#[buffer] info: &[u8],
length: u32,
) -> Result<Vec<u8>, deno_error::JsErrorBox> {
use hkdf::Hkdf;
let mut okm = vec![0u8; length as usize];
macro_rules! run {
($d:ty) => {
Hkdf::<$d>::new(Some(salt), ikm)
.expand(info, &mut okm)
.map_err(|_| crypto_err("HKDF: requested key length is too long"))?
};
}
match hash {
"SHA-1" => run!(sha1::Sha1),
"SHA-256" => run!(sha2::Sha256),
"SHA-384" => run!(sha2::Sha384),
"SHA-512" => run!(sha2::Sha512),
_ => return Err(crypto_err("unsupported HKDF hash")),
}
Ok(okm)
}
/// Fill `len` bytes from the OS CSPRNG. Backs `crypto.getRandomValues`,
/// `crypto.randomUUID`, and `generateKey`, replacing the old Math.random shim
/// (which was neither uniform across typed-array widths nor cryptographically
/// random, and was a fingerprinting tell).
#[op2]
#[buffer]
fn op_random_bytes(len: u32) -> Result<Vec<u8>, deno_error::JsErrorBox> {
let mut buf = vec![0u8; len as usize];
getrandom::getrandom(&mut buf).map_err(|e| crypto_err(format!("getrandom failed: {e}")))?;
Ok(buf)
}
/// Serialize a parsed URL into the WHATWG IDL component shape consumed by the
/// `URL` class in bootstrap.js. Getters read these fields directly so no op
/// call happens per property access.
@@ -1600,6 +1834,13 @@ pub fn build_extension() -> Extension {
op_sleep(),
op_binding_called(),
op_subtle_digest(),
op_subtle_hmac(),
op_subtle_aes_gcm(),
op_subtle_aes_cbc(),
op_subtle_aes_ctr(),
op_subtle_pbkdf2(),
op_subtle_hkdf(),
op_random_bytes(),
op_url_parse(),
op_url_set(),
op_url_resolve(),