From d61a7fa478c8f5eeec5168e02fbc5df5da32a3a3 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Mon, 1 Jul 2024 18:45:29 -0400 Subject: [PATCH 01/22] Add crypto hmac-sha256 --- Cargo.lock | 18 ++++++ crates/core/src/runtime.rs | 3 +- crates/javy/Cargo.toml | 2 + crates/javy/src/apis/cryptox/mod.rs | 92 +++++++++++++++++++++++++++++ crates/javy/src/apis/mod.rs | 2 + crates/javy/src/config.rs | 9 +++ crates/javy/src/runtime.rs | 9 ++- 7 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 crates/javy/src/apis/cryptox/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 88341a78..ef9185f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -883,6 +883,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1276,6 +1277,15 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -1568,6 +1578,7 @@ dependencies = [ "anyhow", "bitflags", "fastrand", + "hmac", "javy-test-macros", "quickcheck", "rmp-serde", @@ -1577,6 +1588,7 @@ dependencies = [ "serde", "serde-transcode", "serde_json", + "sha2", "simd-json", ] @@ -2862,6 +2874,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swc_atoms" version = "0.6.7" diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index b973bd7b..ee366cd8 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -12,7 +12,8 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // we're disabling this temporarily. It will be enabled once we have a // fix forward. .override_json_parse_and_stringify(false) - .javy_json(false); + .javy_json(false) + .javy_cryptox(true); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/Cargo.toml b/crates/javy/Cargo.toml index 3bbe2e94..8544d469 100644 --- a/crates/javy/Cargo.toml +++ b/crates/javy/Cargo.toml @@ -24,6 +24,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" [dev-dependencies] javy-test-macros = { path = "../test-macros/" } diff --git a/crates/javy/src/apis/cryptox/mod.rs b/crates/javy/src/apis/cryptox/mod.rs new file mode 100644 index 00000000..b7143376 --- /dev/null +++ b/crates/javy/src/apis/cryptox/mod.rs @@ -0,0 +1,92 @@ +use crate::quickjs::{context::Intrinsic, qjs, Ctx, Object, String as JSString, Value, Function}; +use crate::{ + hold, hold_and_release, val_to_string, + to_js_error, Args +}; +use anyhow::{bail, Error, Result}; + +use sha2::Sha256; +use hmac::{Hmac, Mac}; + +/// An implemetation of crypto APIs to optimize fuel. +/// Currently, hmacSHA256 is the only function implemented. +pub struct Cryptox; + +impl Intrinsic for Cryptox { + unsafe fn add_intrinsic(ctx: std::ptr::NonNull) { + register(Ctx::from_raw(ctx)).expect("`Cryptox` APIs to succeed") + } +} + +fn register(this: Ctx<'_>) -> Result<()> { + let globals = this.globals(); + + // let crypto_obj = Object::new(cx)?; + let crypto_obj = Object::new(this.clone())?; + + crypto_obj.set( + "hmacSHA256", + 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)) + }), + )?; + + globals.set("cryptox", crypto_obj)?; + + Ok::<_, Error>(()) +} +/// hmac_sha256 applies the HMAC algorithm using sha256 for hashing. +/// Arg[0] - secret +/// Arg[1] - message +/// returns - hex encoded string of hmac. +fn hmac_sha256(args: Args<'_>) -> Result> { + let (cx, args) = args.release(); + + if args.len() != 2 { + bail!("Wrong number of arguments. Expected 2. Got {}", args.len()); + } + + let js_string_secret = val_to_string(&cx, args[0].clone())?; + let js_string_message = val_to_string(&cx, args[1].clone())?; + + /// Create alias for HMAC-SHA256 + type HmacSha256 = Hmac; + + let mut mac = HmacSha256::new_from_slice(&js_string_secret.as_bytes()) + .expect("HMAC can take key of any size"); + mac.update(&js_string_message.as_bytes()); + + let result = mac.finalize(); + let code_bytes = result.into_bytes(); + let code : String = format!("{code_bytes:x}"); + let js_string = JSString::from_str(cx, &code); + Ok(Value::from_string(js_string?)) +} + +#[cfg(test)] +mod tests { + use crate::{quickjs::Value, Config, Runtime}; + use anyhow::{Error, Result}; + + #[test] + fn test_text_encoder_decoder() -> Result<()> { + let mut config = Config::default(); + config.javy_cryptox(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result: Value<'_> = this.eval( + r#" + let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; + let result = cryptox.hmacSHA256("my secret and secure key", "input message"); + expectedHex === result; + "#, + )?; + + assert!(result.as_bool().unwrap()); + Ok::<_, Error>(()) + })?; + Ok(()) + } +} \ No newline at end of file diff --git a/crates/javy/src/apis/mod.rs b/crates/javy/src/apis/mod.rs index 67979d7e..9b9d8202 100644 --- a/crates/javy/src/apis/mod.rs +++ b/crates/javy/src/apis/mod.rs @@ -62,6 +62,7 @@ pub(crate) mod json; pub(crate) mod random; pub(crate) mod stream_io; pub(crate) mod text_encoding; +pub(crate) mod cryptox; pub(crate) use console::*; #[cfg(feature = "json")] @@ -69,3 +70,4 @@ pub(crate) use json::*; pub(crate) use random::*; pub(crate) use stream_io::*; pub(crate) use text_encoding::*; +pub(crate) use cryptox::*; diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index 6f542642..f482ecbe 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -38,6 +38,7 @@ bitflags! { pub(crate) struct JavyIntrinsics: u32 { const STREAM_IO = 1; const JSON = 1 << 1; + const CRYPTOX = 1 << 2; } } @@ -179,6 +180,14 @@ impl Config { self } + /// Whether the `Javy.CRYPTOX` intrinsic will be available. + /// Enabled by default. + // #[cfg(feature = "cryptox")] + pub fn javy_cryptox(&mut self, enable: bool) -> &mut Self { + self.javy_intrinsics.set(JavyIntrinsics::CRYPTOX, enable); + self + } + /// Enables whether the output of console.log will be redirected to /// `stderr`. pub fn redirect_stdout_to_stderr(&mut self, enable: bool) -> &mut Self { diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 22d2f9ab..c94e113f 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -1,7 +1,7 @@ // use crate::quickjs::JSContextRef; use super::from_js_error; use crate::{ - apis::{Console, NonStandardConsole, Random, StreamIO, TextEncoding}, + apis::{Cryptox, Console, NonStandardConsole, Random, StreamIO, TextEncoding}, config::{JSIntrinsics, JavyIntrinsics}, Config, }; @@ -144,6 +144,13 @@ impl Runtime { JavyJson::add_intrinsic(ctx.as_raw()) } } + + if javy_intrinsics.contains(JavyIntrinsics::CRYPTOX) { + // #[cfg(feature = "cryptox")] + unsafe { + Cryptox::add_intrinsic(ctx.as_raw()) + } + } }); Ok(ManuallyDrop::new(context)) From da1aa5041d28b2b1f1221a55d5cb3430231b48cf Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 2 Jul 2024 08:43:35 -0400 Subject: [PATCH 02/22] Use configuration for loading cryptox --- crates/config/src/lib.rs | 2 ++ crates/core/src/runtime.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 8836b9e2..f1dcbbfd 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -31,6 +31,7 @@ bitflags! { const JAVY_STREAM_IO = 1 << 2; const REDIRECT_STDOUT_TO_STDERR = 1 << 3; const TEXT_ENCODING = 1 << 4; + const JAVY_CRYPTOX = 1 << 5; } } @@ -44,5 +45,6 @@ mod tests { assert!(Config::JAVY_STREAM_IO == Config::from_bits(1 << 2).unwrap()); assert!(Config::REDIRECT_STDOUT_TO_STDERR == Config::from_bits(1 << 3).unwrap()); assert!(Config::TEXT_ENCODING == Config::from_bits(1 << 4).unwrap()); + assert!(Config::JAVY_CRYPTOX == Config::from_bits(1 << 5).unwrap()); } } diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index ee366cd8..1e52c74b 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -13,7 +13,7 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // fix forward. .override_json_parse_and_stringify(false) .javy_json(false) - .javy_cryptox(true); + .javy_cryptox(shared_config.contains(SharedConfig::JAVY_CRYPTOX)); Runtime::new(std::mem::take(config)) } From 56b9caab2c6d7d4f0f3dbc4ff02243f549171854 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 2 Jul 2024 10:34:14 -0400 Subject: [PATCH 03/22] Address linting feedback HmacSha256::new_from_slice(&js_string_secret.as_bytes()) => HmacSha256::new_from_slice(js_string_secret.as_bytes()) No need to pass in address here. Same for js_string_message. --- crates/javy/src/apis/cryptox/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/javy/src/apis/cryptox/mod.rs b/crates/javy/src/apis/cryptox/mod.rs index b7143376..862db964 100644 --- a/crates/javy/src/apis/cryptox/mod.rs +++ b/crates/javy/src/apis/cryptox/mod.rs @@ -53,9 +53,9 @@ fn hmac_sha256(args: Args<'_>) -> Result> { /// Create alias for HMAC-SHA256 type HmacSha256 = Hmac; - let mut mac = HmacSha256::new_from_slice(&js_string_secret.as_bytes()) + let mut mac = HmacSha256::new_from_slice(js_string_secret.as_bytes()) .expect("HMAC can take key of any size"); - mac.update(&js_string_message.as_bytes()); + mac.update(js_string_message.as_bytes()); let result = mac.finalize(); let code_bytes = result.into_bytes(); From 59dd9dccb7cff126ea6eff48d7ecf722119d8a9a Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 2 Jul 2024 11:09:04 -0400 Subject: [PATCH 04/22] Switch from cryptox to crypto namespace Also drop javy_ prefix in anticipation of switching to winter cg. --- crates/config/src/lib.rs | 4 ++-- crates/core/src/runtime.rs | 2 +- crates/javy/src/apis/{cryptox => crypto}/mod.rs | 15 +++++++-------- crates/javy/src/apis/mod.rs | 4 ++-- crates/javy/src/config.rs | 10 +++++----- crates/javy/src/runtime.rs | 8 ++++---- 6 files changed, 21 insertions(+), 22 deletions(-) rename crates/javy/src/apis/{cryptox => crypto}/mod.rs (89%) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index f1dcbbfd..2fdc6133 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -31,7 +31,7 @@ bitflags! { const JAVY_STREAM_IO = 1 << 2; const REDIRECT_STDOUT_TO_STDERR = 1 << 3; const TEXT_ENCODING = 1 << 4; - const JAVY_CRYPTOX = 1 << 5; + const CRYPTO = 1 << 5; } } @@ -45,6 +45,6 @@ mod tests { assert!(Config::JAVY_STREAM_IO == Config::from_bits(1 << 2).unwrap()); assert!(Config::REDIRECT_STDOUT_TO_STDERR == Config::from_bits(1 << 3).unwrap()); assert!(Config::TEXT_ENCODING == Config::from_bits(1 << 4).unwrap()); - assert!(Config::JAVY_CRYPTOX == Config::from_bits(1 << 5).unwrap()); + assert!(Config::CRYPTO == Config::from_bits(1 << 5).unwrap()); } } diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 1e52c74b..416e39d0 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -13,7 +13,7 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // fix forward. .override_json_parse_and_stringify(false) .javy_json(false) - .javy_cryptox(shared_config.contains(SharedConfig::JAVY_CRYPTOX)); + .crypto(shared_config.contains(SharedConfig::CRYPTO)); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/src/apis/cryptox/mod.rs b/crates/javy/src/apis/crypto/mod.rs similarity index 89% rename from crates/javy/src/apis/cryptox/mod.rs rename to crates/javy/src/apis/crypto/mod.rs index 862db964..d42e3764 100644 --- a/crates/javy/src/apis/cryptox/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -10,14 +10,13 @@ use hmac::{Hmac, Mac}; /// An implemetation of crypto APIs to optimize fuel. /// Currently, hmacSHA256 is the only function implemented. -pub struct Cryptox; +pub struct Crypto; -impl Intrinsic for Cryptox { +impl Intrinsic for Crypto { unsafe fn add_intrinsic(ctx: std::ptr::NonNull) { - register(Ctx::from_raw(ctx)).expect("`Cryptox` APIs to succeed") + register(Ctx::from_raw(ctx)).expect("`Crypto` APIs to succeed") } } - fn register(this: Ctx<'_>) -> Result<()> { let globals = this.globals(); @@ -32,7 +31,7 @@ fn register(this: Ctx<'_>) -> Result<()> { }), )?; - globals.set("cryptox", crypto_obj)?; + globals.set("crypto", crypto_obj)?; Ok::<_, Error>(()) } @@ -72,14 +71,14 @@ mod tests { #[test] fn test_text_encoder_decoder() -> Result<()> { let mut config = Config::default(); - config.javy_cryptox(true); + config.crypto(true); let runtime = Runtime::new(config)?; runtime.context().with(|this| { let result: Value<'_> = this.eval( r#" let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; - let result = cryptox.hmacSHA256("my secret and secure key", "input message"); + let result = crypto.hmacSHA256("my secret and secure key", "input message"); expectedHex === result; "#, )?; @@ -89,4 +88,4 @@ mod tests { })?; Ok(()) } -} \ No newline at end of file +} diff --git a/crates/javy/src/apis/mod.rs b/crates/javy/src/apis/mod.rs index 9b9d8202..f1b023d9 100644 --- a/crates/javy/src/apis/mod.rs +++ b/crates/javy/src/apis/mod.rs @@ -62,7 +62,7 @@ pub(crate) mod json; pub(crate) mod random; pub(crate) mod stream_io; pub(crate) mod text_encoding; -pub(crate) mod cryptox; +pub(crate) mod crypto; pub(crate) use console::*; #[cfg(feature = "json")] @@ -70,4 +70,4 @@ pub(crate) use json::*; pub(crate) use random::*; pub(crate) use stream_io::*; pub(crate) use text_encoding::*; -pub(crate) use cryptox::*; +pub(crate) use crypto::*; diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index f482ecbe..c678f53c 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -19,6 +19,7 @@ bitflags! { const OPERATORS = 1 << 12; const BIGNUM_EXTENSION = 1 << 13; const TEXT_ENCODING = 1 << 14; + const CRYPTO = 1 << 15; } } @@ -38,7 +39,6 @@ bitflags! { pub(crate) struct JavyIntrinsics: u32 { const STREAM_IO = 1; const JSON = 1 << 1; - const CRYPTOX = 1 << 2; } } @@ -180,11 +180,11 @@ impl Config { self } - /// Whether the `Javy.CRYPTOX` intrinsic will be available. + /// Whether the `crypto` intrinsic will be available. /// Enabled by default. - // #[cfg(feature = "cryptox")] - pub fn javy_cryptox(&mut self, enable: bool) -> &mut Self { - self.javy_intrinsics.set(JavyIntrinsics::CRYPTOX, enable); + // #[cfg(feature = "crypto")] + pub fn crypto(&mut self, enable: bool) -> &mut Self { + self.intrinsics.set(JSIntrinsics::CRYPTO, enable); self } diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index c94e113f..cdcb8d4a 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -1,7 +1,7 @@ // use crate::quickjs::JSContextRef; use super::from_js_error; use crate::{ - apis::{Cryptox, Console, NonStandardConsole, Random, StreamIO, TextEncoding}, + apis::{Crypto, Console, NonStandardConsole, Random, StreamIO, TextEncoding}, config::{JSIntrinsics, JavyIntrinsics}, Config, }; @@ -145,10 +145,10 @@ impl Runtime { } } - if javy_intrinsics.contains(JavyIntrinsics::CRYPTOX) { - // #[cfg(feature = "cryptox")] + if intrinsics.contains(JSIntrinsics::CRYPTO) { + // #[cfg(feature = "crypto")] unsafe { - Cryptox::add_intrinsic(ctx.as_raw()) + Crypto::add_intrinsic(ctx.as_raw()) } } }); From b804d6c728428a3b6fd293268aa1b406ce7f9b63 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 2 Jul 2024 11:52:58 -0400 Subject: [PATCH 05/22] Apply linting changes Ran `cargo fmt -- --check`, and applied suggestions, per ci feedback. --- crates/javy/src/apis/crypto/mod.rs | 11 ++++------- crates/javy/src/apis/mod.rs | 4 ++-- crates/javy/src/runtime.rs | 6 ++---- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index d42e3764..2fac230c 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -1,12 +1,9 @@ -use crate::quickjs::{context::Intrinsic, qjs, Ctx, Object, String as JSString, Value, Function}; -use crate::{ - hold, hold_and_release, val_to_string, - to_js_error, Args -}; +use crate::quickjs::{context::Intrinsic, qjs, Ctx, Function, Object, String as JSString, Value}; +use crate::{hold, hold_and_release, to_js_error, val_to_string, Args}; use anyhow::{bail, Error, Result}; -use sha2::Sha256; use hmac::{Hmac, Mac}; +use sha2::Sha256; /// An implemetation of crypto APIs to optimize fuel. /// Currently, hmacSHA256 is the only function implemented. @@ -58,7 +55,7 @@ fn hmac_sha256(args: Args<'_>) -> Result> { let result = mac.finalize(); let code_bytes = result.into_bytes(); - let code : String = format!("{code_bytes:x}"); + let code: String = format!("{code_bytes:x}"); let js_string = JSString::from_str(cx, &code); Ok(Value::from_string(js_string?)) } diff --git a/crates/javy/src/apis/mod.rs b/crates/javy/src/apis/mod.rs index f1b023d9..d440d6c5 100644 --- a/crates/javy/src/apis/mod.rs +++ b/crates/javy/src/apis/mod.rs @@ -57,17 +57,17 @@ //! //! Disabled by default. pub(crate) mod console; +pub(crate) mod crypto; #[cfg(feature = "json")] pub(crate) mod json; pub(crate) mod random; pub(crate) mod stream_io; pub(crate) mod text_encoding; -pub(crate) mod crypto; pub(crate) use console::*; +pub(crate) use crypto::*; #[cfg(feature = "json")] pub(crate) use json::*; pub(crate) use random::*; pub(crate) use stream_io::*; pub(crate) use text_encoding::*; -pub(crate) use crypto::*; diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index cdcb8d4a..306a715c 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -1,7 +1,7 @@ // use crate::quickjs::JSContextRef; use super::from_js_error; use crate::{ - apis::{Crypto, Console, NonStandardConsole, Random, StreamIO, TextEncoding}, + apis::{Console, Crypto, NonStandardConsole, Random, StreamIO, TextEncoding}, config::{JSIntrinsics, JavyIntrinsics}, Config, }; @@ -147,9 +147,7 @@ impl Runtime { if intrinsics.contains(JSIntrinsics::CRYPTO) { // #[cfg(feature = "crypto")] - unsafe { - Crypto::add_intrinsic(ctx.as_raw()) - } + unsafe { Crypto::add_intrinsic(ctx.as_raw()) } } }); From befd757755dca3e0bb6bb85149d37a491179955b Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 11:47:20 -0400 Subject: [PATCH 06/22] WIP - refactor crypto method shape --- Cargo.lock | 1 + crates/core/src/runtime.rs | 3 +- crates/javy/Cargo.toml | 1 + crates/javy/src/apis/crypto/mod.rs | 139 ++++++++++++++++++++++++----- 4 files changed, 119 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef9185f5..228861b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,7 @@ dependencies = [ "rmp-serde", "rquickjs", "rquickjs-core", + "rquickjs-macro", "rquickjs-sys", "serde", "serde-transcode", diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 416e39d0..b213b033 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -13,7 +13,8 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // fix forward. .override_json_parse_and_stringify(false) .javy_json(false) - .crypto(shared_config.contains(SharedConfig::CRYPTO)); + // .crypto(shared_config.contains(SharedConfig::CRYPTO)); + .crypto(true); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/Cargo.toml b/crates/javy/Cargo.toml index 8544d469..515ba376 100644 --- a/crates/javy/Cargo.toml +++ b/crates/javy/Cargo.toml @@ -13,6 +13,7 @@ categories = ["wasm"] anyhow = { workspace = true } rquickjs = { version = "=0.6.1", features = ["array-buffer", "bindgen"] } rquickjs-core = "=0.6.1" +rquickjs-macro = "=0.6.1" rquickjs-sys = "=0.6.1" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", optional = true } diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 2fac230c..9ecba891 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -1,4 +1,5 @@ -use crate::quickjs::{context::Intrinsic, qjs, Ctx, Function, Object, String as JSString, Value}; +use crate::quickjs::{context::Intrinsic, qjs, Class, Ctx, Function, Object, String as JSString, Value}; + use crate::{hold, hold_and_release, to_js_error, val_to_string, Args}; use anyhow::{bail, Error, Result}; @@ -17,11 +18,11 @@ impl Intrinsic for Crypto { fn register(this: Ctx<'_>) -> Result<()> { let globals = this.globals(); - // let crypto_obj = Object::new(cx)?; + // let crypto_obj = Object::nw(cx)?; let crypto_obj = Object::new(this.clone())?; crypto_obj.set( - "hmacSHA256", + "createHmac", 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)) @@ -32,41 +33,125 @@ fn register(this: Ctx<'_>) -> Result<()> { Ok::<_, Error>(()) } -/// hmac_sha256 applies the HMAC algorithm using sha256 for hashing. -/// Arg[0] - secret -/// Arg[1] - message -/// returns - hex encoded string of hmac. -fn hmac_sha256(args: Args<'_>) -> Result> { - let (cx, args) = args.release(); +/// hmac_sha256 applies the HMAC algorithm for signing. +/// Arg[0] - algorithm (only supports sha256 today) +/// Arg[1] - secret key +/// returns - Hmac object +fn hmac_sha256(args: Args<'_>) -> Result> { + let (ctx, args) = args.release(); if args.len() != 2 { bail!("Wrong number of arguments. Expected 2. Got {}", args.len()); } - let js_string_secret = val_to_string(&cx, args[0].clone())?; - let js_string_message = val_to_string(&cx, args[1].clone())?; + let js_string_algo = val_to_string(&ctx, args[0].clone())?; + let js_string_secret = val_to_string(&ctx, args[1].clone())?; + + // if (js_algo_secret != "sha256") { + // bail!("Argument 1: only sha256 supported."); + // } + + return Ok( + Class::instance( + ctx.clone(), + HmacClass{ + algorithm: js_string_algo.clone(), + key: js_string_secret.clone(), + message: JSString::from_str(ctx, "").unwrap(), + } + ).unwrap() + ); + + // let cls = Class::instance( + // ctx.clone(), + // TestClass { + // inner_object: Object::new(ctx.clone()).unwrap(), + // some_value: 1, + // another_value: 2, + // }, + // ) + // .unwrap(); + + // Pass it to JavaScript + + // mac.update(js_string_message.as_bytes()); + + // let result = mac.finalize(); + // let code_bytes = result.into_bytes(); + // let code: String = format!("{code_bytes:x}"); + // let js_string = JSString::from_str(cx, &code); + // Ok(Value::from_string(js_string?)) +} - /// Create alias for HMAC-SHA256 - type HmacSha256 = Hmac; - let mut mac = HmacSha256::new_from_slice(js_string_secret.as_bytes()) - .expect("HMAC can take key of any size"); - mac.update(js_string_message.as_bytes()); - let result = mac.finalize(); +fn hmac_sha256_result(secret: String, message: String) -> Result { + type HmacSha256 = Hmac; + 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(); let code: String = format!("{code_bytes:x}"); - let js_string = JSString::from_str(cx, &code); - Ok(Value::from_string(js_string?)) + return Ok(code); +} + +#[derive(rquickjs_macro::Trace)] +#[rquickjs_macro::class(rename_all = "camelCase")] +pub struct HmacClass<'js> { + algorithm: String, + key: String, + + #[qjs(get, set)] + message: JSString<'js>, } +#[rquickjs_macro::methods] +impl <'_>HmacClass<'_> { + #[qjs(get, rename = "digest")] + pub fn digest(&self, type_of_digest: JSString<'_>) -> Result> { + let ctx = self.message.ctx(); + + let js_string_message = val_to_string(&ctx, self.message.clone().into()).unwrap(); + + // return hmac_sha256_result(self.key.clone(), js_string_message).unwrap(); + let js_string = JSString::from_str(ctx.clone(), "hello world"); + + // fails with: + // lifetime may not live long enough + // requirement occurs because of the type `rquickjs::Value<'_>`, which makes the generic argument `'_` invariant + // the struct `rquickjs::Value<'js>` is invariant over the parameter `'js` + // see for more information about variancerustcClick for full compiler diagnostic + // mod.rs(111, 19): let's call the lifetime of this reference `'1` + // mod.rs(111, 19): has type `&crypto::HmacClass<'2>` + Ok(Value::from_string(js_string?)) + } +} + +#[derive(rquickjs_macro::Trace)] +#[rquickjs_macro::class(rename_all = "camelCase")] +pub struct TestClass<'js> { + /// These attribute make the accessible from JavaScript with getters and setters. + /// As we used `rename_all = "camelCase"` in the attribute it will be called `innerObject` + /// on the JavaScript side. + #[qjs(get, set)] + inner_object: Object<'js>, + + /// This works for any value which implements `IntoJs` and `FromJs` and is clonable. + #[qjs(get, set)] + some_value: u32, + /// Make a field enumerable. + #[qjs(get, set, enumerable)] + another_value: u32, +} + + #[cfg(test)] mod tests { use crate::{quickjs::Value, Config, Runtime}; use anyhow::{Error, Result}; #[test] - fn test_text_encoder_decoder() -> Result<()> { + fn test_crypto_digest() -> Result<()> { let mut config = Config::default(); config.crypto(true); let runtime = Runtime::new(config)?; @@ -74,13 +159,19 @@ mod tests { runtime.context().with(|this| { let result: Value<'_> = this.eval( r#" - let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; - let result = crypto.hmacSHA256("my secret and secure key", "input message"); - expectedHex === result; + // let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; + // let result = crypto.hmacSHA256("my secret and secure key", "input message"); + // expectedHex === result; + let hmac = crypto.createHmac("sha256", "my secret and secure key"); + hmac.message = "input message"; + // hmac.digest("hex") === expectedHex; + hmac.message === "input message"; + + hmac.digest("hex"); "#, )?; - assert!(result.as_bool().unwrap()); + // assert!(result.as_bool().unwrap()); Ok::<_, Error>(()) })?; Ok(()) From 4c1357f469aa5086757e826cc9fd19e5e276c984 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 15:27:54 -0400 Subject: [PATCH 07/22] Code compiles, crashing running the method No clear reason on why it fails --- crates/javy/src/apis/crypto/mod.rs | 70 ++++++++++-------------------- 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 9ecba891..ca02e899 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -1,5 +1,5 @@ use crate::quickjs::{context::Intrinsic, qjs, Class, Ctx, Function, Object, String as JSString, Value}; - +use rquickjs::Error as JsError; use crate::{hold, hold_and_release, to_js_error, val_to_string, Args}; use anyhow::{bail, Error, Result}; @@ -15,6 +15,7 @@ impl Intrinsic for Crypto { register(Ctx::from_raw(ctx)).expect("`Crypto` APIs to succeed") } } + fn register(this: Ctx<'_>) -> Result<()> { let globals = this.globals(); @@ -47,9 +48,9 @@ fn hmac_sha256(args: Args<'_>) -> Result> { let js_string_algo = val_to_string(&ctx, args[0].clone())?; let js_string_secret = val_to_string(&ctx, args[1].clone())?; - // if (js_algo_secret != "sha256") { - // bail!("Argument 1: only sha256 supported."); - // } + if js_string_algo != "sha256" { + bail!("Argument 1: only sha256 supported."); + } return Ok( Class::instance( @@ -62,16 +63,7 @@ fn hmac_sha256(args: Args<'_>) -> Result> { ).unwrap() ); - // let cls = Class::instance( - // ctx.clone(), - // TestClass { - // inner_object: Object::new(ctx.clone()).unwrap(), - // some_value: 1, - // another_value: 2, - // }, - // ) - // .unwrap(); - + // prior method // Pass it to JavaScript // mac.update(js_string_message.as_bytes()); @@ -106,45 +98,30 @@ pub struct HmacClass<'js> { } #[rquickjs_macro::methods] -impl <'_>HmacClass<'_> { +impl<'js> HmacClass<'js> { #[qjs(get, rename = "digest")] - pub fn digest(&self, type_of_digest: JSString<'_>) -> Result> { + pub fn digest(&self, type_of_digest: JSString<'js>) -> Result, JsError> { let ctx = self.message.ctx(); + let js_type_of_digest = type_of_digest.to_string()?; + if js_type_of_digest != "hex" { + // raises this error: + // mismatched types + // `anyhow::Error` and `rquickjs::Error` have similar names, but are actually distinct typesrustcClick for full compiler diagnostic + // macros.rs(229, 9): Actual error occurred here + // macros.rs(58, 39): Error originated from macro call here + // lib.rs(387, 1): `anyhow::Error` is defined in crate `anyhow` + // result.rs(59, 1): `rquickjs::Error` is defined in crate `rquickjs_core` + // bail!("Only supported digest format is hex"); + } let js_string_message = val_to_string(&ctx, self.message.clone().into()).unwrap(); - // return hmac_sha256_result(self.key.clone(), js_string_message).unwrap(); - let js_string = JSString::from_str(ctx.clone(), "hello world"); - - // fails with: - // lifetime may not live long enough - // requirement occurs because of the type `rquickjs::Value<'_>`, which makes the generic argument `'_` invariant - // the struct `rquickjs::Value<'js>` is invariant over the parameter `'js` - // see for more information about variancerustcClick for full compiler diagnostic - // mod.rs(111, 19): let's call the lifetime of this reference `'1` - // mod.rs(111, 19): has type `&crypto::HmacClass<'2>` - Ok(Value::from_string(js_string?)) + let code = hmac_sha256_result(self.key.clone(), js_string_message).unwrap(); + let js_string = JSString::from_str(ctx.clone(), &code)?; + Ok(Value::from_string(js_string)) } } -#[derive(rquickjs_macro::Trace)] -#[rquickjs_macro::class(rename_all = "camelCase")] -pub struct TestClass<'js> { - /// These attribute make the accessible from JavaScript with getters and setters. - /// As we used `rename_all = "camelCase"` in the attribute it will be called `innerObject` - /// on the JavaScript side. - #[qjs(get, set)] - inner_object: Object<'js>, - - /// This works for any value which implements `IntoJs` and `FromJs` and is clonable. - #[qjs(get, set)] - some_value: u32, - /// Make a field enumerable. - #[qjs(get, set, enumerable)] - another_value: u32, -} - - #[cfg(test)] mod tests { use crate::{quickjs::Value, Config, Runtime}; @@ -167,7 +144,8 @@ mod tests { // hmac.digest("hex") === expectedHex; hmac.message === "input message"; - hmac.digest("hex"); + // this line crashes + // hmac.digest("hex"); "#, )?; From b59ac8efaa7954fe00031685b49901af384d6652 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 16:26:19 -0400 Subject: [PATCH 08/22] Hello world string return successfully --- crates/javy/src/apis/crypto/mod.rs | 41 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index ca02e899..0b7cfcc0 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -99,26 +99,28 @@ pub struct HmacClass<'js> { #[rquickjs_macro::methods] impl<'js> HmacClass<'js> { - #[qjs(get, rename = "digest")] + #[qjs()] pub fn digest(&self, type_of_digest: JSString<'js>) -> Result, JsError> { let ctx = self.message.ctx(); - let js_type_of_digest = type_of_digest.to_string()?; - if js_type_of_digest != "hex" { - // raises this error: - // mismatched types - // `anyhow::Error` and `rquickjs::Error` have similar names, but are actually distinct typesrustcClick for full compiler diagnostic - // macros.rs(229, 9): Actual error occurred here - // macros.rs(58, 39): Error originated from macro call here - // lib.rs(387, 1): `anyhow::Error` is defined in crate `anyhow` - // result.rs(59, 1): `rquickjs::Error` is defined in crate `rquickjs_core` - // bail!("Only supported digest format is hex"); + // let js_type_of_digest = type_of_digest.to_string()?; + // if js_type_of_digest != "hex" { + // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "digest type must be hex")); + // } + + // let js_string_message = val_to_string(&ctx, self.message.clone().into()).map_err(|e| to_js_error(ctx.clone(), e)); + // if js_string_message.is_err() { + // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed val_to_string")); + // } + // let code = hmac_sha256_result(self.key.clone(), js_string_message?).map_err(|e| to_js_error(ctx.clone(), e)); + // if code.is_err() { + // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed hmac_sha256_result")); + // } + // let js_string = JSString::from_str(ctx.clone(), &code?)?; + let js_string = JSString::from_str(ctx.clone(), "hello world"); + if js_string.is_err() { + return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed to convert string")); } - - let js_string_message = val_to_string(&ctx, self.message.clone().into()).unwrap(); - - let code = hmac_sha256_result(self.key.clone(), js_string_message).unwrap(); - let js_string = JSString::from_str(ctx.clone(), &code)?; - Ok(Value::from_string(js_string)) + Ok(Value::from_string(js_string?)) } } @@ -145,11 +147,10 @@ mod tests { hmac.message === "input message"; // this line crashes - // hmac.digest("hex"); + hmac.digest("hex") == "hello world"; "#, )?; - - // assert!(result.as_bool().unwrap()); + assert!(result.as_bool().unwrap()); Ok::<_, Error>(()) })?; Ok(()) From 9807751a9b29d138707bc1a4711d8adbd2129622 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 16:32:47 -0400 Subject: [PATCH 09/22] Hmac works basically --- crates/javy/src/apis/crypto/mod.rs | 40 +++++++++++++----------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 0b7cfcc0..ce572f1e 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -102,21 +102,21 @@ impl<'js> HmacClass<'js> { #[qjs()] pub fn digest(&self, type_of_digest: JSString<'js>) -> Result, JsError> { let ctx = self.message.ctx(); - // let js_type_of_digest = type_of_digest.to_string()?; - // if js_type_of_digest != "hex" { - // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "digest type must be hex")); - // } - - // let js_string_message = val_to_string(&ctx, self.message.clone().into()).map_err(|e| to_js_error(ctx.clone(), e)); - // if js_string_message.is_err() { - // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed val_to_string")); - // } - // let code = hmac_sha256_result(self.key.clone(), js_string_message?).map_err(|e| to_js_error(ctx.clone(), e)); - // if code.is_err() { - // return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed hmac_sha256_result")); - // } - // let js_string = JSString::from_str(ctx.clone(), &code?)?; - let js_string = JSString::from_str(ctx.clone(), "hello world"); + let js_type_of_digest = type_of_digest.to_string()?; + if js_type_of_digest != "hex" { + return Err(rquickjs::Exception::throw_type(&ctx.clone(), "digest type must be hex")); + } + + let js_string_message = val_to_string(&ctx, self.message.clone().into()).map_err(|e| to_js_error(ctx.clone(), e)); + if js_string_message.is_err() { + return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed val_to_string")); + } + let code = hmac_sha256_result(self.key.clone(), js_string_message?).map_err(|e| to_js_error(ctx.clone(), e)); + if code.is_err() { + return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed hmac_sha256_result")); + } + let js_string = JSString::from_str(ctx.clone(), &code?); + // let js_string = JSString::from_str(ctx.clone(), "hello world"); if js_string.is_err() { return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed to convert string")); } @@ -138,16 +138,10 @@ mod tests { runtime.context().with(|this| { let result: Value<'_> = this.eval( r#" - // let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; - // let result = crypto.hmacSHA256("my secret and secure key", "input message"); - // expectedHex === result; + let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; let hmac = crypto.createHmac("sha256", "my secret and secure key"); hmac.message = "input message"; - // hmac.digest("hex") === expectedHex; - hmac.message === "input message"; - - // this line crashes - hmac.digest("hex") == "hello world"; + hmac.digest("hex") === expectedHex; "#, )?; assert!(result.as_bool().unwrap()); From b7794836641eb7c801ff611dd6f4ebc9c1f4c7f2 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 22:32:16 -0400 Subject: [PATCH 10/22] Add update method Replace read/write message field with update field to match nodejs crypto api. Add tests for failure conditions. Clean up string error handling in digest method. --- crates/javy/src/apis/crypto/mod.rs | 103 ++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 30 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index ce572f1e..662f32cb 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -62,17 +62,6 @@ fn hmac_sha256(args: Args<'_>) -> Result> { } ).unwrap() ); - - // prior method - // Pass it to JavaScript - - // mac.update(js_string_message.as_bytes()); - - // let result = mac.finalize(); - // let code_bytes = result.into_bytes(); - // let code: String = format!("{code_bytes:x}"); - // let js_string = JSString::from_str(cx, &code); - // Ok(Value::from_string(js_string?)) } @@ -92,8 +81,6 @@ fn hmac_sha256_result(secret: String, message: String) -> Result { pub struct HmacClass<'js> { algorithm: String, key: String, - - #[qjs(get, set)] message: JSString<'js>, } @@ -102,25 +89,41 @@ impl<'js> HmacClass<'js> { #[qjs()] pub fn digest(&self, type_of_digest: JSString<'js>) -> Result, JsError> { let ctx = self.message.ctx(); - let js_type_of_digest = type_of_digest.to_string()?; + + // Convert JSString to Rust String + let js_type_of_digest = type_of_digest.to_string() + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert type_of_digest to string: {}", e)))?; + if js_type_of_digest != "hex" { - return Err(rquickjs::Exception::throw_type(&ctx.clone(), "digest type must be hex")); + return Err(rquickjs::Exception::throw_type(&ctx, "digest type must be 'hex'")); } - let js_string_message = val_to_string(&ctx, self.message.clone().into()).map_err(|e| to_js_error(ctx.clone(), e)); - if js_string_message.is_err() { - return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed val_to_string")); - } - let code = hmac_sha256_result(self.key.clone(), js_string_message?).map_err(|e| to_js_error(ctx.clone(), e)); - if code.is_err() { - return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed hmac_sha256_result")); - } - let js_string = JSString::from_str(ctx.clone(), &code?); - // let js_string = JSString::from_str(ctx.clone(), "hello world"); - if js_string.is_err() { - return Err(rquickjs::Exception::throw_type(&ctx.clone(), "failed to convert string")); - } - Ok(Value::from_string(js_string?)) + // Convert message to Rust String + let js_string_message = val_to_string(&ctx, self.message.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e)))?; + + // Compute HMAC + let code = hmac_sha256_result(self.key.clone(), js_string_message) + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to compute HMAC: {}", e)))?; + + // Convert result to JSString + let js_string = JSString::from_str(ctx.clone(), &code) + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert result to JSString: {}", e)))?; + + Ok(Value::from_string(js_string)) + } + + #[qjs()] + pub fn update(&mut self, v: JSString<'js>) { + let ctx = self.message.ctx(); + let mut js_string_message = val_to_string(&ctx, self.message.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); + + let js_v = val_to_string(&ctx, v.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); + + js_string_message.push_str(&js_v); + self.message = JSString::from_str(ctx.clone(), &js_string_message).unwrap(); } } @@ -140,7 +143,7 @@ mod tests { r#" let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; let hmac = crypto.createHmac("sha256", "my secret and secure key"); - hmac.message = "input message"; + hmac.update("input message"); hmac.digest("hex") === expectedHex; "#, )?; @@ -149,4 +152,44 @@ mod tests { })?; Ok(()) } + + #[test] + fn test_not_sha256_algo_errors() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result= this.eval::, _>( + r#" + crypto.createHmac("not-sha", "my secret and secure key"); + "#, + ); + assert!(result.is_err()); + assert_eq!("Exception generated by QuickJS", result.err().unwrap().to_string()); + Ok::<_, Error>(()) + })?; + Ok(()) + } + + #[test] + fn test_not_hex_digest_errors() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result= this.eval::, _>( + r#" + let hmac = crypto.createHmac("sha256", "my secret and secure key"); + hmac.update("input message"); + hmac.digest("base64"); + "#, + ); + assert!(result.is_err()); + assert_eq!("Exception generated by QuickJS", result.err().unwrap().to_string()); + Ok::<_, Error>(()) + })?; + Ok(()) + } } From 158a70d05b0da330b91e843e2d259fce4ffb2bd4 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Wed, 3 Jul 2024 22:53:13 -0400 Subject: [PATCH 11/22] Code cleanup - addess lint warnings - renamed variables for clarity - improved method names for clarity - clarified fn definition comments --- crates/javy/src/apis/crypto/mod.rs | 60 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 662f32cb..b2721c0b 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -26,7 +26,7 @@ fn register(this: Ctx<'_>) -> Result<()> { "createHmac", 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)) + hmac_sha256_obj(hold!(this.clone(), args)).map_err(|e| to_js_error(this, e)) }), )?; @@ -34,21 +34,22 @@ fn register(this: Ctx<'_>) -> Result<()> { Ok::<_, Error>(()) } -/// hmac_sha256 applies the HMAC algorithm for signing. + +/// hmac_sha256_obj creates the HMAC object /// Arg[0] - algorithm (only supports sha256 today) /// Arg[1] - secret key /// returns - Hmac object -fn hmac_sha256(args: Args<'_>) -> Result> { +fn hmac_sha256_obj(args: Args<'_>) -> Result> { let (ctx, args) = args.release(); if args.len() != 2 { bail!("Wrong number of arguments. Expected 2. Got {}", args.len()); } - let js_string_algo = val_to_string(&ctx, args[0].clone())?; - let js_string_secret = val_to_string(&ctx, args[1].clone())?; + let algo = val_to_string(&ctx, args[0].clone())?; + let key = val_to_string(&ctx, args[1].clone())?; - if js_string_algo != "sha256" { + if algo != "sha256" { bail!("Argument 1: only sha256 supported."); } @@ -56,16 +57,15 @@ fn hmac_sha256(args: Args<'_>) -> Result> { Class::instance( ctx.clone(), HmacClass{ - algorithm: js_string_algo.clone(), - key: js_string_secret.clone(), + algorithm: algo.clone(), + key: key.clone(), message: JSString::from_str(ctx, "").unwrap(), } ).unwrap() ); } - - +/// hmac_sha256_result applies the HMAC sha256 algorithm for signing. fn hmac_sha256_result(secret: String, message: String) -> Result { type HmacSha256 = Hmac; let mut hmac = HmacSha256::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size"); @@ -73,7 +73,7 @@ fn hmac_sha256_result(secret: String, message: String) -> Result { let result = hmac.finalize(); let code_bytes = result.into_bytes(); let code: String = format!("{code_bytes:x}"); - return Ok(code); + Ok(code) } #[derive(rquickjs_macro::Trace)] @@ -87,43 +87,43 @@ pub struct HmacClass<'js> { #[rquickjs_macro::methods] impl<'js> HmacClass<'js> { #[qjs()] - pub fn digest(&self, type_of_digest: JSString<'js>) -> Result, JsError> { + pub fn digest(&self, js_type_of_digest: JSString<'js>) -> Result, JsError> { let ctx = self.message.ctx(); // Convert JSString to Rust String - let js_type_of_digest = type_of_digest.to_string() - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert type_of_digest to string: {}", e)))?; + let type_of_digest = js_type_of_digest.to_string() + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert type_of_digest to string: {}", e)))?; - if js_type_of_digest != "hex" { - return Err(rquickjs::Exception::throw_type(&ctx, "digest type must be 'hex'")); + if type_of_digest != "hex" { + return Err(rquickjs::Exception::throw_type(ctx, "digest type must be 'hex'")); } // Convert message to Rust String - let js_string_message = val_to_string(&ctx, self.message.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e)))?; + let string_message = val_to_string(ctx, self.message.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e)))?; // Compute HMAC - let code = hmac_sha256_result(self.key.clone(), js_string_message) - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to compute HMAC: {}", e)))?; + let string_digest = hmac_sha256_result(self.key.clone(), string_message) + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to compute HMAC: {}", e)))?; // Convert result to JSString - let js_string = JSString::from_str(ctx.clone(), &code) - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert result to JSString: {}", e)))?; + 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)))?; - Ok(Value::from_string(js_string)) + Ok(Value::from_string(js_string_digest)) } #[qjs()] - pub fn update(&mut self, v: JSString<'js>) { + pub fn update(&mut self, js_v: JSString<'js>) { let ctx = self.message.ctx(); - let mut js_string_message = val_to_string(&ctx, self.message.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); + let mut string_message = val_to_string(ctx, self.message.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); - let js_v = val_to_string(&ctx, v.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(&ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); + let v = val_to_string(ctx, js_v.clone().into()) + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); - js_string_message.push_str(&js_v); - self.message = JSString::from_str(ctx.clone(), &js_string_message).unwrap(); + string_message.push_str(&v); + self.message = JSString::from_str(ctx.clone(), &string_message).unwrap(); } } From b320eece57ed618ec822659c052d37076cd9f680 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 10:43:24 -0400 Subject: [PATCH 12/22] Disable crypto by default --- crates/core/src/runtime.rs | 5 +++-- crates/javy/src/config.rs | 4 ++-- crates/javy/src/runtime.rs | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index b213b033..08aa3d34 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -13,8 +13,9 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // fix forward. .override_json_parse_and_stringify(false) .javy_json(false) - // .crypto(shared_config.contains(SharedConfig::CRYPTO)); - .crypto(true); + // For the time being, ship crypto as off by default + // Later, we may enable it with: .crypto(shared_config.contains(SharedConfig::CRYPTO)) + .crypto(false); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index c678f53c..0eec9c34 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -67,6 +67,7 @@ impl Default for Config { fn default() -> Self { let mut intrinsics = JSIntrinsics::all(); intrinsics.set(JSIntrinsics::TEXT_ENCODING, false); + intrinsics.set(JSIntrinsics::CRYPTO, false); Self { intrinsics, javy_intrinsics: JavyIntrinsics::empty(), @@ -181,8 +182,7 @@ impl Config { } /// Whether the `crypto` intrinsic will be available. - /// Enabled by default. - // #[cfg(feature = "crypto")] + /// Disabled by default. pub fn crypto(&mut self, enable: bool) -> &mut Self { self.intrinsics.set(JSIntrinsics::CRYPTO, enable); self diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 306a715c..1cf29547 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -146,7 +146,6 @@ impl Runtime { } if intrinsics.contains(JSIntrinsics::CRYPTO) { - // #[cfg(feature = "crypto")] unsafe { Crypto::add_intrinsic(ctx.as_raw()) } } }); From 7aa875764bfab14ca8bdbacb076d8157374cf7c1 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 10:43:53 -0400 Subject: [PATCH 13/22] Improve testing --- crates/javy/src/apis/crypto/mod.rs | 36 +++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index b2721c0b..aa8662c5 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -19,7 +19,6 @@ impl Intrinsic for Crypto { fn register(this: Ctx<'_>) -> Result<()> { let globals = this.globals(); - // let crypto_obj = Object::nw(cx)?; let crypto_obj = Object::new(this.clone())?; crypto_obj.set( @@ -120,7 +119,7 @@ impl<'js> HmacClass<'js> { .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); let v = val_to_string(ctx, js_v.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); + .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert update input to string: {}", e))).unwrap(); string_message.push_str(&v); self.message = JSString::from_str(ctx.clone(), &string_message).unwrap(); @@ -129,7 +128,7 @@ impl<'js> HmacClass<'js> { #[cfg(test)] mod tests { - use crate::{quickjs::Value, Config, Runtime}; + use crate::{from_js_error, quickjs::Value, Config, Runtime}; use anyhow::{Error, Result}; #[test] @@ -153,6 +152,31 @@ mod tests { Ok(()) } + #[test] + fn test_crypto_digest_with_lossy_input() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result: Value<'_> = this.eval( + r#" + // matched tested behavior in node v18 + let expectedHex = "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"; + let hmac = crypto.createHmac("sha256", "\uD800\uD800\uD800\uD800\uD800"); + hmac.update("\uD800\uD800\uD800\uD800\uD800"); + let result = hmac.digest("hex"); + console.log(result); + console.log("Match?", result === expectedHex); + result === expectedHex; + "#, + )?; + assert!(result.as_bool().unwrap()); + Ok::<_, Error>(()) + })?; + Ok(()) + } + #[test] fn test_not_sha256_algo_errors() -> Result<()> { let mut config = Config::default(); @@ -166,7 +190,8 @@ mod tests { "#, ); assert!(result.is_err()); - assert_eq!("Exception generated by QuickJS", result.err().unwrap().to_string()); + let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + assert_eq!("Error:2:28 Argument 1: only sha256 supported.\n at (eval_script:2:28)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) @@ -187,7 +212,8 @@ mod tests { "#, ); assert!(result.is_err()); - assert_eq!("Exception generated by QuickJS", result.err().unwrap().to_string()); + let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + assert_eq!("Error:4:26 digest type must be 'hex'\n at (eval_script:4:26)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) From 381a387958435cce21a5844ec2144917352bdb70 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 10:49:24 -0400 Subject: [PATCH 14/22] Add changelog entry to javy crate --- npm/javy/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/npm/javy/CHANGELOG.md b/npm/javy/CHANGELOG.md index 7a1f3e67..33214934 100644 --- a/npm/javy/CHANGELOG.md +++ b/npm/javy/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- Added crypto HmacSha256 API support. + ## [0.1.2] - 2023-07-28 ### Added From 318342b36ef0c97f952ea8bd01455307417153a6 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 11:01:46 -0400 Subject: [PATCH 15/22] Add test that crypto is off by default --- crates/javy/src/apis/crypto/mod.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index aa8662c5..b7cfef99 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -152,6 +152,24 @@ mod tests { Ok(()) } + #[test] + fn test_crypto_disabled_by_default() -> Result<()> { + let runtime = Runtime::new(Config::default())?; + + runtime.context().with(|this| { + let result= this.eval::, _>( + r#" + crypto.createHmac("sha256", "hello world"); + "#, + ); + 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_script:2:21)\n", e.to_string()); + Ok::<_, Error>(()) + })?; + Ok(()) + } + #[test] fn test_crypto_digest_with_lossy_input() -> Result<()> { let mut config = Config::default(); From 2d873c2697a481b5b12936844231e0d95b1c660a Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 16:14:28 -0400 Subject: [PATCH 16/22] Switch to wpt style invoacation Partial implementation, including: - crypto.subtle.sign method - setup wpt testing infra Missing: - validation of sign protocol (hmac and sha256) - add verify method - return value promise to match API format --- Makefile | 2 +- crates/core/Cargo.toml | 1 + crates/core/src/runtime.rs | 2 +- crates/javy/src/apis/crypto/mod.rs | 182 ++++++++++++----------------- wpt/test_spec.js | 3 + 5 files changed, 79 insertions(+), 111 deletions(-) diff --git a/Makefile b/Makefile index 8bd7045f..2bdc5d98 100644 --- a/Makefile +++ b/Makefile @@ -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 +test-wpt: export CORE_FEATURES ?= experimental_event_loop,experimental_crypto test-wpt: # Can't use a prerequisite here b/c a prequisite will not cause a rebuild of the CLI $(MAKE) cli diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 0c122095..fc0e754f 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,3 +21,4 @@ javy-config = { workspace = true } [features] experimental_event_loop = [] +experimental_crypto = [] diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 08aa3d34..30ce6336 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -15,7 +15,7 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { .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(false); + .crypto(cfg!(feature = "experimental_crypto")); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index b7cfef99..90f743b7 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -18,50 +18,48 @@ 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())?; - crypto_obj.set( - "createHmac", + subtle_obj.set( + "sign", Function::new(this.clone(), |this, args| { let (this, args) = hold_and_release!(this, args); - hmac_sha256_obj(hold!(this.clone(), args)).map_err(|e| to_js_error(this, e)) + 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)?; Ok::<_, Error>(()) } -/// hmac_sha256_obj creates the HMAC object -/// Arg[0] - algorithm (only supports sha256 today) -/// Arg[1] - secret key -/// returns - Hmac object -fn hmac_sha256_obj(args: Args<'_>) -> Result> { + +/// hmac_sha256 applies the HMAC algorithm using sha256 for hashing. +/// Arg[0] - secret +/// Arg[1] - message +/// returns - hex encoded string of hmac. +fn hmac_sha256(args: Args<'_>) -> Result> { let (ctx, args) = args.release(); - if args.len() != 2 { - bail!("Wrong number of arguments. Expected 2. Got {}", args.len()); + if args.len() != 3 { + bail!("Wrong number of arguments. Expected 3. Got {}", args.len()); } - let algo = val_to_string(&ctx, args[0].clone())?; - let key = val_to_string(&ctx, args[1].clone())?; - - if algo != "sha256" { - bail!("Argument 1: only sha256 supported."); - } + // let protocol: Object = val_to_string(&ctx, args[0].clone())?; // need to figure out how to convert val to a struct + // use: .get https://docs.rs/rquickjs/0.6.2/rquickjs/struct.Object.html#method.get + + let secret = val_to_string(&ctx, args[1].clone())?; + let message = val_to_string(&ctx, args[2].clone())?; - return Ok( - Class::instance( - ctx.clone(), - HmacClass{ - algorithm: algo.clone(), - key: key.clone(), - message: JSString::from_str(ctx, "").unwrap(), - } - ).unwrap() - ); + 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)))?; + + Ok(Value::from_string(js_string_digest)) } /// hmac_sha256_result applies the HMAC sha256 algorithm for signing. @@ -75,57 +73,6 @@ fn hmac_sha256_result(secret: String, message: String) -> Result { Ok(code) } -#[derive(rquickjs_macro::Trace)] -#[rquickjs_macro::class(rename_all = "camelCase")] -pub struct HmacClass<'js> { - algorithm: String, - key: String, - message: JSString<'js>, -} - -#[rquickjs_macro::methods] -impl<'js> HmacClass<'js> { - #[qjs()] - pub fn digest(&self, js_type_of_digest: JSString<'js>) -> Result, JsError> { - let ctx = self.message.ctx(); - - // Convert JSString to Rust String - let type_of_digest = js_type_of_digest.to_string() - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert type_of_digest to string: {}", e)))?; - - if type_of_digest != "hex" { - return Err(rquickjs::Exception::throw_type(ctx, "digest type must be 'hex'")); - } - - // Convert message to Rust String - let string_message = val_to_string(ctx, self.message.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e)))?; - - // Compute HMAC - let string_digest = hmac_sha256_result(self.key.clone(), string_message) - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to compute HMAC: {}", e)))?; - - // 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)))?; - - Ok(Value::from_string(js_string_digest)) - } - - #[qjs()] - pub fn update(&mut self, js_v: JSString<'js>) { - let ctx = self.message.ctx(); - let mut string_message = val_to_string(ctx, self.message.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert message to string: {}", e))).unwrap(); - - let v = val_to_string(ctx, js_v.clone().into()) - .map_err(|e| rquickjs::Exception::throw_type(ctx, &format!("Failed to convert update input to string: {}", e))).unwrap(); - - string_message.push_str(&v); - self.message = JSString::from_str(ctx.clone(), &string_message).unwrap(); - } -} - #[cfg(test)] mod tests { use crate::{from_js_error, quickjs::Value, Config, Runtime}; @@ -141,9 +88,8 @@ mod tests { let result: Value<'_> = this.eval( r#" let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; - let hmac = crypto.createHmac("sha256", "my secret and secure key"); - hmac.update("input message"); - hmac.digest("hex") === expectedHex; + let result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message"); + result === expectedHex; "#, )?; assert!(result.as_bool().unwrap()); @@ -159,7 +105,7 @@ mod tests { runtime.context().with(|this| { let result= this.eval::, _>( r#" - crypto.createHmac("sha256", "hello world"); + crypto.subtle; "#, ); assert!(result.is_err()); @@ -181,9 +127,7 @@ mod tests { r#" // matched tested behavior in node v18 let expectedHex = "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"; - let hmac = crypto.createHmac("sha256", "\uD800\uD800\uD800\uD800\uD800"); - hmac.update("\uD800\uD800\uD800\uD800\uD800"); - let result = hmac.digest("hex"); + let result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800"); console.log(result); console.log("Match?", result === expectedHex); result === expectedHex; @@ -196,7 +140,7 @@ mod tests { } #[test] - fn test_not_sha256_algo_errors() -> Result<()> { + fn test_crypto_undefined_methods_raise_not_a_function() -> Result<()> { let mut config = Config::default(); config.crypto(true); let runtime = Runtime::new(config)?; @@ -204,36 +148,56 @@ mod tests { runtime.context().with(|this| { let result= this.eval::, _>( r#" - crypto.createHmac("not-sha", "my secret and secure key"); + crypto.subtle.encrypt({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message"); "#, ); assert!(result.is_err()); let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:2:28 Argument 1: only sha256 supported.\n at (eval_script:2:28)\n", e.to_string()); + assert_eq!("Error:2:35 not a function\n at (eval_script:2:35)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) } - #[test] - fn test_not_hex_digest_errors() -> Result<()> { - let mut config = Config::default(); - config.crypto(true); - let runtime = Runtime::new(config)?; - - runtime.context().with(|this| { - let result= this.eval::, _>( - r#" - let hmac = crypto.createHmac("sha256", "my secret and secure key"); - hmac.update("input message"); - hmac.digest("base64"); - "#, - ); - assert!(result.is_err()); - let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:4:26 digest type must be 'hex'\n at (eval_script:4:26)\n", e.to_string()); - Ok::<_, Error>(()) - })?; - Ok(()) - } + // #[test] + // fn test_not_sha256_algo_errors() -> Result<()> { + // let mut config = Config::default(); + // config.crypto(true); + // let runtime = Runtime::new(config)?; + + // runtime.context().with(|this| { + // let result= this.eval::, _>( + // r#" + // crypto.createHmac("not-sha", "my secret and secure key"); + // "#, + // ); + // assert!(result.is_err()); + // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + // assert_eq!("Error:2:28 Argument 1: only sha256 supported.\n at (eval_script:2:28)\n", e.to_string()); + // Ok::<_, Error>(()) + // })?; + // Ok(()) + // } + + // #[test] + // fn test_not_hex_digest_errors() -> Result<()> { + // let mut config = Config::default(); + // config.crypto(true); + // let runtime = Runtime::new(config)?; + + // runtime.context().with(|this| { + // let result= this.eval::, _>( + // r#" + // let hmac = crypto.createHmac("sha256", "my secret and secure key"); + // hmac.update("input message"); + // hmac.digest("base64"); + // "#, + // ); + // assert!(result.is_err()); + // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + // assert_eq!("Error:4:26 digest type must be 'hex'\n at (eval_script:4:26)\n", e.to_string()); + // Ok::<_, Error>(()) + // })?; + // Ok(()) + // } } diff --git a/wpt/test_spec.js b/wpt/test_spec.js index 7470033a..6641fc77 100644 --- a/wpt/test_spec.js +++ b/wpt/test_spec.js @@ -67,4 +67,7 @@ export default [ { testFile: "upstream/encoding/textencoder-utf16-surrogates.any.js", }, + // { + // testFile: "upstream/WebCryptoAPI/sign_verify/hmac.https.any.js", + // }, ]; From 970af055e6d4915a61bc7174035fa078666b2835 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Thu, 4 Jul 2024 16:32:44 -0400 Subject: [PATCH 17/22] Validate protocol name and hash --- crates/javy/src/apis/crypto/mod.rs | 96 ++++++++++++++++-------------- wpt/custom_tests/hmac.any.js | 37 ++++++++++++ 2 files changed, 87 insertions(+), 46 deletions(-) create mode 100644 wpt/custom_tests/hmac.any.js diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 90f743b7..2aa9d701 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -1,5 +1,4 @@ -use crate::quickjs::{context::Intrinsic, qjs, Class, Ctx, Function, Object, String as JSString, Value}; -use rquickjs::Error as JsError; +use crate::quickjs::{context::Intrinsic, qjs, Ctx, Function, Object, String as JSString, Value}; use crate::{hold, hold_and_release, to_js_error, val_to_string, Args}; use anyhow::{bail, Error, Result}; @@ -35,7 +34,6 @@ fn register(this: Ctx<'_>) -> Result<()> { Ok::<_, Error>(()) } - /// hmac_sha256 applies the HMAC algorithm using sha256 for hashing. /// Arg[0] - secret /// Arg[1] - message @@ -47,9 +45,17 @@ fn hmac_sha256(args: Args<'_>) -> Result> { bail!("Wrong number of arguments. Expected 3. Got {}", args.len()); } - // let protocol: Object = val_to_string(&ctx, args[0].clone())?; // need to figure out how to convert val to a struct - // use: .get https://docs.rs/rquickjs/0.6.2/rquickjs/struct.Object.html#method.get + let protocol = args[0].as_object(); + + let js_protocol_name: Value = protocol.expect("protocol struct required").get("name").unwrap(); + 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(); + 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())?; @@ -159,45 +165,43 @@ mod tests { Ok(()) } - // #[test] - // fn test_not_sha256_algo_errors() -> Result<()> { - // let mut config = Config::default(); - // config.crypto(true); - // let runtime = Runtime::new(config)?; - - // runtime.context().with(|this| { - // let result= this.eval::, _>( - // r#" - // crypto.createHmac("not-sha", "my secret and secure key"); - // "#, - // ); - // assert!(result.is_err()); - // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - // assert_eq!("Error:2:28 Argument 1: only sha256 supported.\n at (eval_script:2:28)\n", e.to_string()); - // Ok::<_, Error>(()) - // })?; - // Ok(()) - // } - - // #[test] - // fn test_not_hex_digest_errors() -> Result<()> { - // let mut config = Config::default(); - // config.crypto(true); - // let runtime = Runtime::new(config)?; - - // runtime.context().with(|this| { - // let result= this.eval::, _>( - // r#" - // let hmac = crypto.createHmac("sha256", "my secret and secure key"); - // hmac.update("input message"); - // hmac.digest("base64"); - // "#, - // ); - // assert!(result.is_err()); - // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - // assert_eq!("Error:4:26 digest type must be 'hex'\n at (eval_script:4:26)\n", e.to_string()); - // Ok::<_, Error>(()) - // })?; - // Ok(()) - // } + #[test] + fn test_not_hmac_algo_errors() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result= this.eval::, _>( + r#" + let result = crypto.subtle.sign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + "#, + ); + assert!(result.is_err()); + let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + assert_eq!("Error:2:48 only name=HMAC supported\n at (eval_script:2:48)\n", e.to_string()); + Ok::<_, Error>(()) + })?; + Ok(()) + } + + #[test] + fn test_not_sha256_algo_errors() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let result= this.eval::, _>( + r#" + let result = crypto.subtle.sign({name: "HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + "#, + ); + assert!(result.is_err()); + let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + assert_eq!("Error:2:48 only hash=sha-256 supported\n at (eval_script:2:48)\n", e.to_string()); + Ok::<_, Error>(()) + })?; + Ok(()) + } } diff --git a/wpt/custom_tests/hmac.any.js b/wpt/custom_tests/hmac.any.js new file mode 100644 index 00000000..d72b15bc --- /dev/null +++ b/wpt/custom_tests/hmac.any.js @@ -0,0 +1,37 @@ +// META: script=/WebCryptoAPI/sign-verify/hmac.js + +function getTestVectors() { + var plaintext = new Uint8Array([95, 77, 186, 79, 50, 12, 12, 232, 118, 114, 90, 252, 229, 251, 210, 91, 248, 62, 90, 113, 37, 160, 140, 175, 231, 60, 62, 186, 196, 33, 119, 157, 249, 213, 93, 24, 12, 58, 233, 148, 38, 69, 225, 216, 47, 238, 140, 157, 41, 75, 60, 177, 160, 138, 153, 49, 32, 27, 60, 14, 129, 252, 71, 202, 207, 131, 21, 162, 175, 102, 50, 65, 19, 195, 182, 98, 48, 195, 70, 8, 196, 244, 89, 54, 52, 206, 2, 178, 103, 54, 34, 119, 240, 168, 64, 202, 116, 188, 61, 26, 98, 54, 149, 44, 94, 215, 170, 248, 168, 254, 203, 221, 250, 117, 132, 230, 151, 140, 234, 93, 42, 91, 159, 183, 241, 180, 140, 139, 11, 229, 138, 48, 82, 2, 117, 77, 131, 118, 16, 115, 116, 121, 60, 240, 38, 170, 238, 83, 0, 114, 125, 131, 108, 215, 30, 113, 179, 69, 221, 178, 228, 68, 70, 255, 197, 185, 1, 99, 84, 19, 137, 13, 145, 14, 163, 128, 152, 74, 144, 25, 16, 49, 50, 63, 22, 219, 204, 157, 107, 225, 104, 184, 72, 133, 56, 76, 160, 62, 18, 96, 10, 193, 194, 72, 2, 138, 243, 114, 108, 201, 52, 99, 136, 46, 168, 192, 42, 171]); + + var raw = { + "SHA-256": new Uint8Array([229, 136, 236, 8, 17, 70, 61, 118, 114, 65, 223, 16, 116, 180, 122, 228, 7, 27, 81, 242, 206, 54, 83, 123, 166, 156, 205, 195, 253, 194, 183, 168]), + }; + + var signatures = { + "SHA-256": new Uint8Array([133, 164, 12, 234, 46, 7, 140, 40, 39, 163, 149, 63, 251, 102, 194, 123, 41, 26, 71, 43, 13, 112, 160, 0, 11, 69, 216, 35, 128, 62, 235, 84]), + }; + + // Each test vector has the following fields: + // name - a unique name for this vector + // keyBuffer - an arrayBuffer with the key data + // key - a CryptoKey object for the keyBuffer. INITIALLY null! You must fill this in first to use it! + // hashName - the hash function to sign with + // plaintext - the text to encrypt + // signature - the expected signature + var vectors = []; + Object.keys(raw).forEach(function(hashName) { + vectors.push({ + name: "HMAC with " + hashName, + hash: hashName, + keyBuffer: raw[hashName], + key: null, + plaintext: plaintext, + signature: signatures[hashName] + }); + }); + + return vectors; +} + + +run_test(); From 3a732a7249ddd4255e2264e1508226f06a1c5a3e Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Fri, 5 Jul 2024 10:01:21 -0400 Subject: [PATCH 18/22] WIP - progress on promise --- crates/core/src/runtime.rs | 3 +- crates/javy/src/apis/crypto/mod.rs | 52 ++++++++++++++++++++++++------ p.js | 9 ++++++ 3 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 p.js diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index 30ce6336..f5ee9cac 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -15,7 +15,8 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { .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(cfg!(feature = "experimental_crypto")); + .crypto(true); Runtime::new(std::mem::take(config)) } diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 2aa9d701..11924776 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -1,4 +1,4 @@ -use crate::quickjs::{context::Intrinsic, qjs, Ctx, Function, Object, String as JSString, Value}; +use crate::quickjs::{context::Intrinsic, qjs, Ctx, CatchResultExt, Function, Object, String as JSString, Value, Promise, function::Func, function::This}; use crate::{hold, hold_and_release, to_js_error, val_to_string, Args}; use anyhow::{bail, Error, Result}; @@ -38,7 +38,7 @@ fn register(this: Ctx<'_>) -> Result<()> { /// Arg[0] - secret /// Arg[1] - message /// returns - hex encoded string of hmac. -fn hmac_sha256(args: Args<'_>) -> Result> { +fn hmac_sha256(args: Args<'_>) -> Result> { let (ctx, args) = args.release(); if args.len() != 3 { @@ -64,8 +64,31 @@ fn hmac_sha256(args: Args<'_>) -> Result> { // 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)?) +} - Ok(Value::from_string(js_string_digest)) +fn build_promise_from_value(value: Value<'_>) -> Result> { + 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) } /// hmac_sha256_result applies the HMAC sha256 algorithm for signing. @@ -94,7 +117,9 @@ mod tests { let result: Value<'_> = this.eval( r#" let expectedHex = "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"; - let result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message"); + 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; "#, )?; @@ -129,17 +154,24 @@ mod tests { let runtime = Runtime::new(config)?; runtime.context().with(|this| { - let result: Value<'_> = this.eval( + let result = this.eval::, _>( r#" // matched tested behavior in node v18 let expectedHex = "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"; - let result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800"); - console.log(result); - console.log("Match?", result === expectedHex); + let result = null; + result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800") + // .then(function(signature) { + // result = signature; + // }); + // console.log(result); + // console.log("Match?", result === expectedHex); result === expectedHex; "#, - )?; - assert!(result.as_bool().unwrap()); + ); + // assert!(result.is_err()); + // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); + // assert_eq!("", e.to_string()); + assert!(result.unwrap().as_bool().unwrap()); Ok::<_, Error>(()) })?; Ok(()) diff --git a/p.js b/p.js new file mode 100644 index 00000000..07c8325a --- /dev/null +++ b/p.js @@ -0,0 +1,9 @@ +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)); +} +f(); +console.log("immediate logging"); From eb6d86c7ba5a96888ebd2161c10bb6c41820bb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Cabrera?= Date: Mon, 8 Jul 2024 07:24:54 -0400 Subject: [PATCH 19/22] Improve cargo feature handling and crypto APIs definition 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. --- Makefile | 2 +- crates/core/Cargo.toml | 2 +- crates/core/src/runtime.rs | 13 +++-- crates/javy/Cargo.toml | 6 ++- crates/javy/src/apis/crypto/crypto.js | 17 ++++++ crates/javy/src/apis/crypto/mod.rs | 78 ++++++++++----------------- crates/javy/src/apis/mod.rs | 2 + crates/javy/src/config.rs | 1 + crates/javy/src/runtime.rs | 7 ++- p.js | 18 ++++--- 10 files changed, 76 insertions(+), 70 deletions(-) create mode 100644 crates/javy/src/apis/crypto/crypto.js diff --git a/Makefile b/Makefile index 2bdc5d98..1cfe0ad1 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index fc0e754f..ac4f370a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,4 +21,4 @@ javy-config = { workspace = true } [features] experimental_event_loop = [] -experimental_crypto = [] +crypto = ["experimental_event_loop", "javy/crypto"] diff --git a/crates/core/src/runtime.rs b/crates/core/src/runtime.rs index f5ee9cac..e6725d3f 100644 --- a/crates/core/src/runtime.rs +++ b/crates/core/src/runtime.rs @@ -4,7 +4,7 @@ use javy_config::Config as SharedConfig; pub(crate) fn new(shared_config: SharedConfig) -> Result { 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)) @@ -12,11 +12,10 @@ pub(crate) fn new(shared_config: SharedConfig) -> Result { // 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)) } diff --git a/crates/javy/Cargo.toml b/crates/javy/Cargo.toml index 515ba376..9ad6696f 100644 --- a/crates/javy/Cargo.toml +++ b/crates/javy/Cargo.toml @@ -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/" } @@ -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"] diff --git a/crates/javy/src/apis/crypto/crypto.js b/crates/javy/src/apis/crypto/crypto.js new file mode 100644 index 00000000..55029291 --- /dev/null +++ b/crates/javy/src/apis/crypto/crypto.js @@ -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"); +})(); diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 11924776..66c98121 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -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 { @@ -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>(()) } @@ -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> { +fn hmac_sha256(args: Args<'_>) -> Result> { let (ctx, args) = args.release(); if args.len() != 3 { @@ -47,54 +49,28 @@ fn hmac_sha256(args: Args<'_>) -> Result> { 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> { - 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 { type HmacSha256 = Hmac; - 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(); @@ -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; "#, )?; @@ -134,19 +109,24 @@ mod tests { let runtime = Runtime::new(Config::default())?; runtime.context().with(|this| { - let result= this.eval::, _>( + let result = this.eval::, _>( 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_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_script:2:21)\n", + e.to_string() + ); Ok::<_, Error>(()) })?; Ok(()) } - + #[test] fn test_crypto_digest_with_lossy_input() -> Result<()> { let mut config = Config::default(); diff --git a/crates/javy/src/apis/mod.rs b/crates/javy/src/apis/mod.rs index d440d6c5..9beb495d 100644 --- a/crates/javy/src/apis/mod.rs +++ b/crates/javy/src/apis/mod.rs @@ -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; @@ -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::*; diff --git a/crates/javy/src/config.rs b/crates/javy/src/config.rs index 0eec9c34..f211c96f 100644 --- a/crates/javy/src/config.rs +++ b/crates/javy/src/config.rs @@ -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 diff --git a/crates/javy/src/runtime.rs b/crates/javy/src/runtime.rs index 1cf29547..1947b3d0 100644 --- a/crates/javy/src/runtime.rs +++ b/crates/javy/src/runtime.rs @@ -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, }; @@ -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()) + } } }); diff --git a/p.js b/p.js index 07c8325a..2d092012 100644 --- a/p.js +++ b/p.js @@ -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(); + From 399dd95f9ffb98afb44b560c3268a1de0703a171 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 9 Jul 2024 22:05:31 -0400 Subject: [PATCH 20/22] Add crypto to test-javy make task This allows the crypto tests to run --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1cfe0ad1..efd39813 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ docs: cargo doc --package=javy-core --open --target=wasm32-wasi test-javy: - CARGO_TARGET_WASM32_WASI_RUNNER="wasmtime --dir=." cargo wasi test --package=javy --features json,messagepack -- --nocapture + CARGO_TARGET_WASM32_WASI_RUNNER="wasmtime --dir=." cargo wasi test --package=javy --features json,messagepack,crypto -- --nocapture test-core: cargo wasi test --package=javy-core -- --nocapture From b123c25fa90ca2b452fd4ff9464b4e0547e91d94 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Fri, 12 Jul 2024 21:21:40 -0400 Subject: [PATCH 21/22] Get tests working again --- crates/javy/src/apis/crypto/crypto.js | 2 +- crates/javy/src/apis/crypto/mod.rs | 49 ++++++++++++--------------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/crates/javy/src/apis/crypto/crypto.js b/crates/javy/src/apis/crypto/crypto.js index 55029291..002640b1 100644 --- a/crates/javy/src/apis/crypto/crypto.js +++ b/crates/javy/src/apis/crypto/crypto.js @@ -13,5 +13,5 @@ } globalThis.crypto = crypto; - Reflect.deleteProperty(globalThis, "__javy_cryptoSubtleSign"); + // Reflect.deleteProperty(globalThis, "__javy_cryptoSubtleSign"); })(); diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index 66c98121..f7afe3ff 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -80,25 +80,24 @@ fn hmac_sha256_result(secret: String, message: String) -> Result { #[cfg(test)] mod tests { - use crate::{from_js_error, quickjs::Value, Config, Runtime}; + use crate::{from_js_error, val_to_string, quickjs::Value, Config, Runtime}; use anyhow::{Error, Result}; #[test] - fn test_crypto_digest() -> Result<()> { + fn test_crypto_digest_internal() -> Result<()> { let mut config = Config::default(); config.crypto(true); let runtime = Runtime::new(config)?; runtime.context().with(|this| { - let result: Value<'_> = this.eval( + let result = this.eval::, _>( r#" - 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 }); - result === expectedHex; + const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; + //crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message").then(function(sig) { result = sig }); + __javy_cryptoSubtleSign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message"); "#, - )?; - assert!(result.as_bool().unwrap()); + ); + assert_eq!(val_to_string(&this, result.unwrap()).unwrap(), "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"); Ok::<_, Error>(()) })?; Ok(()) @@ -137,21 +136,12 @@ mod tests { let result = this.eval::, _>( r#" // matched tested behavior in node v18 - let expectedHex = "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"; - let result = null; - result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800") - // .then(function(signature) { - // result = signature; - // }); - // console.log(result); - // console.log("Match?", result === expectedHex); - result === expectedHex; + // result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800") + const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; + __javy_cryptoSubtleSign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800"); "#, - ); - // assert!(result.is_err()); - // let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - // assert_eq!("", e.to_string()); - assert!(result.unwrap().as_bool().unwrap()); + )?; + assert_eq!(val_to_string(&this, result).unwrap(), "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"); Ok::<_, Error>(()) })?; Ok(()) @@ -186,12 +176,15 @@ mod tests { runtime.context().with(|this| { let result= this.eval::, _>( r#" - let result = crypto.subtle.sign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + // let result = crypto.subtle.sign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; + __javy_cryptoSubtleSign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + "#, ); assert!(result.is_err()); let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:2:48 only name=HMAC supported\n at (eval_script:2:48)\n", e.to_string()); + assert_eq!("Error:4:21 only name=HMAC supported\n at (eval_script:4:21)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) @@ -206,12 +199,14 @@ mod tests { runtime.context().with(|this| { let result= this.eval::, _>( r#" - let result = crypto.subtle.sign({name: "HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + // let result = crypto.subtle.sign({name: "HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); + const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; + __javy_cryptoSubtleSign(this, "my secret and secure key", "input message"); "#, ); assert!(result.is_err()); let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:2:48 only hash=sha-256 supported\n at (eval_script:2:48)\n", e.to_string()); + assert_eq!("Error:4:21 only name=HMAC supported\n at (eval_script:4:21)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) From b667f96a86f07348e1739f57b641fe828d254362 Mon Sep 17 00:00:00 2001 From: Eric Walker Date: Tue, 16 Jul 2024 22:30:55 -0400 Subject: [PATCH 22/22] Rewrite tests against promises --- crates/javy/src/apis/crypto/mod.rs | 107 +++++++++++++++++++---------- 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/crates/javy/src/apis/crypto/mod.rs b/crates/javy/src/apis/crypto/mod.rs index f7afe3ff..3fd03956 100644 --- a/crates/javy/src/apis/crypto/mod.rs +++ b/crates/javy/src/apis/crypto/mod.rs @@ -80,8 +80,64 @@ fn hmac_sha256_result(secret: String, message: String) -> Result { #[cfg(test)] mod tests { - use crate::{from_js_error, val_to_string, quickjs::Value, Config, Runtime}; - use anyhow::{Error, Result}; + use crate::{ + from_js_error, quickjs::Ctx, quickjs::Error as JSError, quickjs::Value, val_to_string, + Config, Runtime, + }; + use anyhow::{bail, Error, Result}; + + fn extract_promise_to_string<'a>(ctx: Ctx<'a>, value: Value<'a>) -> Result { + match value.as_promise() { + Some(promise) => { + let resolved = promise.finish::(); + if let Err(JSError::WouldBlock) = resolved { + bail!("unexpected JSError::WouldBlock"); + } else { + Ok(val_to_string(&ctx, resolved.unwrap()).unwrap()) + } + } + None => { + bail!("Expected promise"); + } + } + } + + fn extract_promise_to_err<'a>(ctx: Ctx<'a>, value: Value<'a>) -> Result { + match value.as_promise() { + Some(promise) => { + let resolved = promise.finish::(); + if let Err(JSError::WouldBlock) = resolved { + bail!("unexpected JSError::WouldBlock"); + } else { + assert!(resolved.is_err()); + let e = resolved + .map_err(|e| from_js_error(ctx.clone(), e)) + .unwrap_err(); + Ok(e) + } + } + None => { + bail!("Expected promise"); + } + } + } + + #[test] + fn test_crypto_digest() -> Result<()> { + let mut config = Config::default(); + config.crypto(true); + let runtime = Runtime::new(config)?; + + runtime.context().with(|this| { + let value = this.eval::, _>( + r#"crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message");"#, + ); + let result = extract_promise_to_string(this.clone(), value.unwrap().clone()); + assert_eq!(result?, "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"); + Ok::<_, Error>(()) + })?; + Ok(()) + } #[test] fn test_crypto_digest_internal() -> Result<()> { @@ -91,11 +147,7 @@ mod tests { runtime.context().with(|this| { let result = this.eval::, _>( - r#" - const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; - //crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message").then(function(sig) { result = sig }); - __javy_cryptoSubtleSign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message"); - "#, + r#"globalThis.__javy_cryptoSubtleSign({name: "HMAC", hash: "sha-256"}, "my secret and secure key", "input message");"#, ); assert_eq!(val_to_string(&this, result.unwrap()).unwrap(), "97d2a569059bbcd8ead4444ff99071f4c01d005bcefe0d3567e1be628e5fdcd9"); Ok::<_, Error>(()) @@ -133,15 +185,11 @@ mod tests { let runtime = Runtime::new(config)?; runtime.context().with(|this| { - let result = this.eval::, _>( - r#" - // matched tested behavior in node v18 - // result = crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800") - const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; - __javy_cryptoSubtleSign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800"); - "#, - )?; - assert_eq!(val_to_string(&this, result).unwrap(), "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"); + let value = this.eval::, _>( + r#"crypto.subtle.sign({name: "HMAC", hash: "sha-256"}, "\uD800\uD800\uD800\uD800\uD800", "\uD800\uD800\uD800\uD800\uD800");"#, + ); + let result = extract_promise_to_string(this.clone(), value.unwrap().clone()); + assert_eq!(result?, "c06ae855290abd8f397af6975e9c2f72fe27a90a3e0f0bb73b4f991567501980"); Ok::<_, Error>(()) })?; Ok(()) @@ -174,17 +222,11 @@ mod tests { let runtime = Runtime::new(config)?; runtime.context().with(|this| { - let result= this.eval::, _>( - r#" - // let result = crypto.subtle.sign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); - const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; - __javy_cryptoSubtleSign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); - - "#, + let value = this.eval::, _>( + r#"crypto.subtle.sign({name: "not-HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message");"#, ); - assert!(result.is_err()); - let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:4:21 only name=HMAC supported\n at (eval_script:4:21)\n", e.to_string()); + let e = extract_promise_to_err(this.clone(), value.unwrap().clone())?; + assert_eq!("Error:11:15 only name=HMAC supported\n at (eval_script:11:15)\n at Promise (native)\n at (eval_script:12:12)\n at (eval_script:1:15)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(()) @@ -197,16 +239,11 @@ mod tests { let runtime = Runtime::new(config)?; runtime.context().with(|this| { - let result= this.eval::, _>( - r#" - // let result = crypto.subtle.sign({name: "HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message"); - const __javy_cryptoSubtleSign = globalThis.__javy_cryptoSubtleSign; - __javy_cryptoSubtleSign(this, "my secret and secure key", "input message"); - "#, + let value = this.eval::, _>( + r#"crypto.subtle.sign({name: "HMAC", hash: "not-sha-256"}, "my secret and secure key", "input message");"#, ); - assert!(result.is_err()); - let e = result.map_err(|e| from_js_error(this.clone(), e)).unwrap_err(); - assert_eq!("Error:4:21 only name=HMAC supported\n at (eval_script:4:21)\n", e.to_string()); + let e = extract_promise_to_err(this.clone(), value.unwrap().clone())?; + assert_eq!("Error:11:15 only hash=sha-256 supported\n at (eval_script:11:15)\n at Promise (native)\n at (eval_script:12:12)\n at (eval_script:1:15)\n", e.to_string()); Ok::<_, Error>(()) })?; Ok(())