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:
1 parent
d5d9b7788f
commit
ed75730e2d
4 files changed
+705
-27
No files matched your search
@@ -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"
|
||||
Vendored
+292
-27
@@ -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') {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in new issue
Block a user