Skip to content

Commit

Permalink
Improve cargo feature handling and crypto APIs definition
Browse files Browse the repository at this point in the history
This commit improves the `crypto` feature handling by making the
relationship between crypto and the experimental_event_loop explicit. If
`crypto` is defined, the `experimental_event_loop` feature will also be
enbabled.

Additionally, this commit makes it easier to define the async crypto
APIs by introducing a `crypto.js` file that defines JS bits to make it
easier to return a promise.
  • Loading branch information
saulecabrera committed Jul 8, 2024
1 parent 247e59c commit fa3c661
Show file tree
Hide file tree
Showing 11 changed files with 89 additions and 73 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test-runner:
cargo test --package=javy-runner -- --nocapture

# WPT requires a Javy build with the experimental_event_loop feature to pass
test-wpt: export CORE_FEATURES ?= experimental_event_loop,experimental_crypto
test-wpt: export CORE_FEATURES ?= experimental_event_loop,crypto
test-wpt:
# Can't use a prerequisite here b/c a prequisite will not cause a rebuild of the CLI
$(MAKE) cli
Expand Down
2 changes: 1 addition & 1 deletion crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ javy-config = { workspace = true }

[features]
experimental_event_loop = []
experimental_crypto = []
crypto = ["experimental_event_loop", "javy/crypto"]
16 changes: 13 additions & 3 deletions crates/core/src/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::process;
use anyhow::{anyhow, bail, Error, Result};
use javy::{
from_js_error,
quickjs::{context::EvalOptions, Module, Value},
quickjs::{context::EvalOptions, Error as JSError, Module, Value},
to_js_error, Runtime,
};

