Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ features = [
"Win32_System_Kernel",
"Win32_System_EventLog",
"Win32_Security",
"Win32_Security_Cryptography",
"Win32_System_Ole",
"Win32_System_Threading",
"Win32_System_WinRT",
Expand Down
1 change: 1 addition & 0 deletions runtime/src/global_fns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4403,6 +4403,7 @@ pub(crate) fn init_async_helpers(
register!("__tns_uptime", handle_tns_uptime);
register!("__nsUUID", handle_ns_uuid);
register!("__nsRandomFill", handle_ns_random_fill);
register!("__nsSubtle", crate::subtle_v8::subtle);
register!("__nsIsUiThread", handle_is_ui_thread);
register!("__nsThreadInfo", handle_thread_info);
register!("__nsGetLastJsError", handle_get_last_js_error);
Expand Down
54 changes: 54 additions & 0 deletions runtime/src/interop_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2854,6 +2854,60 @@ fn crypto_random_values_and_uuid() {
);
}

#[test]
fn crypto_subtle() {
run_js_assert(
"crypto_subtle",
r#"
const eq = (actual, expected, what) => {
const a = JSON.stringify(actual), e = JSON.stringify(expected);
if (a !== e) throw new Error(`${what}: expected ${e}, got ${a}`);
};
const hex = (b) => Array.from(new Uint8Array(b), (x) => x.toString(16).padStart(2, '0')).join('');
const bytes = (h) => new Uint8Array(h.match(/../g).map((x) => parseInt(x, 16)));
const utf8 = (s) => new TextEncoder().encode(s);
const rejects = (p) => p.then(() => 'resolved', (e) => e.name);
return (async () => {
const s = crypto.subtle;
eq(hex(await s.digest('SHA-256', utf8('abc'))), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 'digest');
eq(await rejects(s.digest('MD5', utf8('abc'))), 'NotSupportedError', 'unknown digest');
eq(await rejects(s.digest('SHA-256', 'abc')), 'TypeError', 'digest of a string');

const hmac = { name: 'HMAC', hash: 'SHA-256' };
const jefe = await s.importKey('raw', utf8('Jefe'), hmac, true, ['sign', 'verify']);
eq([jefe instanceof CryptoKey, jefe.type, jefe.extractable, jefe.usages, jefe.algorithm.length], [true, 'secret', true, ['sign', 'verify'], 32], 'CryptoKey');
const tag = await s.sign('HMAC', jefe, utf8('what do ya want for nothing?'));
eq(hex(tag), '5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843', 'HMAC RFC 4231');
eq(await s.verify('HMAC', jefe, tag, utf8('what do ya want for nothing?')), true, 'verify');
eq(await s.verify('HMAC', jefe, tag, utf8('tampered')), false, 'verify tampered');
const jwk = await s.exportKey('jwk', jefe);
eq([jwk.kty, jwk.alg, jwk.k], ['oct', 'HS256', 'SmVmZQ'], 'jwk export');
eq(await rejects(s.generateKey(hmac, false, ['encrypt'])), 'SyntaxError', 'bad usage');

const aes = await s.importKey('raw', bytes('feffe9928665731c6d6a8f9467308308'), 'AES-GCM', false, ['encrypt', 'decrypt']);
const gcm = { name: 'AES-GCM', iv: bytes('cafebabefacedbaddecaf888'), additionalData: bytes('feedfacedeadbeeffeedfacedeadbeefabaddad2') };
const plain = bytes('d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39');
const sealed = await s.encrypt(gcm, aes, plain);
eq(hex(sealed).slice(-32), '5bc94fbc3221a5db94fae95ae7121a47', 'AES-GCM tag');
eq(hex(await s.decrypt(gcm, aes, sealed)), hex(plain), 'AES-GCM round trip');
const broken = new Uint8Array(sealed);
broken[0] ^= 1;
eq(await rejects(s.decrypt(gcm, aes, broken)), 'OperationError', 'AES-GCM tampered');

const password = await s.importKey('raw', utf8('password'), 'PBKDF2', false, ['deriveBits', 'deriveKey']);
const pbkdf2 = { name: 'PBKDF2', hash: 'SHA-1', salt: utf8('salt'), iterations: 4096 };
eq(hex(await s.deriveBits(pbkdf2, password, 160)), '4b007901b765489abead49d926f721d065a429c1', 'PBKDF2 RFC 6070');
const derived = await s.deriveKey(pbkdf2, password, { name: 'AES-GCM', length: 256 }, true, ['encrypt']);
eq([derived.algorithm.name, (await s.exportKey('raw', derived)).byteLength], ['AES-GCM', 32], 'deriveKey');

const ikm = await s.importKey('raw', bytes('0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b'), 'HKDF', false, ['deriveBits']);
const hkdf = { name: 'HKDF', hash: 'SHA-256', salt: bytes('000102030405060708090a0b0c'), info: bytes('f0f1f2f3f4f5f6f7f8f9') };
eq(hex(await s.deriveBits(hkdf, ikm, 336)), '3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865', 'HKDF RFC 5869');
})();
"#,
);
}

#[test]
fn delegate_arguments_must_be_delegates() {
run_js_assert(
Expand Down
25 changes: 22 additions & 3 deletions runtime/src/js/crypto.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,29 @@
// Web Crypto's random source, filled from the OS RNG by `__nsRandomFill`. Hosts that already have
// Web Crypto: the OS RNG via `__nsRandomFill`, `subtle` via `__nsSubtle`. Hosts that already have
// `crypto` (Node, Deno, Bun) keep their own.
(function () {
var fill = globalThis.__nsRandomFill;
if (typeof fill !== 'function') return;
var subtleNative = globalThis.__nsSubtle;
if (typeof fill !== 'function' && typeof subtleNative !== 'function') return;
var crypto = globalThis.crypto || (globalThis.crypto = {});

if (typeof subtleNative === 'function' && !crypto.subtle) {
var CryptoKey = function CryptoKey() {
throw new TypeError('Illegal constructor');
};
var subtle = {};
['digest', 'generateKey', 'importKey', 'exportKey', 'sign', 'verify', 'encrypt', 'decrypt',
'deriveBits', 'deriveKey', 'wrapKey', 'unwrapKey'].forEach(function (op) {
subtle[op] = function () {
var args = Array.prototype.slice.call(arguments);
return new Promise(function (resolve) {
resolve(subtleNative(op, args, CryptoKey.prototype));
});
};
});
crypto.subtle = subtle;
globalThis.CryptoKey = CryptoKey;
}

function isIntegerArray(array) {
return array instanceof Int8Array || array instanceof Uint8Array ||
array instanceof Uint8ClampedArray || array instanceof Int16Array ||
Expand All @@ -14,7 +33,7 @@
(array instanceof BigInt64Array || array instanceof BigUint64Array));
}

if (typeof crypto.getRandomValues !== 'function') {
if (typeof fill === 'function' && typeof crypto.getRandomValues !== 'function') {
crypto.getRandomValues = function getRandomValues(array) {
var prefix = "Failed to execute 'getRandomValues' on 'Crypto': ";
if (!isIntegerArray(array)) {
Expand Down
3 changes: 3 additions & 0 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ mod property_call;
#[cfg(feature = "classic")]
mod proxy_manifest_loader;
pub mod source_protect;
mod subtle;
#[cfg(feature = "classic")]
mod subtle_v8;
mod struct_reference;
#[cfg(feature = "classic")]
mod struct_value;
Expand Down
3 changes: 3 additions & 0 deletions runtime/src/napi_engine/interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,9 @@ pub fn install_interop(env: &Env) -> napi::Result<()> {
})?;
global.set_named_property("__nsRandomFill", random_fn)?;

let subtle_fn = env.create_function_from_closure("__nsSubtle", crate::napi_engine::subtle::call)?;
global.set_named_property("__nsSubtle", subtle_fn)?;

let func_ctor: JsFunction = global.get_named_property("Function")?;
let sources = [
INTEROP_HELPERS_JS,
Expand Down
1 change: 1 addition & 0 deletions runtime/src/napi_engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub mod module_natives;
pub mod module_runner;
pub mod ns_hostobject;
pub mod ns_proxy;
pub mod subtle;
pub mod timers;
pub mod url;
pub mod value;
Expand Down
192 changes: 192 additions & 0 deletions runtime/src/napi_engine/subtle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
//! `crypto.subtle` for napi engines: `__nsSubtle(op, args, CryptoKey.prototype)`.

use crate::napi_engine::value::buffer_data;
use crate::subtle::{dispatch, Js as Engine, Key};
use napi::{
sys, CallContext, Env, JsFunction, JsObject, JsUnknown, NapiRaw, NapiValue, Property, PropertyAttributes,
Status, ValueType,
};
use serde_json::Value;

struct Napi<'e> {
env: &'e Env,
proto: sys::napi_value,
}

impl Napi<'_> {
fn value(&self, raw: sys::napi_value) -> JsUnknown {
unsafe { JsUnknown::from_raw_unchecked(self.env.raw(), raw) }
}

fn object(&self, raw: sys::napi_value) -> Option<JsObject> {
matches!(self.value(raw).get_type(), Ok(ValueType::Object | ValueType::Function))
.then(|| unsafe { JsObject::from_raw_unchecked(self.env.raw(), raw) })
}

fn call_global(&self, object: &str, method: &str, arg: sys::napi_value) -> Option<JsUnknown> {
let target: JsObject = self.env.get_global().ok()?.get_named_property(object).ok()?;
let f: JsFunction = target.get_named_property(method).ok()?;
let out = f.call(Some(&target), &[self.value(arg)]);
if out.is_err() {
let mut exception = std::ptr::null_mut();
unsafe { sys::napi_get_and_clear_last_exception(self.env.raw(), &mut exception) };
}
out.ok()
}
}

impl Engine for Napi<'_> {
type V = sys::napi_value;

fn undefined(&mut self) -> Self::V {
self.env.get_undefined().map(|u| unsafe { u.raw() }).unwrap_or(std::ptr::null_mut())
}

fn is_nullish(&mut self, v: Self::V) -> bool {
matches!(self.value(v).get_type(), Ok(ValueType::Undefined | ValueType::Null))
}

fn string(&mut self, v: Self::V) -> Option<String> {
let value = self.value(v);
if value.get_type().ok()? != ValueType::String {
return None;
}
Some(value.coerce_to_string().ok()?.into_utf8().ok()?.as_str().ok()?.to_owned())
}

fn number(&mut self, v: Self::V) -> Option<f64> {
let value = self.value(v);
if value.get_type().ok()? != ValueType::Number {
return None;
}
value.coerce_to_number().ok()?.get_double().ok()
}

fn boolean(&mut self, v: Self::V) -> bool {
self.value(v).coerce_to_bool().and_then(|b| b.get_value()).unwrap_or(false)
}

fn bytes(&mut self, v: Self::V) -> Option<Vec<u8>> {
let (data, len) = buffer_data(self.env, &self.value(v))?;
Some(if len == 0 || data.is_null() {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(data as *const u8, len).to_vec() }
})
}

fn get(&mut self, v: Self::V, name: &str) -> Self::V {
self.object(v)
.and_then(|o| o.get_named_property::<JsUnknown>(name).ok())
.map(|value| unsafe { value.raw() })
.unwrap_or_else(|| self.undefined())
}

fn array(&mut self, v: Self::V) -> Option<Vec<Self::V>> {
let array = self.object(v)?;
if !array.is_array().ok()? {
return None;
}
(0..array.get_array_length().ok()?)
.map(|i| array.get_element::<JsUnknown>(i).ok().map(|e| unsafe { e.raw() }))
.collect()
}

fn stringify(&mut self, v: Self::V) -> Option<String> {
self.object(v)?;
let text = self.call_global("JSON", "stringify", v)?;
self.string(unsafe { text.raw() })
}

fn key(&mut self, v: Self::V) -> Option<Key> {
let object = self.object(v)?;
self.env.unwrap::<Key>(&object).ok().map(|key| key.clone())
}

fn array_buffer(&mut self, bytes: Vec<u8>) -> Self::V {
self.env
.create_arraybuffer_with_data(bytes)
.map(|b| unsafe { b.into_raw().raw() })
.unwrap_or_else(|_| self.undefined())
}

fn bool_value(&mut self, b: bool) -> Self::V {
self.env.get_boolean(b).map(|b| unsafe { b.raw() }).unwrap_or_else(|_| self.undefined())
}

fn json(&mut self, value: &Value) -> Self::V {
let Ok(text) = self.env.create_string(&value.to_string()) else {
return self.undefined();
};
self.call_global("JSON", "parse", unsafe { text.raw() })
.map(|v| unsafe { v.raw() })
.unwrap_or_else(|| self.undefined())
}

fn crypto_key(&mut self, key: Key) -> Self::V {
let algorithm = self.json(&key.algorithm.to_json());
let created = self.call_global("Object", "create", self.proto);
let mut object = match created.and_then(|o| self.object(unsafe { o.raw() })) {
Some(o) => o,
None => match self.env.create_object() {
Ok(o) => o,
Err(_) => return self.undefined(),
},
};
let built = (|| -> napi::Result<()> {
let mut usages = self.env.create_array_with_length(key.usages.len())?;
for (i, usage) in key.usages.iter().enumerate() {
usages.set_element(i as u32, self.env.create_string(usage)?)?;
}
let kind = self.env.create_string(key.kind())?;
let extractable = self.env.get_boolean(key.extractable)?;
let algorithm = self.value(algorithm);
object.define_properties(&[
Property::new("type")?.with_value(&kind).with_property_attributes(PropertyAttributes::Enumerable),
Property::new("extractable")?
.with_value(&extractable)
.with_property_attributes(PropertyAttributes::Enumerable),
Property::new("algorithm")?
.with_value(&algorithm)
.with_property_attributes(PropertyAttributes::Enumerable),
Property::new("usages")?.with_value(&usages).with_property_attributes(PropertyAttributes::Enumerable),
])?;
self.env.wrap(&mut object, key)
})();
if built.is_err() {
return self.undefined();
}
unsafe { object.raw() }
}
}

fn throw(env: &Env, name: &str, message: &str) -> napi::Error {
let thrown = (|| -> napi::Result<()> {
let global = env.get_global()?;
let text = env.create_string(message)?;
let error = if name == "TypeError" {
let ctor: JsFunction = global.get_named_property("TypeError")?;
ctor.new_instance(&[text])?
} else {
let ctor: JsFunction = global.get_named_property("DOMException")?;
ctor.new_instance(&[text.into_unknown(), env.create_string(name)?.into_unknown()])?
};
env.throw(error)
})();
match thrown {
Ok(()) => napi::Error::new(Status::PendingException, String::new()),
Err(_) => napi::Error::from_reason(format!("{name}: {message}")),
}
}

pub(crate) fn call(ctx: CallContext) -> napi::Result<JsUnknown> {
let op = ctx.get::<JsUnknown>(0)?.coerce_to_string()?.into_utf8()?.as_str()?.to_owned();
let args = unsafe { ctx.get::<JsUnknown>(1)?.raw() };
let proto = unsafe { ctx.get::<JsUnknown>(2)?.raw() };
let mut engine = Napi { env: ctx.env, proto };
let args = engine.array(args).unwrap_or_default();
match dispatch(&mut engine, &op, &args) {
Ok(v) => Ok(unsafe { JsUnknown::from_raw_unchecked(ctx.env.raw(), v) }),
Err(e) => Err(throw(ctx.env, e.name, &e.message)),
}
}
Loading