Expand All @@ -26,7 +26,12 @@ pub fn run_bytecode(runtime: &Runtime, bytecode: &[u8]) {

if cfg!(feature = "experimental_event_loop") {
// If the experimental event loop is enabled, trigger it.
promise.finish::<Value>().map(|_| ())
let resolved = promise.finish::<Value>();
if let Err(JSError::WouldBlock) = resolved {
Ok(())
} else {
resolved.map(|_| ())
}
} else {
// Else we simply expect the promise to resolve immediately.
match promise.result() {
Expand Down Expand Up @@ -66,7 +71,12 @@ pub fn invoke_function(runtime: &Runtime, fn_module: &str, fn_name: &str) {
if let Some(promise) = value.as_promise() {
if cfg!(feature = "experimental_event_loop") {
// If the experimental event loop is enabled, trigger it.
promise.finish::<Value>().map(|_| ())
let resolved = promise.finish::<Value>();
if let Err(JSError::WouldBlock) = resolved {
Ok(())
} else {
resolved.map(|_| ())
}
} else {
// Else we simply expect the promise to resolve immediately.
match promise.result() {
Expand Down
13 changes: 6 additions & 7 deletions crates/core/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@ use javy_config::Config as SharedConfig;

pub(crate) fn new(shared_config: SharedConfig) -> Result<Runtime> {
let mut config = Config::default();
let config = config
config
.text_encoding(shared_config.contains(SharedConfig::TEXT_ENCODING))
.redirect_stdout_to_stderr(shared_config.contains(SharedConfig::REDIRECT_STDOUT_TO_STDERR))
.javy_stream_io(shared_config.contains(SharedConfig::JAVY_STREAM_IO))
// Due to an issue with our custom serializer and property accesses
// we're disabling this temporarily. It will be enabled once we have a
// fix forward.
.override_json_parse_and_stringify(false)
.javy_json(false)
// For the time being, ship crypto as off by default
// Later, we may enable it with: .crypto(shared_config.contains(SharedConfig::CRYPTO))
// .crypto(cfg!(feature = "experimental_crypto"));
.crypto(true);
.javy_json(false);

Runtime::new(std::mem::take(config))
#[cfg(feature = "crypto")]
config.crypto(shared_config.contains(SharedConfig::CRYPTO));

Runtime::new(std::mem::take(&mut config))
}
6 changes: 4 additions & 2 deletions crates/javy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ quickcheck = "1"
bitflags = { workspace = true }
fastrand = "2.1.0"
simd-json = { version = "0.13.10", optional = true, default-features = false, features = ["big-int-as-float", "serde_impl"] }
sha2 = "0.10.8"
hmac = "0.12.1"
sha2 = { version = "0.10.8", optional = true }
hmac = { version = "0.12.1", optional = true }

[dev-dependencies]
javy-test-macros = { path = "../test-macros/" }
Expand All @@ -42,3 +42,5 @@ messagepack = ["rmp-serde", "serde-transcode"]
# implications of enabling by default (due to the extra dependencies) and also
# because the native implementation is probably fine for most use-cases.
json = ["serde_json", "serde-transcode", "simd-json"]
# Enable support for WinterCG-compatible Crypto APIs
crypto = ["dep:sha2", "dep:hmac"]
17 changes: 17 additions & 0 deletions crates/javy/src/apis/crypto/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(function() {
const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign;

const crypto = {
subtle: {}
};


crypto.subtle.sign = function(obj, key, msg) {
return new Promise((resolve, _) => {
resolve(__javy_cryptoSubtleSign(obj, key, msg));
});
}

globalThis.crypto = crypto;
Reflect.deleteProperty(globalThis, "__javy_cryptoSubtleSign");
})();
78 changes: 29 additions & 49 deletions crates/javy/src/apis/crypto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use crate::quickjs::{context::Intrinsic, qjs, Ctx, CatchResultExt, Function, Object, String as JSString, Value, Promise, function::Func, function::This};
use crate::quickjs::{
context::{EvalOptions, Intrinsic},
qjs, Ctx, Function, String as JSString, Value,
};
use crate::{hold, hold_and_release, to_js_error, val_to_string, Args};
use anyhow::{bail, Error, Result};

use hmac::{Hmac, Mac};
use sha2::Sha256;

/// An implemetation of crypto APIs to optimize fuel.
/// Currently, hmacSHA256 is the only function implemented.
/// A Winter CG compatible implementation of the Crypto API.
/// Currently, the following methods are implemented:
/// * `crypto.subtle.sign`, with HMAC sha256
pub struct Crypto;

impl Intrinsic for Crypto {
Expand All @@ -17,19 +21,17 @@ impl Intrinsic for Crypto {

fn register(this: Ctx<'_>) -> Result<()> {
let globals = this.globals();
let crypto_obj = Object::new(this.clone())?;
let subtle_obj = Object::new(this.clone())?;

subtle_obj.set(
"sign",
globals.set(
"__javy_cryptoSubtleSign",
Function::new(this.clone(), |this, args| {
let (this, args) = hold_and_release!(this, args);
hmac_sha256(hold!(this.clone(), args)).map_err(|e| to_js_error(this, e))
}),
)?;

crypto_obj.set("subtle", subtle_obj)?;
globals.set("crypto", crypto_obj)?;
let mut opts = EvalOptions::default();
opts.strict = false;
this.eval_with_options(include_str!("crypto.js"), opts)?;

Ok::<_, Error>(())
}
Expand All @@ -38,7 +40,7 @@ fn register(this: Ctx<'_>) -> Result<()> {
/// Arg[0] - secret
/// Arg[1] - message
/// returns - hex encoded string of hmac.
fn hmac_sha256(args: Args<'_>) -> Result<Promise<'_>> {
fn hmac_sha256(args: Args<'_>) -> Result<Value<'_>> {
let (ctx, args) = args.release();

if args.len() != 3 {
Expand All @@ -47,54 +49,28 @@ fn hmac_sha256(args: Args<'_>) -> Result<Promise<'_>> {

let protocol = args[0].as_object();

let js_protocol_name: Value = protocol.expect("protocol struct required").get("name").unwrap();
let js_protocol_name: Value = protocol.expect("protocol struct required").get("name")?;
if val_to_string(&ctx, js_protocol_name.clone())? != "HMAC" {
bail!("only name=HMAC supported");
}

let js_protocol_name: Value = protocol.expect("protocol struct required").get("hash").unwrap();
let js_protocol_name: Value = protocol.expect("protocol struct required").get("hash")?;
if val_to_string(&ctx, js_protocol_name.clone())? != "sha-256" {
bail!("only hash=sha-256 supported");
}
let secret = val_to_string(&ctx, args[1].clone())?;
let message = val_to_string(&ctx, args[2].clone())?;

let string_digest = hmac_sha256_result(secret, message);

// Convert result to JSString
let js_string_digest = JSString::from_str(ctx.clone(), &string_digest?)
.map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert result to JSString: {}", e)))?;
//Value::from_string(js_string_digest);
let string = Value::from_string(js_string_digest);
// let promise = Promise::from_value(string)
// .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert value to promise: {}", e)))?;

// let promise = Promise::from_value(string);
Ok(build_promise_from_value(string)?)
}

fn build_promise_from_value(value: Value<'_>) -> Result<Promise<'_>> {
let ctx = value.ctx();
let (promise, resolve, _) = Promise::new(&ctx).unwrap();
let cb = Func::new( || {
"hello world"
});

promise
.get::<_, Function>("then")
.catch(&ctx)
.unwrap()
.call::<_, ()>((This(promise.clone()), cb))
.catch(&ctx)
.unwrap();

return Ok(promise)
let string_digest = hmac_sha256_result(secret, message)?;
let result = JSString::from_str(ctx.clone(), &string_digest)?;
Ok(result.into())
}

/// hmac_sha256_result applies the HMAC sha256 algorithm for signing.
fn hmac_sha256_result(secret: String, message: String) -> Result<String> {
type HmacSha256 = Hmac<Sha256>;
let mut hmac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
let mut hmac =
HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
hmac.update(message.as_bytes());
let result = hmac.finalize();
let code_bytes = result.into_bytes();
Expand All @@ -119,7 +95,6 @@ mod tests {
let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9";
let result = null;
crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message").then(function(sig) { result = sig });
console.log(result);
result === expectedHex;
"#,
)?;
Expand All @@ -134,19 +109,24 @@ mod tests {
let runtime = Runtime::new(Config::default())?;

runtime.context().with(|this| {
let result= this.eval::<Value<'_>, _>(
let result = this.eval::<Value<'_>, _>(
r#"
crypto.subtle;
"#,
);
assert!(result.is_err());
let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err();
assert_eq!("Error:2:21 'crypto' is not defined\n at <eval> (eval_script:2:21)\n", e.to_string());
let e = result
.map_err(|e| from_js_error(this.clone(), e))
.unwrap_err();
assert_eq!(
"Error:2:21 'crypto' is not defined\n at <eval> (eval_script:2:21)\n",
e.to_string()
);
Ok::<_, Error>(())
})?;
Ok(())
}

#[test]
fn test_crypto_digest_with_lossy_input() -> Result<()> {
let mut config = Config::default();
Expand Down
2 changes: 2 additions & 0 deletions crates/javy/src/apis/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
//!
//! Disabled by default.
pub(crate) mod console;
#[cfg(feature = "crypto")]
pub(crate) mod crypto;
#[cfg(feature = "json")]
pub(crate) mod json;
Expand All @@ -65,6 +66,7 @@ pub(crate) mod stream_io;
pub(crate) mod text_encoding;

pub(crate) use console::*;
#[cfg(feature = "crypto")]
pub(crate) use crypto::*;
#[cfg(feature = "json")]
pub(crate) use json::*;
Expand Down
1 change: 1 addition & 0 deletions crates/javy/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ impl Config {

/// Whether the `crypto` intrinsic will be available.
/// Disabled by default.
#[cfg(feature = "crypto")]
pub fn crypto(&mut self, enable: bool) -> &mut Self {
self.intrinsics.set(JSIntrinsics::CRYPTO, enable);
self
Expand Down
7 changes: 5 additions & 2 deletions crates/javy/src/runtime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// use crate::quickjs::JSContextRef;
use super::from_js_error;
use crate::{
apis::{Console, Crypto, NonStandardConsole, Random, StreamIO, TextEncoding},
apis::{Console, NonStandardConsole, Random, StreamIO, TextEncoding},
config::{JSIntrinsics, JavyIntrinsics},
Config,
};
Expand Down Expand Up @@ -146,7 +146,10 @@ impl Runtime {
}

if intrinsics.contains(JSIntrinsics::CRYPTO) {
unsafe { Crypto::add_intrinsic(ctx.as_raw()) }
#[cfg(feature = "crypto")]
unsafe {
crate::apis::Crypto::add_intrinsic(ctx.as_raw())
}
}
});

Expand Down
18 changes: 10 additions & 8 deletions p.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const promiseA = new Promise((resolve, reject) => {
resolve(777);
});
// At this point, "promiseA" is already settled.
async f () => {
await promiseA.then((val) => console.log("asynchronous logging has val:", val));

export async function main() {
const expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9";

const result = await crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message");
console.log(result);
console.log(result === expectedHex);
}
f();
console.log("immediate logging");

await main();

0 comments on commit fa3c661

Please sign in to comment.