diff --git a/TODO.md b/TODO.md index e069c54..ee934ce 100644 --- a/TODO.md +++ b/TODO.md @@ -68,8 +68,8 @@ i want to implement those API's as well. - [x] AbortController - [x] AbortSignal -- [ ] Crypto -- [ ] CryptoKey +- [x] Crypto +- [x] CryptoKey - [x] Blob - [ ] ByteLengthQueuingStrategy @@ -85,25 +85,25 @@ i want to implement those API's as well. - [x] async - [x] sync -- [ ] FormData -- [ ] Headers +- [x] FormData +- [x] Headers - [ ] ReadableByteStreamController - [ ] ReadableStream - [ ] ReadableStreamBYOBReader - [ ] ReadableStreamBYOBRequest - [ ] ReadableStreamDefaultController - [ ] ReadableStreamDefaultReader -- [ ] Request -- [ ] Response -- [ ] SubtleCrypto -- [ ] TextDecoder +- [x] Request +- [x] Response +- [x] SubtleCrypto +- [x] TextDecoder - [ ] TextDecoderStream -- [ ] TextEncoder +- [x] TextEncoder - [ ] TextEncoderStream - [ ] TransformStream - [ ] TransformStreamDefaultController -- [ ] URL -- [ ] URLSearchParams +- [x] URL +- [x] URLSearchParams - [ ] WritableStream - [ ] WritableStreamDefaultController @@ -115,7 +115,7 @@ i want to implement those API's as well. - [ ] WebAssembly.Table Global methods / properties: -- [ ] globalThis +- [x] globalThis - [x] globalThis.atob() - [x] globalThis.btoa() - [x] globalThis.console @@ -124,15 +124,15 @@ Global methods / properties: - and also easier to implement OTEL later - TODO: console.span(level: "info" | "debug" | "warn" | "error" | "trace", name: string, fn: (span) => void) - TODO: console.table -- [ ] globalThis.crypto +- [x] globalThis.crypto - [x] globalThis.fetch() - [x] globalThis.navigator.userAgent -- [ ] globalThis.performance.now() -- [ ] globalThis.performance.timeOrigin +- [x] globalThis.performance.now() +- [x] globalThis.performance.timeOrigin - [x] globalThis.queueMicrotask() - [x] globalThis.setTimeout() / globalThis.clearTimeout() - [x] globalThis.setInterval() / globalThis.clearInterval() -- [ ] globalThis.structuredClone() +- [x] globalThis.structuredClone() - [ ] WASM - [ ] globalThis.WebAssembly.compile() diff --git a/modules/Cargo.toml b/modules/Cargo.toml index 2a3d92f..3fa5175 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -14,11 +14,19 @@ tracing = "0.1.44" itoa = "1.0.15" ryu = "1.0.20" simd-json = "0.17.0" -rand = "0.9.2" +rand_core = { version = "=0.10.0-rc-2" } +rand = { version = "0.10.0-rc.2", features = [ + "std", + "std_rng", + "thread_rng", +], default-features = false } tokio = { version = "1", features = ["full"], optional = true } # intl -chrono = { version = "0.4", features = ["std", "clock"], default-features = false, optional = true } +chrono = { version = "0.4", features = [ + "std", + "clock", +], default-features = false, optional = true } chrono-tz = { version = "0.10", default-features = false, optional = true } iana-time-zone = { version = "0.1", optional = true } @@ -71,6 +79,61 @@ flate2 = { version = "1", features = ["miniz_oxide"], default-features = false } zstd = { version = "0.13", default-features = false } +# crypto +crc32c = { version = "0.6", default-features = false } +crc32fast = { version = "1", default-features = false } +aes = { version = "0.9.0-rc.2" } +aes-gcm = { version = "0.11.0-rc.2", features = [ + "alloc", +], default-features = false } +aes-kw = { version = "0.3.0-rc.1", features = [ + "oid", +], default-features = false } +cbc = { version = "0.2.0-rc.2", features = ["alloc"] } +const-oid = { version = "0.10", features = ["db"], default-features = false } +ctr = { version = "0.10.0-rc.2", default-features = false } +der = { version = "0.8.0-rc.10", features = [ + "derive", + "alloc", +], default-features = false } +ecdsa = { version = "0.17.0-rc.9", default-features = false } +elliptic-curve = { version = "0.14.0-rc.17", features = [ + "alloc", +], default-features = false } +md-5 = { version = "0.11.0-rc.3", default-features = false } +rsa = { version = "0.10.0-rc.10", features = [ + "std", + "sha2", + "encoding", + "os_rng", +], default-features = false } +p256 = { version = "0.14.0-rc.1", features = [ + "ecdh", + "ecdsa", + "pkcs8", +], default-features = false } +p384 = { version = "0.14.0-rc.1", features = [ + "ecdh", + "ecdsa", + "pkcs8", +], default-features = false } +p521 = { version = "0.14.0-rc.1", features = [ + "ecdh", + "ecdsa", + "pkcs8", +], default-features = false } +pkcs8 = { version = "0.11.0-rc.8", default-features = false, features = [ + "alloc", +] } +spki = { version = "0.8.0-rc.4", features = [ + "alloc", +], default-features = false } +x25519-dalek = { version = "3.0.0-pre.3", features = [ + "static_secrets", + "zeroize", +], default-features = false } + + [features] default = [ "event", @@ -84,8 +147,10 @@ default = [ "url", "fetch", "intl", + "crypto", ] +crypto = [] event = [] abort = ["event"] console = [] diff --git a/modules/src/crypto/crc32.rs b/modules/src/crypto/crc32.rs new file mode 100644 index 0000000..8ea14ca --- /dev/null +++ b/modules/src/crypto/crc32.rs @@ -0,0 +1,69 @@ +use std::hash::Hasher; + +use crate::utils::bytes::ObjectBytes; +use crc32c::Crc32cHasher; +use rsquickjs::{prelude::This, Class, Ctx, Result}; + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct Crc32c { + #[qjs(skip_trace)] + hasher: crc32c::Crc32cHasher, +} + +#[rsquickjs::methods] +impl Crc32c { + #[qjs(constructor)] + fn new() -> Self { + Self { + hasher: Crc32cHasher::default(), + } + } + + #[qjs(rename = "digest")] + fn crc32c_digest(&self) -> u64 { + self.hasher.finish() + } + + #[qjs(rename = "update")] + fn crc32c_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); + Ok(this.0) + } +} + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct Crc32 { + #[qjs(skip_trace)] + hasher: crc32fast::Hasher, +} + +#[rsquickjs::methods] +impl Crc32 { + #[qjs(constructor)] + fn new() -> Self { + Self { + hasher: crc32fast::Hasher::new(), + } + } + + #[qjs(rename = "digest")] + fn crc32_digest(&self) -> u64 { + self.hasher.finish() + } + + #[qjs(rename = "update")] + fn crc32_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); + Ok(this.0) + } +} diff --git a/modules/src/crypto/md5_hash.rs b/modules/src/crypto/md5_hash.rs new file mode 100644 index 0000000..b0b3ed9 --- /dev/null +++ b/modules/src/crypto/md5_hash.rs @@ -0,0 +1,43 @@ +use crate::utils::bytes::{bytes_to_typed_array, ObjectBytes}; +use md5::{Digest as Md5Digest, Md5 as MdHasher}; +use rsquickjs::{function::Opt, prelude::This, Class, Ctx, Result, Value}; + +use super::encoded_bytes; + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct Md5 { + #[qjs(skip_trace)] + hasher: MdHasher, +} + +#[rsquickjs::methods] +impl Md5 { + #[qjs(constructor)] + fn new() -> Self { + Self { + hasher: MdHasher::new(), + } + } + + #[qjs(rename = "digest")] + fn md5_digest<'js>(&self, ctx: Ctx<'js>, encoding: Opt) -> Result> { + let digest = self.hasher.clone().finalize(); + let bytes: &[u8] = digest.as_ref(); + + match encoding.0 { + Some(encoding) => encoded_bytes(ctx, bytes, &encoding), + None => bytes_to_typed_array(ctx, bytes), + } + } + + #[qjs(rename = "update")] + fn md5_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().hasher.update(bytes.as_bytes(&ctx)?); + Ok(this.0) + } +} diff --git a/modules/src/crypto/mod.rs b/modules/src/crypto/mod.rs new file mode 100644 index 0000000..ecd4052 --- /dev/null +++ b/modules/src/crypto/mod.rs @@ -0,0 +1,341 @@ +mod crc32; +mod md5_hash; +mod sha_hash; +mod subtle; + +use std::slice; +use std::sync::LazyLock; + +use crate::buffer::Buffer; +use crate::utils::bytes::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER}; +use crate::utils::ctx::CtxExtension; +use crate::utils::encoding::{bytes_to_b64_string, bytes_to_hex_string}; +use crate::utils::{ + bytes::{bytes_to_typed_array, get_start_end_indexes, ObjectBytes}, + error::ErrorExtensions, + module::{export_default, ModuleInfo}, + result::ResultExt, +}; +use rand::Rng; +use ring::rand::{SecureRandom, SystemRandom}; +use rsquickjs::prelude::Async; +use rsquickjs::{ + atom::PredefinedAtom, + function::{Constructor, Opt}, + module::{Declarations, Exports, ModuleDef}, + prelude::{Func, Rest}, + Class, Ctx, Error, Exception, Function, IntoJs, Null, Object, Result, Value, +}; +use subtle::{ + subtle_decrypt, subtle_derive_bits, subtle_derive_key, subtle_digest, subtle_encrypt, + subtle_export_key, subtle_generate_key, subtle_import_key, subtle_sign, subtle_unwrap_key, + subtle_verify, subtle_wrap_key, CryptoKey, SubtleCrypto, +}; + +use self::{ + crc32::{Crc32, Crc32c}, + md5_hash::Md5, + sha_hash::{Hash, Hmac, ShaAlgorithm, ShaHash}, +}; + +pub static SYSTEM_RANDOM: LazyLock = LazyLock::new(SystemRandom::new); + +fn encoded_bytes<'js>(ctx: Ctx<'js>, bytes: &[u8], encoding: &str) -> Result> { + match encoding { + "hex" => { + let hex = bytes_to_hex_string(bytes); + let hex = rsquickjs::String::from_str(ctx, &hex)?; + Ok(Value::from_string(hex)) + } + "base64" => { + let b64 = bytes_to_b64_string(bytes); + let b64 = rsquickjs::String::from_str(ctx, &b64)?; + Ok(Value::from_string(b64)) + } + _ => bytes_to_typed_array(ctx, bytes), + } +} + +#[inline] +pub fn random_byte_array(length: usize) -> Vec { + let mut vec = vec![0; length]; + SYSTEM_RANDOM.fill(&mut vec).unwrap(); + vec +} + +fn get_random_bytes(ctx: Ctx, length: usize) -> Result { + let random_bytes = random_byte_array(length); + Buffer(random_bytes).into_js(&ctx) +} + +fn get_random_int(first: i64, second: Opt) -> Result { + let mut rng = rand::rng(); + let random_number = match second.0 { + Some(max) => rng.random_range(first..max), + None => rng.random_range(0..first), + }; + + Ok(random_number) +} + +fn random_fill<'js>(ctx: Ctx<'js>, obj: Object<'js>, args: Rest>) -> Result<()> { + let args_iter = args.0.into_iter(); + let mut args_iter = args_iter.rev(); + + let callback: Function = args_iter + .next() + .and_then(|v| v.into_function()) + .or_throw_msg(&ctx, "Callback required")?; + let size = args_iter + .next() + .and_then(|arg| arg.as_int()) + .map(|i| i as usize); + let offset = args_iter + .next() + .and_then(|arg| arg.as_int()) + .map(|i| i as usize); + + ctx.clone().spawn_exit(async move { + if let Err(err) = random_fill_sync(ctx.clone(), obj.clone(), Opt(offset), Opt(size)) { + let err = err.into_value(&ctx)?; + () = callback.call((err,))?; + + return Ok(()); + } + () = callback.call((Null.into_js(&ctx), obj))?; + Ok::<_, Error>(()) + })?; + Ok(()) +} + +fn random_fill_sync<'js>( + ctx: Ctx<'js>, + obj: Object<'js>, + offset: Opt, + size: Opt, +) -> Result> { + let offset = offset.unwrap_or(0); + + if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { + let (array_buffer, source_length, source_offset) = object_bytes + .get_array_buffer()? + .expect(ERROR_MSG_NOT_ARRAY_BUFFER); + let raw = array_buffer + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(&ctx)?; + + let (start, end) = get_start_end_indexes(source_length, size.0, offset); + + let bytes = unsafe { slice::from_raw_parts_mut(raw.ptr.as_ptr(), source_length) }; + + SYSTEM_RANDOM + .fill(&mut bytes[start + source_offset..end - source_offset]) + .unwrap(); + } + + Ok(obj) +} + +fn get_random_values<'js>(ctx: Ctx<'js>, obj: Object<'js>) -> Result> { + if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { + if matches!( + object_bytes, + ObjectBytes::F64Array(_) | ObjectBytes::F32Array(_) + ) { + return Err(Exception::throw_message(&ctx, "Unsupported TypedArray")); + } + + let (array_buffer, source_length, source_offset) = object_bytes + .get_array_buffer()? + .expect(ERROR_MSG_NOT_ARRAY_BUFFER); + let raw = array_buffer + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(&ctx)?; + + if source_length > 0x10000 { + return Err(Exception::throw_message( + &ctx, + "QuotaExceededError: The requested length exceeds 65,536 bytes", + )); + } + + let bytes = unsafe { + std::slice::from_raw_parts_mut(raw.ptr.as_ptr().add(source_offset), source_length) + }; + + SYSTEM_RANDOM.fill(bytes).unwrap() + } + + Ok(obj) +} + +fn uuidv4() -> String { + let uuid = rand::random::() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000; + + static HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; + let bytes = uuid.to_be_bytes(); + + let mut buf = [0u8; 36]; + + // Precomputed positions for 32 hex digits (excluding hyphens) + static HEX_POS: [usize; 32] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, + ]; + + // Map each byte to its hex representation + let mut hex_idx = 0; + for &byte in &bytes[..] { + let high = HEX_CHARS[(byte >> 4) as usize]; + let low = HEX_CHARS[(byte & 0x0f) as usize]; + + buf[HEX_POS[hex_idx]] = high; + buf[HEX_POS[hex_idx + 1]] = low; + hex_idx += 2; + } + + // Insert hyphens at standard positions + buf[8] = b'-'; + buf[13] = b'-'; + buf[18] = b'-'; + buf[23] = b'-'; + + // SAFETY: The buffer only contains valid UTF-8 characters (hex digits and hyphens) + // that were explicitly set from the HEX_CHARS array and hyphen literals + unsafe { String::from_utf8_unchecked(buf.to_vec()) } +} + +#[rsquickjs::class] +#[derive(rsquickjs::JsLifetime, rsquickjs::class::Trace)] +struct Crypto {} + +#[rsquickjs::methods] +impl Crypto { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get, rename = PredefinedAtom::SymbolToStringTag)] + pub fn to_string_tag(&self) -> &'static str { + stringify!(Crypto) + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + Class::::define(&globals)?; + let crypto = Class::instance(ctx.clone(), Crypto {})?; + + crypto.set("createHash", Func::from(Hash::new))?; + crypto.set("createHmac", Func::from(Hmac::new))?; + crypto.set("randomBytes", Func::from(get_random_bytes))?; + crypto.set("randomInt", Func::from(get_random_int))?; + crypto.set("randomUUID", Func::from(uuidv4))?; + crypto.set("randomFillSync", Func::from(random_fill_sync))?; + crypto.set("randomFill", Func::from(random_fill))?; + crypto.set("getRandomValues", Func::from(get_random_values))?; + + { + Class::::define(&globals)?; + Class::::define(&globals)?; + + let subtle = Class::instance(ctx.clone(), SubtleCrypto {})?; + + subtle.set("decrypt", Func::from(Async(subtle_decrypt)))?; + subtle.set("deriveKey", Func::from(Async(subtle_derive_key)))?; + subtle.set("deriveBits", Func::from(Async(subtle_derive_bits)))?; + subtle.set("digest", Func::from(Async(subtle_digest)))?; + subtle.set("encrypt", Func::from(Async(subtle_encrypt)))?; + subtle.set("exportKey", Func::from(Async(subtle_export_key)))?; + subtle.set("generateKey", Func::from(Async(subtle_generate_key)))?; + subtle.set("importKey", Func::from(Async(subtle_import_key)))?; + subtle.set("sign", Func::from(Async(subtle_sign)))?; + subtle.set("verify", Func::from(Async(subtle_verify)))?; + subtle.set("wrapKey", Func::from(Async(subtle_wrap_key)))?; + subtle.set("unwrapKey", Func::from(Async(subtle_unwrap_key)))?; + crypto.set("subtle", subtle)?; + } + + globals.set("crypto", crypto)?; + + Ok(()) +} + +pub struct CryptoModule; + +impl ModuleDef for CryptoModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("createHash")?; + declare.declare("createHmac")?; + declare.declare("Crc32")?; + declare.declare("Crc32c")?; + declare.declare("Md5")?; + declare.declare("randomBytes")?; + declare.declare("randomUUID")?; + declare.declare("randomInt")?; + declare.declare("randomFillSync")?; + declare.declare("randomFill")?; + declare.declare("getRandomValues")?; + + for sha_algorithm in ShaAlgorithm::iter() { + let class_name = sha_algorithm.class_name(); + declare.declare(class_name)?; + } + declare.declare("crypto")?; + declare.declare("webcrypto")?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + for sha_algorithm in ShaAlgorithm::iter() { + let class_name: &str = sha_algorithm.class_name(); + let algo = sha_algorithm; + + let ctor = + Constructor::new_class::(ctx.clone(), move |ctx, secret| { + struct Args<'js>(Ctx<'js>, Opt>); + let Args(ctx, secret) = Args(ctx, secret); + ShaHash::new(ctx, algo.clone(), secret) + })?; + + default.set(class_name, ctor)?; + } + + let crypto: Object = ctx.globals().get("crypto")?; + + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + + default.set("createHash", Func::from(Hash::new))?; + default.set("createHmac", Func::from(Hmac::new))?; + default.set("randomBytes", Func::from(get_random_bytes))?; + default.set("randomInt", Func::from(get_random_int))?; + default.set("randomUUID", Func::from(uuidv4))?; + default.set("randomFillSync", Func::from(random_fill_sync))?; + default.set("randomFill", Func::from(random_fill))?; + default.set("getRandomValues", Func::from(get_random_values))?; + default.set("crypto", crypto.clone())?; + default.set("webcrypto", crypto)?; + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: CryptoModule) -> Self { + ModuleInfo { + name: "crypto", + module: val, + } + } +} diff --git a/modules/src/crypto/sha_hash.rs b/modules/src/crypto/sha_hash.rs new file mode 100644 index 0000000..48f7321 --- /dev/null +++ b/modules/src/crypto/sha_hash.rs @@ -0,0 +1,234 @@ +use crate::{ + iterable_enum, + utils::{ + bytes::{bytes_to_typed_array, ObjectBytes}, + result::ResultExt, + }, +}; +use ring::{ + digest::{self, Context as DigestContext}, + hmac::{self, Context as HmacContext}, +}; +use rsquickjs::{function::Opt, prelude::This, Class, Ctx, Result, Value}; + +use super::encoded_bytes; + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct Hmac { + #[qjs(skip_trace)] + context: HmacContext, +} + +#[rsquickjs::methods] +impl Hmac { + #[qjs(skip)] + pub fn new<'js>(ctx: Ctx<'js>, algorithm: String, key_value: ObjectBytes<'js>) -> Result { + let algorithm = ShaAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; + let algorithm = *algorithm.hmac_algorithm(); + + Ok(Self { + context: HmacContext::with_key(&hmac::Key::new(algorithm, key_value.as_bytes(&ctx)?)), + }) + } + + fn digest<'js>(&self, ctx: Ctx<'js>, encoding: Opt) -> Result> { + let signature = self.context.clone().sign(); + let bytes: &[u8] = signature.as_ref(); + + match encoding.into_inner() { + Some(encoding) => encoded_bytes(ctx, bytes, &encoding), + None => bytes_to_typed_array(ctx, bytes), + } + } + + fn update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + let bytes = bytes.as_bytes(&ctx)?; + this.0.borrow_mut().context.update(bytes); + + Ok(this.0) + } +} + +impl Clone for Hmac { + fn clone(&self) -> Self { + Self { + context: self.context.clone(), + } + } +} + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct Hash { + #[qjs(skip_trace)] + context: DigestContext, +} + +#[rsquickjs::methods] +impl Hash { + #[qjs(skip)] + pub fn new(ctx: Ctx<'_>, algorithm: String) -> Result { + let algorithm = ShaAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; + let algorithm = algorithm.digest_algorithm(); + + Ok(Self { + context: DigestContext::new(algorithm), + }) + } + + #[qjs(rename = "digest")] + fn hash_digest<'js>(&self, ctx: Ctx<'js>, encoding: Opt) -> Result> { + let digest = self.context.clone().finish(); + let bytes: &[u8] = digest.as_ref(); + + match encoding.0 { + Some(encoding) => encoded_bytes(ctx, bytes, &encoding), + None => bytes_to_typed_array(ctx, bytes), + } + } + + #[qjs(rename = "update")] + fn hash_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + let bytes = bytes.as_bytes(&ctx)?; + this.0.borrow_mut().context.update(bytes); + Ok(this.0) + } +} + +#[derive(Debug, Clone)] +pub enum ShaAlgorithm { + SHA1, + SHA256, + SHA384, + SHA512, +} + +iterable_enum!(ShaAlgorithm, SHA1, SHA256, SHA384, SHA512); + +impl ShaAlgorithm { + pub fn class_name(&self) -> &'static str { + match self { + ShaAlgorithm::SHA1 => "Sha1", + ShaAlgorithm::SHA256 => "Sha256", + ShaAlgorithm::SHA384 => "Sha384", + ShaAlgorithm::SHA512 => "Sha512", + } + } + pub fn hmac_algorithm(&self) -> &'static hmac::Algorithm { + match self { + ShaAlgorithm::SHA1 => &hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + ShaAlgorithm::SHA256 => &hmac::HMAC_SHA256, + ShaAlgorithm::SHA384 => &hmac::HMAC_SHA384, + ShaAlgorithm::SHA512 => &hmac::HMAC_SHA512, + } + } + + pub fn digest_algorithm(&self) -> &'static digest::Algorithm { + match self { + ShaAlgorithm::SHA1 => &digest::SHA1_FOR_LEGACY_USE_ONLY, + ShaAlgorithm::SHA256 => &digest::SHA256, + ShaAlgorithm::SHA384 => &digest::SHA384, + ShaAlgorithm::SHA512 => &digest::SHA512, + } + } + + pub fn as_str(&self) -> &'static str { + match self { + ShaAlgorithm::SHA1 => "SHA-1", + ShaAlgorithm::SHA256 => "SHA-256", + ShaAlgorithm::SHA384 => "SHA-384", + ShaAlgorithm::SHA512 => "SHA-512", + } + } + + pub fn as_numeric_str(&self) -> &'static str { + match self { + ShaAlgorithm::SHA1 => "1", + ShaAlgorithm::SHA256 => "256", + ShaAlgorithm::SHA384 => "384", + ShaAlgorithm::SHA512 => "512", + } + } +} + +impl TryFrom<&str> for ShaAlgorithm { + type Error = String; + fn try_from(s: &str) -> std::result::Result { + Ok(match s.to_ascii_uppercase().as_str() { + "SHA1" => ShaAlgorithm::SHA1, + "SHA-1" => ShaAlgorithm::SHA1, + "SHA256" => ShaAlgorithm::SHA256, + "SHA-256" => ShaAlgorithm::SHA256, + "SHA384" => ShaAlgorithm::SHA384, + "SHA-384" => ShaAlgorithm::SHA384, + "SHA512" => ShaAlgorithm::SHA512, + "SHA-512" => ShaAlgorithm::SHA512, + _ => return Err(["'", s, "' not available"].concat()), + }) + } +} + +impl AsRef for ShaAlgorithm { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct ShaHash { + #[qjs(skip_trace)] + secret: Option>, + #[qjs(skip_trace)] + bytes: Vec, + #[qjs(skip_trace)] + algorithm: ShaAlgorithm, +} + +#[rsquickjs::methods] +impl ShaHash { + #[qjs(skip)] + pub fn new(ctx: Ctx, algorithm: ShaAlgorithm, secret: Opt>) -> Result { + let secret = secret.0.map(|bytes| bytes.into_bytes(&ctx)).transpose()?; + + Ok(ShaHash { + secret, + bytes: Vec::new(), + algorithm, + }) + } + + #[qjs(rename = "digest")] + fn sha_digest<'js>(&self, ctx: Ctx<'js>) -> Result> { + if let Some(secret) = &self.secret { + let key_value = secret; + let key = hmac::Key::new(*self.algorithm.hmac_algorithm(), key_value); + + return bytes_to_typed_array(ctx, hmac::sign(&key, &self.bytes).as_ref()); + } + + bytes_to_typed_array( + ctx, + digest::digest(self.algorithm.digest_algorithm(), &self.bytes).as_ref(), + ) + } + + #[qjs(rename = "update")] + fn sha_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().bytes = bytes.try_into().or_throw(&ctx)?; + Ok(this.0) + } +} diff --git a/modules/src/crypto/subtle/crypto_key.rs b/modules/src/crypto/subtle/crypto_key.rs new file mode 100644 index 0000000..847fed0 --- /dev/null +++ b/modules/src/crypto/subtle/crypto_key.rs @@ -0,0 +1,111 @@ +use std::rc::Rc; + +use crate::str_enum; +use rsquickjs::{ + atom::PredefinedAtom, + class::{Trace, Tracer}, + Ctx, Exception, Result, Value, +}; + +use super::key_algorithm::KeyAlgorithm; + +#[derive(PartialEq)] +pub enum KeyKind { + Secret, + Private, + Public, +} + +str_enum!(KeyKind,Secret => "secret", Private => "private", Public => "public"); + +#[rsquickjs::class] +#[derive(rsquickjs::JsLifetime)] +pub struct CryptoKey { + pub kind: KeyKind, + pub extractable: bool, + pub algorithm: KeyAlgorithm, + pub name: Box, + pub usages: Vec, + pub handle: Rc<[u8]>, +} + +impl CryptoKey { + pub fn new( + kind: KeyKind, + name: N, + extractable: bool, + algorithm: KeyAlgorithm, + usages: Vec, + handle: H, + ) -> Self + where + N: Into>, + H: Into>, + { + Self { + kind, + extractable, + algorithm, + name: name.into(), + usages, + handle: handle.into(), + } + } +} + +impl<'js> Trace<'js> for CryptoKey { + fn trace<'a>(&self, _: Tracer<'a, 'js>) {} +} + +#[rsquickjs::methods] +impl CryptoKey { + #[qjs(constructor)] + fn constructor(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get, rename = "type")] + pub fn get_type(&self) -> &str { + self.kind.as_str() + } + + #[qjs(get)] + pub fn extractable(&self) -> bool { + self.extractable + } + + #[qjs(get, rename = PredefinedAtom::SymbolToStringTag)] + pub fn to_string_tag(&self) -> &'static str { + stringify!(CryptoKey) + } + + #[qjs(get)] + pub fn algorithm<'js>(&self, ctx: Ctx<'js>) -> Result> { + self.algorithm + .as_object(&ctx, self.name.as_ref()) + .map(|a| a.into_value()) + } + + #[qjs(get)] + pub fn usages(&self) -> Vec { + self.usages.clone() + } +} + +impl CryptoKey { + pub fn check_validity(&self, usage: &str) -> std::result::Result<(), String> { + for key in self.usages.iter() { + if key == usage { + return Ok(()); + } + } + Err([ + "CryptoKey with '", + self.name.as_ref(), + "', doesn't support '", + usage, + "'", + ] + .concat()) + } +} diff --git a/modules/src/crypto/subtle/derive.rs b/modules/src/crypto/subtle/derive.rs new file mode 100644 index 0000000..330ac47 --- /dev/null +++ b/modules/src/crypto/subtle/derive.rs @@ -0,0 +1,213 @@ +use std::num::NonZeroU32; + +use crate::utils::result::ResultExt; +use p256::pkcs8::DecodePrivateKey; +use ring::{hkdf, pbkdf2}; +use rsquickjs::{Array, ArrayBuffer, Class, Ctx, Exception, Result, Value}; + +use crate::crypto::{ + sha_hash::ShaAlgorithm, + subtle::{CryptoKey, EllipticCurve}, +}; + +use super::{ + algorithm_mismatch_error, algorithm_not_supported_error, + crypto_key::KeyKind, + derive_algorithm::DeriveAlgorithm, + key_algorithm::{ + EcAlgorithm, KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages, KeyDerivation, + }, +}; + +struct HkdfOutput(usize); + +impl hkdf::KeyType for HkdfOutput { + fn len(&self) -> usize { + self.0 + } +} + +pub async fn subtle_derive_bits<'js>( + ctx: Ctx<'js>, + algorithm: DeriveAlgorithm, + base_key: Class<'js, CryptoKey>, + length: u32, +) -> Result> { + let base_key = base_key.borrow(); + base_key.check_validity("deriveBits").or_throw(&ctx)?; + + let bytes = derive_bits(&ctx, &algorithm, &base_key, length)?; + ArrayBuffer::new(ctx, bytes) +} + +fn derive_bits( + ctx: &Ctx<'_>, + algorithm: &DeriveAlgorithm, + base_key: &CryptoKey, + length: u32, +) -> Result> { + let bits = match algorithm { + DeriveAlgorithm::Ecdh { curve, public_key } => { + if let KeyAlgorithm::Ec { + curve: base_key_curve, + algorithm, + } = &base_key.algorithm + { + if curve == base_key_curve + && base_key.kind == KeyKind::Private + && matches!(algorithm, EcAlgorithm::Ecdh) + { + let handle = &base_key.handle; + return Ok(match curve { + EllipticCurve::P256 => { + let secret_key = + p256::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + let public_key = + p256::PublicKey::from_sec1_bytes(public_key).or_throw(ctx)?; + let shared_secret = p256::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + shared_secret.raw_secret_bytes().to_vec() + } + EllipticCurve::P384 => { + let secret_key = + p384::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + let public_key = + p384::PublicKey::from_sec1_bytes(public_key).or_throw(ctx)?; + let shared_secret = p384::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + shared_secret.raw_secret_bytes().to_vec() + } + EllipticCurve::P521 => { + let secret_key = + p521::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + let public_key = + p521::PublicKey::from_sec1_bytes(public_key).or_throw(ctx)?; + let shared_secret = p521::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + shared_secret.raw_secret_bytes().to_vec() + } + }); + } + return Err(Exception::throw_message( + ctx, + "ECDH curve must be same as baseKey", + )); + } + return algorithm_mismatch_error(ctx, "ECDH"); + } + DeriveAlgorithm::X25519 { public_key } => { + if !matches!(base_key.algorithm, KeyAlgorithm::X25519) { + return algorithm_mismatch_error(ctx, "X25519"); + } + + let private_array: [u8; 32] = base_key.handle.as_ref().try_into().or_throw(ctx)?; + let public_array: [u8; 32] = public_key.as_ref().try_into().or_throw(ctx)?; + let secret_key = x25519_dalek::StaticSecret::from(private_array); + let public_key = x25519_dalek::PublicKey::from(public_array); + let shared_secret = secret_key.diffie_hellman(&public_key); + shared_secret.as_bytes().to_vec() + } + DeriveAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { + if !matches!(base_key.algorithm, KeyAlgorithm::HkdfImport) { + return algorithm_mismatch_error(ctx, "HKDF"); + } + let hash_algorithm = match hash { + ShaAlgorithm::SHA1 => hkdf::HKDF_SHA1_FOR_LEGACY_USE_ONLY, + ShaAlgorithm::SHA256 => hkdf::HKDF_SHA256, + ShaAlgorithm::SHA384 => hkdf::HKDF_SHA384, + ShaAlgorithm::SHA512 => hkdf::HKDF_SHA512, + }; + let salt = hkdf::Salt::new(hash_algorithm, salt); + let info: &[&[u8]] = &[&info[..]]; + let prk = salt.extract(&base_key.handle); + let out_length = (length / 8).try_into().or_throw(ctx)?; + let okm = prk + .expand(info, HkdfOutput((length / 8).try_into().or_throw(ctx)?)) + .or_throw(ctx)?; + let mut out = vec![0u8; out_length]; + okm.fill(&mut out).or_throw(ctx)?; + + out + } + DeriveAlgorithm::Derive(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) => { + if !matches!(base_key.algorithm, KeyAlgorithm::Pbkdf2Import) { + return algorithm_mismatch_error(ctx, "PBKDF2"); + } + let hash_algorithm = match hash { + ShaAlgorithm::SHA1 => pbkdf2::PBKDF2_HMAC_SHA1, + ShaAlgorithm::SHA256 => pbkdf2::PBKDF2_HMAC_SHA256, + ShaAlgorithm::SHA384 => pbkdf2::PBKDF2_HMAC_SHA384, + ShaAlgorithm::SHA512 => pbkdf2::PBKDF2_HMAC_SHA512, + }; + + let mut out = vec![0; (length / 8).try_into().or_throw(ctx)?]; + let not_zero_iterations = NonZeroU32::new(*iterations) + .ok_or_else(|| Exception::throw_message(ctx, "Iterations are zero"))?; + pbkdf2::derive( + hash_algorithm, + not_zero_iterations, + salt, + &base_key.handle, + &mut out, + ); + + out + } + }; + Ok(bits) +} + +pub async fn subtle_derive_key<'js>( + ctx: Ctx<'js>, + algorithm: DeriveAlgorithm, + base_key: Class<'js, CryptoKey>, + derived_key_algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result> { + let KeyAlgorithmWithUsages { + algorithm: derived_key_algorithm, + name, + public_usages, + .. + } = KeyAlgorithm::from_js( + &ctx, + KeyAlgorithmMode::Derive, + derived_key_algorithm, + key_usages, + )?; + + let length = match &derived_key_algorithm { + KeyAlgorithm::Aes { length } => *length, + KeyAlgorithm::Hmac { length, .. } => *length, + KeyAlgorithm::Derive { .. } => 0, + _ => { + return algorithm_not_supported_error(&ctx); + } + }; + + let base_key = &base_key.borrow(); + + let bytes = derive_bits(&ctx, &algorithm, base_key, length as u32)?; + + let key = CryptoKey::new( + KeyKind::Secret, + name, + extractable, + derived_key_algorithm, + public_usages, + bytes, + ); + + Class::instance(ctx, key) +} diff --git a/modules/src/crypto/subtle/derive_algorithm.rs b/modules/src/crypto/subtle/derive_algorithm.rs new file mode 100644 index 0000000..f1e651c --- /dev/null +++ b/modules/src/crypto/subtle/derive_algorithm.rs @@ -0,0 +1,66 @@ +use std::rc::Rc; + +use crate::utils::object::ObjectExt; +use rsquickjs::{Class, Ctx, Exception, FromJs, Result, Value}; + +use super::{ + algorithm_mismatch_error, + key_algorithm::{KeyAlgorithm, KeyDerivation}, + CryptoKey, EllipticCurve, +}; + +#[derive(Debug)] +pub enum DeriveAlgorithm { + X25519 { + public_key: Rc<[u8]>, + }, + Ecdh { + curve: EllipticCurve, + public_key: Rc<[u8]>, + }, + Derive(KeyDerivation), +} + +impl<'js> FromJs<'js> for DeriveAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.into_object_or_throw(ctx, "algorithm")?; + + let name: String = obj.get_required("name", "algorithm")?; + + Ok(match name.as_str() { + "X25519" => { + let public_key: Class = obj.get_required("public", "algorithm")?; + let public_key = public_key.borrow(); + + if !matches!(public_key.algorithm, KeyAlgorithm::X25519) { + return algorithm_mismatch_error(ctx, &name); + } + + DeriveAlgorithm::X25519 { + public_key: public_key.handle.clone(), + } + } + "ECDH" => { + let public_key: Class = obj.get_required("public", "algorithm")?; + let public_key = public_key.borrow(); + + if let KeyAlgorithm::Ec { curve, .. } = &public_key.algorithm { + DeriveAlgorithm::Ecdh { + curve: curve.clone(), + public_key: public_key.handle.clone(), + } + } else { + return algorithm_mismatch_error(ctx, &name); + } + } + "HKDF" => DeriveAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), + "PBKDF2" => DeriveAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), + _ => { + return Err(Exception::throw_message( + ctx, + "Algorithm 'name' must be X25519 | ECDH | HKDF | PBKDF2", + )) + } + }) + } +} diff --git a/modules/src/crypto/subtle/digest.rs b/modules/src/crypto/subtle/digest.rs new file mode 100644 index 0000000..4e8e5ea --- /dev/null +++ b/modules/src/crypto/subtle/digest.rs @@ -0,0 +1,30 @@ +use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use ring::digest::Context; +use rsquickjs::{ArrayBuffer, Ctx, Result, Value}; + +use crate::crypto::sha_hash::ShaAlgorithm; + +pub async fn subtle_digest<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + data: ObjectBytes<'js>, +) -> Result> { + let algorithm = if let Some(algorithm) = algorithm.as_string() { + algorithm.to_string().or_throw(&ctx)? + } else { + algorithm.get_required::<_, String>("name", "algorithm")? + }; + + let sha_algorithm = ShaAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; + let bytes = digest(&sha_algorithm, data.as_bytes(&ctx)?); + ArrayBuffer::new(ctx, bytes) +} + +pub fn digest(sha_algorithm: &ShaAlgorithm, data: &[u8]) -> Vec { + let hash = sha_algorithm.digest_algorithm(); + let mut context = Context::new(hash); + context.update(data); + let digest = context.finish(); + + digest.as_ref().to_vec() +} diff --git a/modules/src/crypto/subtle/encryption.rs b/modules/src/crypto/subtle/encryption.rs new file mode 100644 index 0000000..e1051a8 --- /dev/null +++ b/modules/src/crypto/subtle/encryption.rs @@ -0,0 +1,263 @@ +use crate::utils::{bytes::ObjectBytes, result::ResultExt}; +use aes::cipher::typenum::U12; +use aes_gcm::Nonce; +use aes_kw::{KeyInit, KwAes128, KwAes192, KwAes256}; + +use rsa::{ + pkcs1::{DecodeRsaPrivateKey, DecodeRsaPublicKey}, + Oaep, RsaPrivateKey, RsaPublicKey, +}; +use rsquickjs::{ArrayBuffer, Class, Ctx, Exception, Result}; + +use crate::crypto::sha_hash::ShaAlgorithm; + +pub(super) enum OaepPadding { + Sha256(Oaep), + Sha384(Oaep), + Sha512(Oaep), +} + +use super::{ + algorithm_mismatch_error, encryption_algorithm::EncryptionAlgorithm, extract_aes_length, + key_algorithm::KeyAlgorithm, AesCbcDecVariant, AesCbcEncVariant, AesCtrVariant, AesGcmVariant, + CryptoKey, EncryptionMode, +}; + +pub async fn subtle_decrypt<'js>( + ctx: Ctx<'js>, + algorithm: EncryptionAlgorithm, + key: Class<'js, CryptoKey>, + data: ObjectBytes<'js>, +) -> Result> { + let key = key.borrow(); + key.check_validity("decrypt").or_throw(&ctx)?; + let bytes = encrypt_decrypt( + &ctx, + &algorithm, + &key, + data.as_bytes(&ctx)?, + EncryptionMode::Encryption, + EncryptionOperation::Decrypt, + )?; + ArrayBuffer::new(ctx, bytes) +} + +pub async fn subtle_encrypt<'js>( + ctx: Ctx<'js>, + algorithm: EncryptionAlgorithm, + key: Class<'js, CryptoKey>, + data: ObjectBytes<'js>, +) -> Result> { + let key = key.borrow(); + key.check_validity("encrypt").or_throw(&ctx)?; + + let bytes = encrypt_decrypt( + &ctx, + &algorithm, + &key, + data.as_bytes(&ctx)?, + EncryptionMode::Encryption, + EncryptionOperation::Encrypt, + )?; + ArrayBuffer::new(ctx, bytes) +} + +pub enum EncryptionOperation { + Encrypt, + Decrypt, +} + +pub fn encrypt_decrypt( + ctx: &Ctx<'_>, + algorithm: &EncryptionAlgorithm, + key: &CryptoKey, + data: &[u8], + mode: EncryptionMode, + operation: EncryptionOperation, +) -> Result> { + let handle = key.handle.as_ref(); + let bytes = match algorithm { + EncryptionAlgorithm::AesCbc { iv } => { + let length = extract_aes_length(ctx, key, "AES-CBC")?; + match operation { + EncryptionOperation::Encrypt => { + let variant = AesCbcEncVariant::new(length, handle, iv).or_throw(ctx)?; + variant.encrypt(data) + } + EncryptionOperation::Decrypt => { + let variant = AesCbcDecVariant::new(length, handle, iv).or_throw(ctx)?; + variant.decrypt(data).or_throw(ctx)? + } + } + } + EncryptionAlgorithm::AesCtr { + counter, + length: encryption_length, + } => { + let length = extract_aes_length(ctx, key, "AES-CTR")?; + + let mut variant = + AesCtrVariant::new(length, *encryption_length, handle, counter).or_throw(ctx)?; + match operation { + EncryptionOperation::Encrypt => variant.encrypt(data).or_throw(ctx)?, + EncryptionOperation::Decrypt => variant.decrypt(data).or_throw(ctx)?, + } + } + EncryptionAlgorithm::AesGcm { + iv, + tag_length, + additional_data, + } => { + let length = extract_aes_length(ctx, key, "AES-GCM")?; + + let nonce: &ctr::cipher::Array<_, _> = + &Nonce::::try_from(iv.as_ref()).or_throw(ctx)?; + + let variant = AesGcmVariant::new(length, *tag_length, handle).or_throw(ctx)?; + match operation { + EncryptionOperation::Encrypt => variant + .encrypt(nonce, data, additional_data.as_deref()) + .or_throw(ctx)?, + EncryptionOperation::Decrypt => variant + .decrypt(nonce, data, additional_data.as_deref()) + .or_throw(ctx)?, + } + } + EncryptionAlgorithm::AesKw => { + let padding = match mode { + EncryptionMode::Encryption => { + return Err(Exception::throw_message( + ctx, + "AES-KW can only be used for wrapping keys", + )); + } + EncryptionMode::Wrapping(padding) => padding, + }; + + let is_encrypt = matches!(operation, EncryptionOperation::Encrypt); + + //Only create new vec if padding is needed, otherwise use original slice + let data = if !data.len().is_multiple_of(8) && is_encrypt && padding != 0 { + let padding_size = (8 - (data.len() % 8)) % 8; + let mut padded = Vec::with_capacity(data.len() + padding_size); + padded.extend_from_slice(data); + padded.resize(data.len() + padding_size, padding); + std::borrow::Cow::Owned(padded) + } else { + std::borrow::Cow::Borrowed(data) + }; + + match handle.len() { + 16 => { + let kek = KwAes128::new(handle.try_into().or_throw(ctx)?); + match operation { + EncryptionOperation::Encrypt => { + let mut buf = vec![0u8; data.len() + 8]; + let result = kek.wrap_key(&data, &mut buf).or_throw(ctx)?; + rsquickjs::Result::Ok(result.to_vec()) + } + EncryptionOperation::Decrypt => { + let mut buf = vec![0u8; data.len()]; + let result = kek.unwrap_key(&data, &mut buf).or_throw(ctx)?; + Ok(result.to_vec()) + } + } + } + 24 => { + let kek = KwAes192::new(handle.try_into().or_throw(ctx)?); + match operation { + EncryptionOperation::Encrypt => { + let mut buf = vec![0u8; data.len() + 8]; + let result = kek.wrap_key(&data, &mut buf).or_throw(ctx)?; + Ok(result.to_vec()) + } + EncryptionOperation::Decrypt => { + let mut buf = vec![0u8; data.len()]; + let result = kek.unwrap_key(&data, &mut buf).or_throw(ctx)?; + Ok(result.to_vec()) + } + } + } + 32 => { + let kek = KwAes256::new(handle.try_into().or_throw(ctx)?); + match operation { + EncryptionOperation::Encrypt => { + let mut buf = vec![0u8; data.len() + 8]; + let result = kek.wrap_key(&data, &mut buf).or_throw(ctx)?; + Ok(result.to_vec()) + } + EncryptionOperation::Decrypt => { + let mut buf = vec![0u8; data.len()]; + let result = kek.unwrap_key(&data, &mut buf).or_throw(ctx)?; + Ok(result.to_vec()) + } + } + } + _ => return Err(Exception::throw_message(ctx, "Invalid AES-KW key length")), + } + .or_throw(ctx)? + } + EncryptionAlgorithm::RsaOaep { label } => { + let hash = match &key.algorithm { + KeyAlgorithm::Rsa { hash, .. } => hash, + _ => return algorithm_mismatch_error(ctx, "RSA-OAEP"), + }; + let padding = rsa_oaep_padding(ctx, label, hash)?; + match operation { + EncryptionOperation::Encrypt => { + let public_key = RsaPublicKey::from_pkcs1_der(handle).or_throw(ctx)?; + let mut rng = rand::rng(); + match padding { + OaepPadding::Sha256(p) => { + public_key.encrypt(&mut rng, p, data).or_throw(ctx)? + } + OaepPadding::Sha384(p) => { + public_key.encrypt(&mut rng, p, data).or_throw(ctx)? + } + OaepPadding::Sha512(p) => { + public_key.encrypt(&mut rng, p, data).or_throw(ctx)? + } + } + } + EncryptionOperation::Decrypt => { + let private_key = RsaPrivateKey::from_pkcs1_der(handle).or_throw(ctx)?; + match padding { + OaepPadding::Sha256(p) => private_key.decrypt(p, data).or_throw(ctx)?, + OaepPadding::Sha384(p) => private_key.decrypt(p, data).or_throw(ctx)?, + OaepPadding::Sha512(p) => private_key.decrypt(p, data).or_throw(ctx)?, + } + } + } + } + }; + Ok(bytes) +} + +pub fn rsa_oaep_padding( + ctx: &Ctx<'_>, + label: &Option>, + hash: &ShaAlgorithm, +) -> Result { + let mut padding = match hash { + ShaAlgorithm::SHA1 => { + return Err(Exception::throw_message( + ctx, + "SHA-1 is not supported for RSA-OAEP", + )); + } + ShaAlgorithm::SHA256 => OaepPadding::Sha256(Oaep::new()), + ShaAlgorithm::SHA384 => OaepPadding::Sha384(Oaep::new()), + ShaAlgorithm::SHA512 => OaepPadding::Sha512(Oaep::new()), + }; + if let Some(label) = label { + if !label.is_empty() { + match &mut padding { + OaepPadding::Sha256(p) => p.label = Some(label.to_owned()), + OaepPadding::Sha384(p) => p.label = Some(label.to_owned()), + OaepPadding::Sha512(p) => p.label = Some(label.to_owned()), + } + } + } + + Ok(padding) +} diff --git a/modules/src/crypto/subtle/encryption_algorithm.rs b/modules/src/crypto/subtle/encryption_algorithm.rs new file mode 100644 index 0000000..6a89025 --- /dev/null +++ b/modules/src/crypto/subtle/encryption_algorithm.rs @@ -0,0 +1,114 @@ +use crate::utils::{bytes::ObjectBytes, object::ObjectExt}; +use rsquickjs::{Ctx, Exception, FromJs, Result, Value}; + +use super::algorithm_not_supported_error; + +#[derive(Debug)] +pub enum EncryptionAlgorithm { + AesCbc { + iv: Box<[u8]>, + }, + AesCtr { + counter: Box<[u8]>, + length: u32, + }, + AesGcm { + iv: Box<[u8]>, + tag_length: u8, + additional_data: Option>, + }, + RsaOaep { + label: Option>, + }, + AesKw, +} + +impl<'js> FromJs<'js> for EncryptionAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.into_object_or_throw(ctx, "algorithm")?; + + let name: String = obj.get_required("name", "algorithm")?; + + match name.as_str() { + "AES-CBC" => { + let iv = obj + .get_required::<_, ObjectBytes>("iv", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + if iv.len() != 16 { + return Err(Exception::throw_message( + ctx, + "invalid length of iv. Currently supported 16 bytes", + )); + } + + Ok(EncryptionAlgorithm::AesCbc { iv }) + } + "AES-CTR" => { + let counter = obj + .get_required::<_, ObjectBytes>("counter", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let length = obj.get_required::<_, u32>("length", "algorithm")?; + + if !matches!(length, 32 | 64 | 128) { + return Err(Exception::throw_message( + ctx, + "invalid counter length. Currently supported 32/64/128 bits", + )); + } + + Ok(EncryptionAlgorithm::AesCtr { counter, length }) + } + "AES-GCM" => { + let iv = obj + .get_required::<_, ObjectBytes>("iv", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + //FIXME only 12? 96 maybe recommended? + if iv.len() != 12 { + return Err(Exception::throw_type( + ctx, + "invalid length of iv. Currently supported 12 bytes", + )); + } + + let additional_data = obj + .get_optional::<_, ObjectBytes>("additionalData")? + .map(|v| v.into_bytes(ctx)) + .transpose()? + .map(|vec| vec.into_boxed_slice()); + + let tag_length = obj.get_optional::<_, u8>("tagLength")?.unwrap_or(128); + + //ensure tag length is supported using a match statement 32, 64, 96, 104, 112, 120, or 128 + if !matches!(tag_length, 96 | 104 | 112 | 120 | 128) { + return Err(Exception::throw_message( + ctx, + "Invalid tagLength. Currently supported 96/104/112/120/128 bits", + )); + } + + Ok(EncryptionAlgorithm::AesGcm { + iv, + additional_data, + tag_length, + }) + } + "RSA-OAEP" => { + let label = obj + .get_optional::<_, ObjectBytes>("label")? + .map(|bytes| bytes.into_bytes(ctx)) + .transpose()? + .map(|vec| vec.into_boxed_slice()); + + Ok(EncryptionAlgorithm::RsaOaep { label }) + } + "AES-KW" => Ok(EncryptionAlgorithm::AesKw), + _ => algorithm_not_supported_error(ctx), + } + } +} diff --git a/modules/src/crypto/subtle/export_key.rs b/modules/src/crypto/subtle/export_key.rs new file mode 100644 index 0000000..be6aadf --- /dev/null +++ b/modules/src/crypto/subtle/export_key.rs @@ -0,0 +1,398 @@ +use crate::utils::encoding::bytes_to_b64_url_safe_string; +use crate::utils::result::ResultExt; +use der::{ + asn1::{self, BitString, OctetStringRef}, + Decode, Encode, SecretDocument, +}; +use elliptic_curve::{ + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + AffinePoint, CurveArithmetic, FieldBytesSize, +}; +use pkcs8::PrivateKeyInfoRef; +use rsa::{ + pkcs1::DecodeRsaPrivateKey, + pkcs8::{AssociatedOid, DecodePrivateKey, EncodePrivateKey}, + RsaPrivateKey, +}; +use rsquickjs::{ArrayBuffer, Class, Ctx, Exception, Object, Result}; +use spki::{AlgorithmIdentifier, AlgorithmIdentifierOwned, SubjectPublicKeyInfo}; + +use crate::crypto::subtle::CryptoKey; + +pub fn algorithm_export_error(ctx: &Ctx<'_>, algorithm: &str, format: &str) -> Result { + Err(Exception::throw_message( + ctx, + &["Export of ", algorithm, " as ", format, " is not supported"].concat(), + )) +} + +use super::{ + crypto_key::KeyKind, + key_algorithm::{EcAlgorithm, KeyAlgorithm, KeyFormat}, + EllipticCurve, +}; + +pub enum ExportOutput<'js> { + Bytes(Vec), + Object(Object<'js>), +} + +pub async fn subtle_export_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key: Class<'js, CryptoKey>, +) -> Result> { + let key = key.borrow(); + + let export = export_key(&ctx, format, &key)?; + + Ok(match export { + ExportOutput::Bytes(bytes) => ArrayBuffer::new(ctx, bytes)?.into_object(), + ExportOutput::Object(object) => object, + }) +} + +pub fn export_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormat, + key: &CryptoKey, +) -> Result> { + if !key.extractable { + return Err(Exception::throw_type( + ctx, + "The CryptoKey is non extractable", + )); + }; + let bytes = match format { + KeyFormat::Jwk => return Ok(ExportOutput::Object(export_jwk(ctx, key)?)), + KeyFormat::Raw => export_raw(ctx, key), + KeyFormat::Spki => export_spki(ctx, key), + KeyFormat::Pkcs8 => export_pkcs8(ctx, key), + }?; + Ok(ExportOutput::Bytes(bytes)) +} + +fn export_raw(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + if key.kind == KeyKind::Private { + return Err(Exception::throw_type( + ctx, + "Private Crypto keys can't be exported as raw format", + )); + }; + if !matches!( + key.algorithm, + KeyAlgorithm::Aes { .. } + | KeyAlgorithm::Ec { .. } + | KeyAlgorithm::Hmac { .. } + | KeyAlgorithm::Rsa { .. } + | KeyAlgorithm::Ed25519 + | KeyAlgorithm::X25519 + ) { + return algorithm_export_error(ctx, &key.name, "raw"); + } + Ok(key.handle.to_vec()) +} + +fn export_pkcs8(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + let handle = key.handle.as_ref(); + + if key.kind != KeyKind::Private { + return Err(Exception::throw_type( + ctx, + "Public or Secret Crypto keys can't be exported as pkcs8 format", + )); + } + + let bytes: Vec = match &key.algorithm { + KeyAlgorithm::Ec { .. } | KeyAlgorithm::Ed25519 => handle.into(), + KeyAlgorithm::X25519 => PrivateKeyInfoRef::new( + AlgorithmIdentifier { + oid: const_oid::db::rfc8410::ID_X_25519, + parameters: None, + }, + OctetStringRef::new(handle).or_throw(ctx)?, + ) + .to_der() + .or_throw(ctx)?, + KeyAlgorithm::Rsa { .. } => rsa_der_pkcs1_to_pkcs8(ctx, handle)?.as_bytes().to_vec(), + _ => return algorithm_export_error(ctx, &key.name, "pkcs8"), + }; + Ok(bytes) +} + +fn rsa_der_pkcs1_to_pkcs8(ctx: &Ctx, handle: &[u8]) -> Result { + let private_key = RsaPrivateKey::from_pkcs1_der(handle).or_throw(ctx)?; + private_key.to_pkcs8_der().or_throw(ctx) +} + +fn export_spki(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + if key.kind != KeyKind::Public { + return Err(Exception::throw_type( + ctx, + "Private or Secret Crypto keys can't be exported as spki format", + )); + } + + let public_key_bytes = key.handle.as_ref(); + let bytes: Vec = match &key.algorithm { + KeyAlgorithm::X25519 => { + let key_info = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifierRef { + oid: const_oid::db::rfc8410::ID_X_25519, + parameters: None, + }, + subject_public_key: BitString::from_bytes(public_key_bytes).unwrap(), + }; + + key_info.to_der().unwrap() + } + KeyAlgorithm::Ec { curve, algorithm } => { + let alg_id = AlgorithmIdentifierOwned { + oid: elliptic_curve::ALGORITHM_OID, + parameters: Some(match curve { + EllipticCurve::P256 => (&p256::NistP256::OID).into(), + EllipticCurve::P384 => (&p384::NistP384::OID).into(), + EllipticCurve::P521 => (&p521::NistP521::OID).into(), + }), + }; + let alg_id = match algorithm { + EcAlgorithm::Ecdh => AlgorithmIdentifier { + oid: const_oid::db::rfc5912::ID_EC_PUBLIC_KEY, + parameters: alg_id.parameters, + }, + _ => alg_id, + }; + + //unwrap ok, key is always valid after this stage + let key_info = SubjectPublicKeyInfo { + algorithm: alg_id, + subject_public_key: BitString::from_bytes(public_key_bytes).unwrap(), + }; + + key_info.to_der().unwrap() + } + KeyAlgorithm::Ed25519 => { + let key_info = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifierOwned { + oid: const_oid::db::rfc8410::ID_ED_25519, + parameters: None, + }, + subject_public_key: BitString::from_bytes(public_key_bytes).unwrap(), + }; + key_info.to_der().unwrap() + } + + KeyAlgorithm::Rsa { .. } => { + //unwrap ok, key is always valid after this stage + let key_info = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifier { + oid: const_oid::db::rfc5912::RSA_ENCRYPTION, + parameters: Some(asn1::AnyRef::from(asn1::Null)), + }, + subject_public_key: BitString::from_bytes(public_key_bytes).unwrap(), + }; + + key_info.to_der().unwrap() + } + _ => return algorithm_export_error(ctx, &key.name, "spki"), + }; + + Ok(bytes) +} + +fn export_jwk<'js>(ctx: &Ctx<'js>, key: &CryptoKey) -> Result> { + let name = key.name.as_ref(); + let handle = key.handle.as_ref(); + let obj = Object::new(ctx.clone())?; + obj.set("key_ops", key.usages())?; + obj.set("ext", true)?; + match &key.algorithm { + KeyAlgorithm::Aes { length } => { + let prefix = match length { + 128 => "A128", + 192 => "A192", + 256 => "A256", + _ => unreachable!(), + }; + let suffix = &name[("AES-".len())..]; + let alg = [prefix, suffix].concat(); + + let k = bytes_to_b64_url_safe_string(handle); + obj.set("kty", "oct")?; + obj.set("k", k)?; + obj.set("alg", alg)? + } + KeyAlgorithm::Hmac { hash, .. } => { + let k = bytes_to_b64_url_safe_string(handle); + obj.set("kty", "oct")?; + obj.set("alg", ["HS", &hash.as_str()[4..]].concat())?; + obj.set("k", k)?; + } + KeyAlgorithm::Ec { curve, .. } => { + fn set_public_key_coords( + obj: &Object<'_>, + public_key: elliptic_curve::PublicKey, + ) -> Result<()> + where + C: CurveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + { + let p = public_key.to_encoded_point(false); + let x = p.x().unwrap().as_slice(); + let y = p.y().unwrap().as_slice(); + obj.set("x", bytes_to_b64_url_safe_string(x))?; + obj.set("y", bytes_to_b64_url_safe_string(y))?; + Ok(()) + } + + fn set_private_key_props( + obj: &Object<'_>, + private_key: elliptic_curve::SecretKey, + ) -> Result<()> + where + C: elliptic_curve::Curve + elliptic_curve::CurveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + { + let public_key = private_key.public_key(); + set_public_key_coords(obj, public_key)?; + obj.set( + "d", + bytes_to_b64_url_safe_string(private_key.to_bytes().as_slice()), + )?; + Ok(()) + } + + match key.kind { + KeyKind::Public => match curve { + EllipticCurve::P256 => { + let public_key = p256::PublicKey::from_sec1_bytes(handle).or_throw(ctx)?; + set_public_key_coords(&obj, public_key)?; + } + EllipticCurve::P384 => { + let public_key = p384::PublicKey::from_sec1_bytes(handle).or_throw(ctx)?; + set_public_key_coords(&obj, public_key)?; + } + EllipticCurve::P521 => { + let public_key = p521::PublicKey::from_sec1_bytes(handle).or_throw(ctx)?; + set_public_key_coords(&obj, public_key)?; + } + }, + KeyKind::Private => match curve { + EllipticCurve::P256 => { + let private_key = p256::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + set_private_key_props(&obj, private_key)?; + } + EllipticCurve::P384 => { + let private_key = p384::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + set_private_key_props(&obj, private_key)?; + } + EllipticCurve::P521 => { + let private_key = p521::SecretKey::from_pkcs8_der(handle).or_throw(ctx)?; + set_private_key_props(&obj, private_key)?; + } + }, + _ => unreachable!(), + } + + obj.set("kty", "EC")?; + obj.set("crv", curve.as_str())?; + } + KeyAlgorithm::Ed25519 => { + if key.kind == KeyKind::Private { + let pki = PrivateKeyInfoRef::try_from(handle).or_throw(ctx)?; + let pub_key = pki.public_key.as_ref().unwrap(); + set_okp_jwk_props( + name, + &obj, + Some(pki.private_key.as_bytes()), + pub_key.raw_bytes(), + )?; + } else { + set_okp_jwk_props(name, &obj, None, handle)?; + } + } + KeyAlgorithm::Rsa { hash, .. } => { + let (n, e) = match key.kind { + KeyKind::Public => { + let public_key = rsa::pkcs1::RsaPublicKey::from_der(handle).or_throw(ctx)?; + let n = bytes_to_b64_url_safe_string(public_key.modulus.as_bytes()); + let e = bytes_to_b64_url_safe_string(public_key.public_exponent.as_bytes()); + (n, e) + } + KeyKind::Private => { + let private_key = rsa::pkcs1::RsaPrivateKey::from_der(handle).or_throw(ctx)?; + let n = bytes_to_b64_url_safe_string(private_key.modulus.as_bytes()); + let e = bytes_to_b64_url_safe_string(private_key.public_exponent.as_bytes()); + let d = bytes_to_b64_url_safe_string(private_key.private_exponent.as_bytes()); + let p = bytes_to_b64_url_safe_string(private_key.prime1.as_bytes()); + let q = bytes_to_b64_url_safe_string(private_key.prime2.as_bytes()); + let dp = bytes_to_b64_url_safe_string(private_key.exponent1.as_bytes()); + let dq = bytes_to_b64_url_safe_string(private_key.exponent2.as_bytes()); + let qi = bytes_to_b64_url_safe_string(private_key.coefficient.as_bytes()); + obj.set("d", d)?; + obj.set("p", p)?; + obj.set("q", q)?; + obj.set("dp", dp)?; + obj.set("dq", dq)?; + obj.set("qi", qi)?; + (n, e) + } + _ => { + unreachable!() + } + }; + + let alg_suffix = hash.as_numeric_str(); + + let alg_prefix = match name { + "RSASSA-PKCS1-v1_5" => "RS", + "RSA-PSS" => "PS", + "RSA-OAEP" => "RSA-OAEP-", + _ => unreachable!(), + }; + + let alg = [alg_prefix, alg_suffix].concat(); + + obj.set("kty", "RSA")?; + obj.set("n", n)?; + obj.set("e", e)?; + obj.set("alg", alg)?; + } + KeyAlgorithm::X25519 => match key.kind { + KeyKind::Private => { + let array: [u8; 32] = handle.try_into().or_throw(ctx)?; + let secret = x25519_dalek::StaticSecret::from(array); + let public_key = x25519_dalek::PublicKey::from(&secret); + set_okp_jwk_props(name, &obj, Some(secret.as_bytes()), public_key.as_bytes())?; + } + KeyKind::Public => { + let public_key = handle; + set_okp_jwk_props(name, &obj, None, public_key)?; + } + _ => unreachable!(), + }, + //cant be exported + _ => return algorithm_export_error(ctx, &key.name, "jwk"), + }; + + Ok(obj) +} + +fn set_okp_jwk_props( + crv: &str, + obj: &Object<'_>, + private_key: Option<&[u8]>, + public_key: &[u8], +) -> Result<()> { + let x = bytes_to_b64_url_safe_string(public_key); + obj.set("kty", "OKP")?; + obj.set("crv", crv)?; + obj.set("x", x)?; + if let Some(private_key) = private_key { + let d = bytes_to_b64_url_safe_string(private_key); + obj.set("d", d)?; + } + Ok(()) +} diff --git a/modules/src/crypto/subtle/generate_key.rs b/modules/src/crypto/subtle/generate_key.rs new file mode 100644 index 0000000..e1919fe --- /dev/null +++ b/modules/src/crypto/subtle/generate_key.rs @@ -0,0 +1,214 @@ +use crate::utils::result::ResultExt; +use ring::{ + rand::SecureRandom, + signature::{EcdsaKeyPair, Ed25519KeyPair, KeyPair}, +}; +use rsa::{ + pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey}, + pkcs8::{DecodePrivateKey, EncodePrivateKey}, + BoxedUint, RsaPrivateKey, +}; +use rsquickjs::{object::Property, Array, Class, Ctx, Exception, Object, Result, Value}; + +use crate::crypto::{sha_hash::ShaAlgorithm, CryptoKey, SYSTEM_RANDOM}; + +use super::{ + algorithm_not_supported_error, + crypto_key::KeyKind, + key_algorithm::{KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages}, + EllipticCurve, +}; + +pub async fn subtle_generate_key<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result> { + let KeyAlgorithmWithUsages { + name, + algorithm: key_algorithm, + private_usages, + public_usages, + } = KeyAlgorithm::from_js(&ctx, KeyAlgorithmMode::Generate, algorithm, key_usages)?; + + let (private_key, public_or_secret_key) = generate_key(&ctx, &key_algorithm)?; + + if matches!( + key_algorithm, + KeyAlgorithm::Aes { .. } | KeyAlgorithm::Hmac { .. } + ) { + return Ok(Class::instance( + ctx, + CryptoKey::new( + KeyKind::Secret, + name, + extractable, + key_algorithm, + public_usages, + public_or_secret_key, + ), + )? + .into_value()); + } + + let private_key = Class::instance( + ctx.clone(), + CryptoKey::new( + KeyKind::Private, + name.clone(), + extractable, + key_algorithm.clone(), + private_usages, + private_key, + ), + )?; + + let public_key = Class::instance( + ctx.clone(), + CryptoKey::new( + KeyKind::Public, + name, + extractable, + key_algorithm, + public_usages, + public_or_secret_key, + ), + )?; + + let key_pair = Object::new(ctx.clone())?; + key_pair.prop("privateKey", Property::from(private_key).enumerable())?; + key_pair.prop("publicKey", Property::from(public_key).enumerable())?; + Ok(key_pair.into_value()) +} + +fn generate_key(ctx: &Ctx<'_>, algorithm: &KeyAlgorithm) -> Result<(Vec, Vec)> { + let private_key; + let public_or_secret_key; + match algorithm { + KeyAlgorithm::Aes { length } => { + let length = *length as usize; + + match length { + 128 | 192 | 256 => (), + _ => { + return Err(Exception::throw_message( + ctx, + "AES key length must be 128, 192, or 256 bits", + )) + } + } + + public_or_secret_key = generate_symmetric_key(ctx, length / 8)?; + private_key = vec![]; + } + KeyAlgorithm::Hmac { hash, length } => { + let length = get_hash_length(ctx, hash, *length)?; + public_or_secret_key = generate_symmetric_key(ctx, length)?; + private_key = vec![]; + } + KeyAlgorithm::Ec { curve, .. } => { + let rng = &(*SYSTEM_RANDOM); + + match curve { + EllipticCurve::P256 => { + let pkcs8 = EcdsaKeyPair::generate_pkcs8( + &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, + rng, + ) + .or_throw(ctx)?; + private_key = pkcs8.as_ref().into(); + let signing_key = p256::SecretKey::from_pkcs8_der(&private_key).unwrap(); + public_or_secret_key = signing_key.public_key().to_sec1_bytes().into(); + } + EllipticCurve::P384 => { + let pkcs8 = EcdsaKeyPair::generate_pkcs8( + &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING, + rng, + ) + .or_throw(ctx)?; + private_key = pkcs8.as_ref().into(); + let signing_key = p384::SecretKey::from_pkcs8_der(&private_key).unwrap(); + public_or_secret_key = signing_key.public_key().to_sec1_bytes().into(); + } + EllipticCurve::P521 => { + let mut rng = rand::rng(); + let key = p521::SecretKey::try_from_rng(&mut rng).or_throw(ctx)?; + let pkcs8 = key.to_pkcs8_der().or_throw(ctx)?; + private_key = pkcs8.as_bytes().into(); + public_or_secret_key = key.public_key().to_sec1_bytes().into(); + } + } + } + KeyAlgorithm::Ed25519 => { + let rng = &(*SYSTEM_RANDOM); + let pkcs8 = Ed25519KeyPair::generate_pkcs8(rng).or_throw(ctx)?; + private_key = pkcs8.as_ref().into(); + + let key_pair = Ed25519KeyPair::from_pkcs8(&private_key).unwrap(); + public_or_secret_key = key_pair.public_key().as_ref().into(); + } + + KeyAlgorithm::X25519 => { + let mut rng = rand::rng(); + let secret_key = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + private_key = secret_key.as_bytes().into(); + public_or_secret_key = x25519_dalek::PublicKey::from(&secret_key).as_bytes().into(); + } + KeyAlgorithm::Rsa { + modulus_length, + public_exponent, + .. + } => { + let public_exponent = public_exponent.as_ref().as_ref(); + // Convert public exponent bytes to u64 value + let exponent: u64 = match public_exponent { + [0x01, 0x00, 0x01] => 65537, // Standard RSA exponent F4 (0x10001) + [0x03] => 3, // Alternative RSA exponent 3 + bytes + if bytes.ends_with(&[0x03]) + && bytes[..bytes.len() - 1].iter().all(|&b| b == 0) => + { + 3 + } + _ => return Err(Exception::throw_message(ctx, "Invalid RSA public exponent")), + }; + let exp = BoxedUint::from(exponent); + let mut rng = rand::rng(); + let rsa_private_key = + RsaPrivateKey::new_with_exp(&mut rng, *modulus_length as usize, exp) + .or_throw(ctx)?; + + let public_key = rsa_private_key + .to_public_key() + .to_pkcs1_der() + .or_throw(ctx)?; + + let pkcs1 = rsa_private_key.to_pkcs1_der().or_throw(ctx)?; + + private_key = pkcs1.as_bytes().into(); + + public_or_secret_key = public_key.as_bytes().into(); + } + _ => return algorithm_not_supported_error(ctx), + }; + Ok((private_key, public_or_secret_key)) +} + +fn generate_symmetric_key(ctx: &Ctx<'_>, length: usize) -> Result> { + let mut key = vec![0u8; length]; + SYSTEM_RANDOM.fill(&mut key).or_throw(ctx)?; + Ok(key) +} + +pub fn get_hash_length(ctx: &Ctx, hash: &ShaAlgorithm, length: u16) -> Result { + if length == 0 { + return Ok(hash.hmac_algorithm().digest_algorithm().block_len()); + } + + if !length.is_multiple_of(8) || (length / 8) > ring::digest::MAX_BLOCK_LEN.try_into().unwrap() { + return Err(Exception::throw_message(ctx, "Invalid HMAC key length")); + } + + Ok((length / 8) as usize) +} diff --git a/modules/src/crypto/subtle/import_key.rs b/modules/src/crypto/subtle/import_key.rs new file mode 100644 index 0000000..cc9cc58 --- /dev/null +++ b/modules/src/crypto/subtle/import_key.rs @@ -0,0 +1,67 @@ +use crate::utils::{bytes::ObjectBytes, object::ObjectExt}; +use rsquickjs::{Array, Class, Ctx, FromJs, Result, Value}; + +use crate::crypto::subtle::CryptoKey; + +use super::{ + crypto_key::KeyKind, + key_algorithm::{ + KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages, KeyFormat, KeyFormatData, + }, +}; + +#[allow(dead_code)] +pub async fn subtle_import_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key_data: Value<'js>, + algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result> { + let format = match format { + KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Jwk => KeyFormatData::Jwk(key_data.into_object_or_throw(&ctx, "keyData")?), + }; + + import_key(ctx, format, algorithm, extractable, key_usages) +} + +pub fn import_key<'js>( + ctx: Ctx<'js>, + format: KeyFormatData<'js>, + algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result> { + let mut kind = KeyKind::Public; + let mut data = Vec::new(); + + let KeyAlgorithmWithUsages { + name, + algorithm: key_algorithm, + public_usages, + private_usages, + } = KeyAlgorithm::from_js( + &ctx, + KeyAlgorithmMode::Import { + kind: &mut kind, + data: &mut data, + format, + }, + algorithm, + key_usages, + )?; + + let usages = match kind { + KeyKind::Public | KeyKind::Secret => public_usages, + KeyKind::Private => private_usages, + }; + + Class::instance( + ctx, + CryptoKey::new(kind, name, extractable, key_algorithm, usages, data), + ) +} diff --git a/modules/src/crypto/subtle/key_algorithm.rs b/modules/src/crypto/subtle/key_algorithm.rs new file mode 100644 index 0000000..f3f40c1 --- /dev/null +++ b/modules/src/crypto/subtle/key_algorithm.rs @@ -0,0 +1,1112 @@ +#![allow(clippy::uninlined_format_args)] + +use std::rc::Rc; + +use crate::str_enum; +use crate::utils::encoding::bytes_from_b64_url_safe; +use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use der::{ + asn1::{OctetStringRef, UintRef}, + Decode, Encode, +}; +use pkcs8::PrivateKeyInfoRef; +use rsa::pkcs8::EncodePrivateKey; +use rsquickjs::{ + atom::PredefinedAtom, Array, Ctx, Exception, FromJs, Object, Result, TypedArray, Value, +}; +use spki::{AlgorithmIdentifier, ObjectIdentifier}; + +use crate::crypto::sha_hash::ShaAlgorithm; + +use super::{ + algorithm_mismatch_error, algorithm_not_supported_error, crypto_key::KeyKind, + to_name_and_maybe_object, EllipticCurve, +}; + +#[derive(Clone, Copy, PartialEq)] +pub enum KeyUsage { + //7 values, can be max 255 (u8) 0b11111111 + Encrypt, + Decrypt, + WrapKey, + UnwrapKey, + Sign, + Verify, + DeriveKey, + DeriveBits, +} + +impl TryFrom<&str> for KeyUsage { + type Error = String; + + fn try_from(s: &str) -> std::result::Result { + Ok(match s { + "encrypt" => KeyUsage::Encrypt, + "decrypt" => KeyUsage::Decrypt, + "wrapKey" => KeyUsage::WrapKey, + "unwrapKey" => KeyUsage::UnwrapKey, + "sign" => KeyUsage::Sign, + "verify" => KeyUsage::Verify, + "deriveKey" => KeyUsage::DeriveKey, + "deriveBits" => KeyUsage::DeriveBits, + _ => return Err(["Invalid key usage: ", s].concat()), + }) + } +} + +impl KeyUsage { + fn classify_and_check_usages<'js>( + ctx: &Ctx<'js>, + key_usage_algorithm: KeyUsageAlgorithm, + key_usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, + kind: Option<&KeyKind>, + ) -> Result<()> { + let (mut private_usages_mask, mut public_usages_mask) = key_usage_algorithm.masks(); + + match kind { + Some(KeyKind::Private) => public_usages_mask = 0, + Some(KeyKind::Secret) | Some(KeyKind::Public) => private_usages_mask = 0, + None => {} + }; + + let allowed_usages = private_usages_mask | public_usages_mask; + + let mut generated_public_usages = Vec::with_capacity(4); + let mut generated_private_usages = Vec::with_capacity(4); + + let mut has_any_usages = false; + + for usage in key_usages.iter::() { + has_any_usages = true; + let value = usage?; + let usage = KeyUsage::try_from(value.as_str()).or_throw(ctx)?; + let usage = usage.mask(); + if allowed_usages & usage != usage { + return Err(Exception::throw_message( + ctx, + &["Invalid key usage '", &value, "'"].concat(), + )); + } + + if private_usages_mask == public_usages_mask { + generated_private_usages.push(value.clone()); + generated_public_usages.push(value); + } else if private_usages_mask & usage == usage { + generated_private_usages.push(value); + } else if public_usages_mask & usage == usage { + generated_public_usages.push(value); + } + } + + *private_usages = generated_private_usages; + *public_usages = generated_public_usages; + + if !has_any_usages { + return Err(Exception::throw_message(ctx, "Key usages empty")); + } + + if private_usages_mask > 0 && private_usages.is_empty() { + return Err(Exception::throw_message( + ctx, + "No required private key usages provided", + )); + } + + if private_usages != public_usages { + let valid_usage = match kind { + Some(KeyKind::Secret) | Some(KeyKind::Public) => { + private_usages.is_empty() && !public_usages.is_empty() + } + Some(KeyKind::Private) => !private_usages.is_empty() && public_usages.is_empty(), + None => true, + }; + + if !valid_usage { + return Err(Exception::throw_message(ctx, "Invalid key usage")); + } + } + + Ok(()) + } + + const fn mask(self) -> u16 { + 1 << self as u16 + } +} + +#[repr(u16)] +#[derive(Clone, Copy)] +pub enum KeyUsageAlgorithm { + //single mask algorithms (symmetric) + AesKw = KeyUsage::WrapKey.mask() | KeyUsage::UnwrapKey.mask(), + //all non-KW AES + Symmetric = (KeyUsage::Encrypt.mask()) + | (KeyUsage::Decrypt.mask()) + | (KeyUsage::WrapKey.mask()) + | (KeyUsage::UnwrapKey.mask()), + + Hmac = (KeyUsage::Sign.mask()) | (KeyUsage::Verify.mask()), + + //two mask algorithms (asymmetric) - use high bits as for private, low bits for public + //HKDF, PBKDF2, X25519 + Derive = ((KeyUsage::DeriveKey.mask() | KeyUsage::DeriveBits.mask()) << 8) + | KeyUsage::DeriveKey.mask() + | KeyUsage::DeriveBits.mask(), + + RsaOaep = ((KeyUsage::Decrypt.mask() | KeyUsage::UnwrapKey.mask()) << 8) //private + | KeyUsage::Encrypt.mask() | KeyUsage::WrapKey.mask(), //public + + //ECDSA, ED25519, all non-OEAP RSA + Sign = (KeyUsage::Sign.mask() << 8) //private + | KeyUsage::Verify.mask(), //public +} +impl KeyUsageAlgorithm { + fn masks(&self) -> (u16, u16) { + let value = *self as u16; + let private_mask = value >> 8; + let public_mask = value & 0xFF; + (private_mask, public_mask) + } +} + +#[derive(Debug, Clone)] +pub enum KeyDerivation { + Hkdf { + hash: ShaAlgorithm, + salt: Box<[u8]>, + info: Box<[u8]>, + }, + Pbkdf2 { + hash: ShaAlgorithm, + salt: Box<[u8]>, + iterations: u32, + }, +} + +impl KeyDerivation { + pub fn for_hkdf_object<'js>(ctx: &Ctx<'js>, obj: Object<'js>) -> Result { + let hash = extract_sha_hash(ctx, &obj)?; + + let salt = obj + .get_required::<_, ObjectBytes>("salt", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let info = obj + .get_required::<_, ObjectBytes>("info", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + Ok(KeyDerivation::Hkdf { hash, salt, info }) + } + + pub fn for_pbkf2_object<'js>(ctx: &&Ctx<'js>, obj: Object<'js>) -> Result { + let hash = extract_sha_hash(ctx, &obj)?; + + let salt = obj + .get_required::<_, ObjectBytes>("salt", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let iterations = obj.get_required("iterations", "algorithm")?; + Ok(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) + } +} + +#[derive(Debug, Clone)] +pub enum EcAlgorithm { + Ecdh, + Ecdsa, +} + +#[derive(Debug, Clone)] +pub enum KeyAlgorithm { + Aes { + length: u16, + }, + Ec { + curve: EllipticCurve, + algorithm: EcAlgorithm, + }, + X25519, + Ed25519, + Hmac { + hash: ShaAlgorithm, + length: u16, + }, + Rsa { + modulus_length: u32, + public_exponent: Rc>, + hash: ShaAlgorithm, + }, + Derive(KeyDerivation), + HkdfImport, + Pbkdf2Import, +} + +pub enum KeyFormat { + Jwk, + Raw, + Spki, + Pkcs8, +} + +str_enum!(KeyFormat, Jwk => "jwk", Raw => "raw", Spki => "spki", Pkcs8 => "pkcs8"); + +impl<'js> FromJs<'js> for KeyFormat { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + if let Some(string) = value.as_string() { + let string = string.to_string()?; + match string.as_str() { + "jwk" => return Ok(KeyFormat::Jwk), + "raw" => return Ok(KeyFormat::Raw), + "spki" => return Ok(KeyFormat::Spki), + "pkcs8" => return Ok(KeyFormat::Pkcs8), + _ => {} + }; + } + Err(Exception::throw_message( + ctx, + "Key import/export format must be 'jwk','raw','spki' or 'pkcs8'", + )) + } +} + +pub enum KeyFormatData<'js> { + Jwk(Object<'js>), + Raw(ObjectBytes<'js>), + Spki(ObjectBytes<'js>), + Pkcs8(ObjectBytes<'js>), +} + +pub enum KeyAlgorithmMode<'a, 'js> { + Import { + format: KeyFormatData<'js>, + kind: &'a mut KeyKind, + data: &'a mut Vec, + }, + Generate, + Derive, +} + +pub struct KeyAlgorithmWithUsages { + pub name: String, + pub algorithm: KeyAlgorithm, + pub public_usages: Vec, + pub private_usages: Vec, +} + +impl KeyAlgorithm { + pub fn from_js<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + value: Value<'js>, + usages: Array<'js>, + ) -> Result { + let (name, obj) = to_name_and_maybe_object(ctx, value)?; + let mut public_usages = vec![]; + let mut private_usages = vec![]; + let algorithm_name = name.as_str(); + let algorithm = match algorithm_name { + "Ed25519" => { + let key_kind = if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_okp_key( + ctx, + format, + kind, + data, + const_oid::db::rfc8410::ID_ED_25519, + algorithm_name, + )?; + Some(kind) + } else { + None + }; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Sign, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + KeyAlgorithm::Ed25519 + } + "X25519" => { + let key_kind = if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_okp_key( + ctx, + format, + kind, + data, + const_oid::db::rfc8410::ID_X_25519, + algorithm_name, + )?; + Some(kind) + } else { + None + }; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Derive, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + KeyAlgorithm::X25519 + } + "AES-CBC" | "AES-CTR" | "AES-GCM" | "AES-KW" => { + let mut key_kind = None; + let length = if let KeyAlgorithmMode::Import { data, format, kind } = mode { + let l = import_symmetric_key(ctx, format, kind, data, algorithm_name, None)?; + key_kind = Some(kind); + l + } else { + obj.or_throw(ctx)?.get_required("length", "algorithm")? + } as u16; + + if !matches!(length, 128 | 192 | 256) { + return Err(Exception::throw_message( + ctx, + &format!( + "Algorithm 'length' must be one of: 128, 192, or 256 = {}", + length + ), + )); + } + + KeyUsage::classify_and_check_usages( + ctx, + if name == "AES-KW" { + KeyUsageAlgorithm::AesKw + } else { + KeyUsageAlgorithm::Symmetric + }, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + + KeyAlgorithm::Aes { length } + } + "ECDH" => Self::from_ec( + ctx, + mode, + obj, + algorithm_name, + EcAlgorithm::Ecdh, + &usages, + &mut private_usages, + &mut public_usages, + KeyUsageAlgorithm::Derive, + )?, + + "ECDSA" => Self::from_ec( + ctx, + mode, + obj, + algorithm_name, + EcAlgorithm::Ecdsa, + &usages, + &mut private_usages, + &mut public_usages, + KeyUsageAlgorithm::Sign, + )?, + "HMAC" => { + let obj = obj.or_throw(ctx)?; + let hash = extract_sha_hash(ctx, &obj)?; + + let mut length = obj.get_optional("length")?.unwrap_or_default(); + + let key_kind = if let KeyAlgorithmMode::Import { data, format, kind } = mode { + let data_length = + import_symmetric_key(ctx, format, kind, data, algorithm_name, Some(&hash))?; + if length == 0 { + length = data_length as u16 + } + Some(kind) + } else { + None + }; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Hmac, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + + KeyAlgorithm::Hmac { hash, length } + } + "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-v1_5" => { + let obj = obj.or_throw(ctx)?; + let hash = extract_sha_hash(ctx, &obj)?; + + let (modulus_length, public_exponent, key_kind) = + if let KeyAlgorithmMode::Import { format, kind, data } = mode { + let (mod_length, exp) = + import_rsa_key(ctx, format, kind, data, algorithm_name, &hash)?; + (mod_length, exp, Some(kind)) + } else { + let modulus_length = obj.get_required("modulusLength", "algorithm")?; + let public_exponent: TypedArray = + obj.get_required("publicExponent", "algorithm")?; + let public_exponent = public_exponent + .as_bytes() + .ok_or_else(|| { + Exception::throw_message(ctx, "Array buffer has been detached") + })? + .to_owned() + .into_boxed_slice(); + (modulus_length, public_exponent, None) + }; + + KeyUsage::classify_and_check_usages( + ctx, + if name == "RSA-OAEP" { + KeyUsageAlgorithm::RsaOaep + } else { + KeyUsageAlgorithm::Sign + }, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + + let public_exponent = Rc::new(public_exponent); + + KeyAlgorithm::Rsa { + modulus_length, + public_exponent, + hash, + } + } + "HKDF" => { + let (algorithm, key_kind) = match mode { + KeyAlgorithmMode::Import { format, kind, data } => { + import_derive_key(ctx, format, kind, data, algorithm_name)?; + + (KeyAlgorithm::HkdfImport, Some(kind)) + } + KeyAlgorithmMode::Derive => { + let obj = obj.or_throw(ctx)?; + ( + KeyAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), + None, + ) + } + _ => { + return algorithm_not_supported_error(ctx); + } + }; + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Derive, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + algorithm + } + + "PBKDF2" => { + let (algorithm, key_kind) = match mode { + KeyAlgorithmMode::Import { format, kind, data } => { + import_derive_key(ctx, format, kind, data, algorithm_name)?; + (KeyAlgorithm::Pbkdf2Import, Some(kind)) + } + KeyAlgorithmMode::Derive => { + let obj = obj.or_throw(ctx)?; + ( + KeyAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), + None, + ) + } + _ => { + return algorithm_not_supported_error(ctx); + } + }; + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Derive, + &usages, + &mut private_usages, + &mut public_usages, + key_kind.as_deref(), + )?; + algorithm + } + _ => return algorithm_not_supported_error(ctx), + }; + + Ok(KeyAlgorithmWithUsages { + name, + algorithm, + public_usages, + private_usages, + }) + } + + pub fn as_object<'js, T: AsRef>(&self, ctx: &Ctx<'js>, name: T) -> Result> { + let obj = Object::new(ctx.clone())?; + obj.set(PredefinedAtom::Name, name.as_ref())?; + match self { + KeyAlgorithm::Aes { length } => { + obj.set(PredefinedAtom::Length, length)?; + } + KeyAlgorithm::Ec { curve, .. } => { + obj.set("namedCurve", curve.as_str())?; + } + + KeyAlgorithm::Hmac { hash, length } => { + let hash_obj = create_hash_object(ctx, hash)?; + obj.set("hash", hash_obj)?; + + obj.set(PredefinedAtom::Length, length)?; + } + KeyAlgorithm::Rsa { + modulus_length, + public_exponent, + hash, + } => { + let public_exponent = public_exponent.as_ref().to_vec(); + let array = TypedArray::new(ctx.clone(), public_exponent)?; + + let hash_obj = create_hash_object(ctx, hash)?; + obj.set("hash", hash_obj)?; + + obj.set("modulusLength", modulus_length)?; + obj.set("publicExponent", array)?; + } + KeyAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { + let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; + let info = TypedArray::::new(ctx.clone(), info.to_vec())?; + + obj.set("hash", hash.as_str())?; + obj.set("salt", salt)?; + obj.set("info", info)?; + } + KeyAlgorithm::Derive(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) => { + let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; + obj.set("hash", hash.as_str())?; + obj.set("salt", salt)?; + obj.set("iterations", iterations)?; + } + _ => {} + }; + Ok(obj) + } + + #[allow(clippy::too_many_arguments)] + fn from_ec<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: std::result::Result, &str>, + algorithm_name: &str, + algorithm: EcAlgorithm, + key_usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, + key_usage_algorithm: KeyUsageAlgorithm, + ) -> Result { + let obj = obj.or_throw(ctx)?; + let curve_name: String = obj.get_required("namedCurve", "algorithm")?; + let curve = EllipticCurve::try_from(curve_name.as_str()).or_throw(ctx)?; + + let key_kind = if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_ec_key(ctx, format, kind, data, algorithm_name, &curve, &curve_name)?; + Some(kind) + } else { + None + }; + + KeyUsage::classify_and_check_usages( + ctx, + key_usage_algorithm, + key_usages, + private_usages, + public_usages, + key_kind.as_deref(), + )?; + + Ok(KeyAlgorithm::Ec { curve, algorithm }) + } +} + +fn import_derive_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, +) -> Result<()> { + if let KeyFormatData::Raw(object_bytes) = format { + *data = object_bytes.into_bytes(ctx)?; + *kind = KeyKind::Secret; + } else { + return Err(Exception::throw_message( + ctx, + &[algorithm_name, " only supports 'raw' import format"].concat(), + )); + } + + Ok(()) +} + +fn import_rsa_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + hash: &ShaAlgorithm, +) -> Result<(u32, Box<[u8]>)> { + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != const_oid::db::rfc5912::RSA_ENCRYPTION { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + fn public_key_info( + ctx: &Ctx<'_>, + kind: &mut KeyKind, + data: &mut Vec, + public_key: rsa::pkcs1::RsaPublicKey<'_>, + ) -> Result<(usize, Vec)> { + *data = public_key.to_der().or_throw(ctx)?; + *kind = KeyKind::Public; + let modulus_length = public_key.modulus.as_bytes().len() * 8; + let public_exponent = public_key.public_exponent.as_bytes().to_vec(); + Ok((modulus_length, public_exponent)) + } + + macro_rules! uint_ref_from_b64 { + ($name:ident,$ctx:expr,$bytes:expr) => { + let bytes = bytes_from_b64_url_safe($bytes).or_throw($ctx)?; + let $name = UintRef::new(&bytes).or_throw($ctx)?; + }; + } + + let (modulus_length, public_exponent) = match format { + KeyFormatData::Jwk(object) => { + let kty: String = object.get_required("kty", "keyData")?; + let alg: String = object.get_required("alg", "keyData")?; + if kty != "RSA" { + return algorithm_mismatch_error(ctx, algorithm_name); + } + let prefix = &alg[..2]; + let numeric_hash_str = match prefix { + "RS" => { + if algorithm_name == "RSA-OAEP" { + if !alg.starts_with(algorithm_name) { + return algorithm_mismatch_error(ctx, algorithm_name); + } + &alg["RSA-OAEP-".len()..] + } else if algorithm_name != "RSASSA-PKCS1-v1_5" { + return algorithm_mismatch_error(ctx, algorithm_name); + } else { + &alg["RS".len()..] + } + } + "PS" => { + if algorithm_name != "RSA-PSS" { + return algorithm_mismatch_error(ctx, algorithm_name); + } + &alg["PS".len()..] + } + _ => return algorithm_mismatch_error(ctx, algorithm_name), + }; + if numeric_hash_str != hash.as_numeric_str() { + return hash_mismatch_error(ctx, hash); + } + + let n: String = object.get_required("n", "keyData")?; + let e: String = object.get_required("e", "keyData")?; + + uint_ref_from_b64!(modulus, ctx, n.as_bytes()); + uint_ref_from_b64!(public_exponent, ctx, e.as_bytes()); + + if let Some(d) = object.get_optional::<_, String>("d")? { + let p: String = object.get_required("p", "keyData")?; + let q: String = object.get_required("q", "keyData")?; + let dp: String = object.get_required("dp", "keyData")?; + let dq: String = object.get_required("dq", "keyData")?; + let qi: String = object.get_required("qi", "keyData")?; + + uint_ref_from_b64!(private_exponent, ctx, d.as_bytes()); + uint_ref_from_b64!(prime1, ctx, p.as_bytes()); + uint_ref_from_b64!(prime2, ctx, q.as_bytes()); + uint_ref_from_b64!(exponent1, ctx, dp.as_bytes()); + uint_ref_from_b64!(exponent2, ctx, dq.as_bytes()); + uint_ref_from_b64!(coefficient, ctx, qi.as_bytes()); + + let modulus_length = modulus.as_bytes().len() * 8; + + let private_key = rsa::pkcs1::RsaPrivateKey { + modulus, + public_exponent, + private_exponent, + prime1, + prime2, + exponent1, + exponent2, + coefficient, + other_prime_infos: None, + }; + + *data = private_key.to_der().or_throw(ctx)?; + *kind = KeyKind::Private; + (modulus_length, public_exponent.as_bytes().to_vec()) + } else { + let public_key = rsa::pkcs1::RsaPublicKey { + modulus, + public_exponent, + }; + public_key_info(ctx, kind, data, public_key)? + } + } + KeyFormatData::Raw(object_bytes) => { + let public_key = + rsa::pkcs1::RsaPublicKey::from_der(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; + public_key_info(ctx, kind, data, public_key)? + } + KeyFormatData::Pkcs8(object_bytes) => { + let pk_info = PrivateKeyInfoRef::from_der(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; + let object_identifier = pk_info.algorithm.oid; + validate_oid(object_identifier)?; + + let private_key = rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key.as_bytes()) + .or_throw(ctx)?; + + let public_exponent = private_key.public_exponent.as_bytes().to_vec(); + let modulus_length = private_key.modulus.as_bytes().len() * 8; + *data = pk_info.private_key.to_der().or_throw(ctx)?; + *kind = KeyKind::Private; + + (modulus_length, public_exponent) + } + KeyFormatData::Spki(object_bytes) => { + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw(ctx)?; + + let object_identifier = pk_info.algorithm.oid; + validate_oid(object_identifier)?; + + let public_key = + rsa::pkcs1::RsaPublicKey::from_der(pk_info.subject_public_key.raw_bytes()) + .or_throw(ctx)?; + + public_key_info(ctx, kind, data, public_key)? + } + }; + + let public_exponent = public_exponent.into_boxed_slice(); + Ok((modulus_length as u32, public_exponent)) +} + +fn import_symmetric_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + hash: Option<&ShaAlgorithm>, +) -> Result { + *kind = KeyKind::Secret; + + match format { + KeyFormatData::Jwk(object) => { + let kty: String = object.get_required("kty", "keyData")?; + if kty == "oct" { + let k: String = object.get_required("k", "keyData")?; + let alg: String = object.get_required("alg", "keyData")?; + + let prefix = &alg[..1]; + + match (prefix, hash) { + //HMAC - HS256, HS512 etc + ("H", Some(hash)) => { + if &alg[2..] != hash.as_numeric_str() { + return hash_mismatch_error(ctx, hash); + } + } + //AES - A256KW, A256GCM, A256CRT, A512CBC etc + ("A", None) => { + //extract AES-{suffix} + let (_, name_suffix) = algorithm_name.split_once("-").unwrap_or_default(); + let aes_variant = &alg[4..]; + + if aes_variant != name_suffix { + return algorithm_mismatch_error(ctx, algorithm_name); + } + } + _ => return algorithm_mismatch_error(ctx, algorithm_name), + } + + *data = bytes_from_b64_url_safe(k.as_bytes()).or_throw(ctx)?; + return Ok(data.len() * 8); + } + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + + *data = bytes; + return Ok(data.len() * 8); + } + _ => {} + } + algorithm_mismatch_error(ctx, algorithm_name) +} + +fn import_ec_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + curve: &EllipticCurve, + curve_name: &str, +) -> Result<()> { + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != elliptic_curve::ALGORITHM_OID { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + fn decode_to_curve( + ctx: &Ctx<'_>, + value: &str, + ) -> Result> { + let value_bytes = value.as_bytes(); + + let mut field_bytes = elliptic_curve::FieldBytes::::default(); + let mut bytes = bytes_from_b64_url_safe(value_bytes).or_throw(ctx)?; + if bytes.len() < field_bytes.len() { + bytes.resize(field_bytes.len() - bytes.len(), 0); + } + + field_bytes.copy_from_slice(&bytes); + + Ok(field_bytes) + } + + fn decode_jwk_to_ec_point_bytes( + ctx: &Ctx<'_>, + curve: &EllipticCurve, + x: &str, + y: &str, + ) -> Result> { + let point_bytes = match curve { + EllipticCurve::P256 => { + let x = decode_to_curve::(ctx, x)?; + let y = decode_to_curve::(ctx, y)?; + + p256::EncodedPoint::from_affine_coordinates(&x, &y, false).to_bytes() + } + EllipticCurve::P384 => { + let x = decode_to_curve::(ctx, x)?; + let y = decode_to_curve::(ctx, y)?; + + p384::EncodedPoint::from_affine_coordinates(&x, &y, false).to_bytes() + } + EllipticCurve::P521 => { + let x = decode_to_curve::(ctx, x)?; + let y = decode_to_curve::(ctx, y)?; + + p521::EncodedPoint::from_affine_coordinates(&x, &y, false).to_bytes() + } + }; + + Ok(point_bytes.to_vec()) + } + + match format { + KeyFormatData::Jwk(object) => { + let kty: String = object.get_required("kty", "keyData")?; + if kty != "EC" { + return algorithm_mismatch_error(ctx, algorithm_name); + } + + let jwk_crv: String = object.get_required("crv", "keyData")?; + if curve_name != jwk_crv { + return Err(Exception::throw_type( + ctx, + &["Key is using a ", curve_name].concat(), + )); + } + + if let Some(d) = object.get_optional::<_, String>("d")? { + let private_key = match curve { + EllipticCurve::P256 => { + let d = decode_to_curve::(ctx, &d)?; + let key = p256::SecretKey::from_bytes(&d).or_throw(ctx)?; + key.to_pkcs8_der().or_throw(ctx)? + } + EllipticCurve::P384 => { + let d = decode_to_curve::(ctx, &d)?; + let key = p384::SecretKey::from_bytes(&d).or_throw(ctx)?; + key.to_pkcs8_der().or_throw(ctx)? + } + EllipticCurve::P521 => { + let d = decode_to_curve::(ctx, &d)?; + let key = p521::SecretKey::from_bytes(&d).or_throw(ctx)?; + key.to_pkcs8_der().or_throw(ctx)? + } + }; + + *data = private_key.as_bytes().to_vec(); + *kind = KeyKind::Private; + } else { + *kind = KeyKind::Public; + let x: String = object.get_required("x", "keyData")?; + let y: String = object.get_required("y", "keyData")?; + + let point_bytes = decode_jwk_to_ec_point_bytes(ctx, curve, &x, &y)?; + *data = point_bytes; + } + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + if bytes.len() != 32 { + return Err(Exception::throw_type( + ctx, + &[algorithm_name, " keys must be 32 bytes long"].concat(), + )); + } + *data = bytes; + *kind = KeyKind::Public; + } + KeyFormatData::Spki(object_bytes) => { + let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw(ctx)?; + validate_oid(spki.algorithm.oid)?; + *data = spki.subject_public_key.raw_bytes().into(); + *kind = KeyKind::Public; + } + KeyFormatData::Pkcs8(object_bytes) => { + let pkcs8 = PrivateKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; + validate_oid(pkcs8.algorithm.oid)?; + *data = object_bytes.into_bytes(ctx)?; + *kind = KeyKind::Private; + } + }; + Ok(()) +} + +fn import_okp_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + oid: ObjectIdentifier, + algorithm_name: &str, +) -> Result<()> { + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != oid { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + match format { + KeyFormatData::Jwk(object) => { + let crv: String = object.get_required("crv", "keyData")?; + if crv != algorithm_name { + return algorithm_mismatch_error(ctx, algorithm_name); + } + let x: String = object.get_required("x", "keyData")?; + let public_key = bytes_from_b64_url_safe(x.as_bytes()).or_throw(ctx)?; + + if let Some(d) = object.get_optional::<_, String>("d")? { + let private_key = bytes_from_b64_url_safe(d.as_bytes()).or_throw(ctx)?; + + let pk_info = PrivateKeyInfoRef::new( + AlgorithmIdentifier { + oid, + parameters: None, + }, + OctetStringRef::new(private_key.as_slice()).or_throw(ctx)?, + ); + + *data = pk_info.to_der().or_throw(ctx)?; + *kind = KeyKind::Private; + } else { + *data = public_key; + *kind = KeyKind::Public; + } + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + if bytes.len() != 32 { + return Err(Exception::throw_type( + ctx, + &[algorithm_name, " keys must be 32 bytes long"].concat(), + )); + } + *data = bytes; + *kind = KeyKind::Public; + } + KeyFormatData::Spki(object_bytes) => { + let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw(ctx)?; + validate_oid(spki.algorithm.oid)?; + *data = spki.subject_public_key.raw_bytes().into(); + *kind = KeyKind::Public; + } + KeyFormatData::Pkcs8(object_bytes) => { + let pkcs8 = PrivateKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; + validate_oid(pkcs8.algorithm.oid)?; + *data = object_bytes.into_bytes(ctx)?; + *kind = KeyKind::Private; + } + }; + Ok(()) +} + +pub fn extract_sha_hash<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> Result { + let hash: Value = obj.get_required("hash", "algorithm")?; + let hash = if let Some(string) = hash.as_string() { + string.to_string() + } else if let Some(obj) = hash.into_object() { + obj.get_required("name", "hash") + } else { + return Err(Exception::throw_message( + ctx, + "hash must be a string or an object", + )); + }?; + ShaAlgorithm::try_from(hash.as_str()).or_throw(ctx) +} + +fn create_hash_object<'js>(ctx: &Ctx<'js>, hash: &ShaAlgorithm) -> Result> { + let hash_obj = Object::new(ctx.clone())?; + hash_obj.set(PredefinedAtom::Name, hash.as_str())?; + Ok(hash_obj) +} + +pub fn hash_mismatch_error(ctx: &Ctx<'_>, hash: &ShaAlgorithm) -> Result { + Err(Exception::throw_message( + ctx, + &["Algorithm hash expected to be ", hash.as_str()].concat(), + )) +} diff --git a/modules/src/crypto/subtle/mod.rs b/modules/src/crypto/subtle/mod.rs new file mode 100644 index 0000000..a0d1dea --- /dev/null +++ b/modules/src/crypto/subtle/mod.rs @@ -0,0 +1,379 @@ +mod crypto_key; +mod derive; +mod derive_algorithm; +mod digest; +mod encryption; +mod encryption_algorithm; +mod export_key; +mod generate_key; +mod import_key; +mod key_algorithm; +mod sign; +mod sign_algorithm; +mod verify; +mod wrapping; + +pub use crypto_key::CryptoKey; +pub use derive::subtle_derive_bits; +pub use derive::subtle_derive_key; +pub use digest::subtle_digest; +pub use encryption::subtle_decrypt; +pub use encryption::subtle_encrypt; +pub use export_key::subtle_export_key; +pub use generate_key::subtle_generate_key; +pub use import_key::subtle_import_key; +use key_algorithm::KeyAlgorithm; +use ring::digest::Digest; +pub use sign::subtle_sign; +pub use verify::subtle_verify; +pub use wrapping::subtle_unwrap_key; +pub use wrapping::subtle_wrap_key; + +use aes::cipher::BlockModeDecrypt; +use aes::cipher::BlockModeEncrypt; + +use crate::str_enum; +use crate::utils::object::ObjectExt; +use aes::{ + cipher::{ + block_padding::{Error as PaddingError, Pkcs7}, + consts::{U12, U13, U14, U15, U16}, + InvalidLength, KeyIvInit, StreamCipher, StreamCipherError, + }, + Aes128, Aes192, Aes256, +}; +use aes_gcm::{ + aead::{Aead, Payload}, + AesGcm, KeyInit, Nonce, +}; +use ctr::{Ctr128BE, Ctr32BE, Ctr64BE}; +use rsquickjs::{atom::PredefinedAtom, Ctx, Exception, Object, Result, Value}; + +use crate::crypto::sha_hash::ShaAlgorithm; + +#[rsquickjs::class] +#[derive(rsquickjs::JsLifetime, rsquickjs::class::Trace)] +pub struct SubtleCrypto {} + +#[rsquickjs::methods] +impl SubtleCrypto { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get, rename = PredefinedAtom::SymbolToStringTag)] + pub fn to_string_tag(&self) -> &'static str { + stringify!(SubtleCrypto) + } +} + +pub enum AesCbcEncVariant { + Aes128(cbc::Encryptor), + Aes192(cbc::Encryptor), + Aes256(cbc::Encryptor), +} + +impl AesCbcEncVariant { + pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { + let variant: AesCbcEncVariant = match key_len { + 128 => Self::Aes128(cbc::Encryptor::new_from_slices(key, iv)?), + 192 => Self::Aes192(cbc::Encryptor::new_from_slices(key, iv)?), + 256 => Self::Aes256(cbc::Encryptor::new_from_slices(key, iv)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt(self, data: &[u8]) -> Vec { + match self { + Self::Aes128(v) => v.encrypt_padded_vec::(data), + Self::Aes192(v) => v.encrypt_padded_vec::(data), + Self::Aes256(v) => v.encrypt_padded_vec::(data), + } + } +} + +pub enum AesCbcDecVariant { + Aes128(cbc::Decryptor), + Aes192(cbc::Decryptor), + Aes256(cbc::Decryptor), +} + +impl AesCbcDecVariant { + pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { + let variant: AesCbcDecVariant = match key_len { + 128 => Self::Aes128(cbc::Decryptor::new_from_slices(key, iv)?), + 192 => Self::Aes192(cbc::Decryptor::new_from_slices(key, iv)?), + 256 => Self::Aes256(cbc::Decryptor::new_from_slices(key, iv)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn decrypt(self, data: &[u8]) -> std::result::Result, PaddingError> { + Ok(match self { + Self::Aes128(v) => v.decrypt_padded_vec::(data)?, + Self::Aes192(v) => v.decrypt_padded_vec::(data)?, + Self::Aes256(v) => v.decrypt_padded_vec::(data)?, + }) + } +} + +pub enum AesCtrVariant { + Aes128Ctr32(Ctr32BE), + Aes128Ctr64(Ctr64BE), + Aes128Ctr128(Ctr128BE), + Aes192Ctr32(Ctr32BE), + Aes192Ctr64(Ctr64BE), + Aes192Ctr128(Ctr128BE), + Aes256Ctr32(Ctr32BE), + Aes256Ctr64(Ctr64BE), + Aes256Ctr128(Ctr128BE), +} + +impl AesCtrVariant { + pub fn new( + key_len: u16, + encryption_length: u32, + key: &[u8], + counter: &[u8], + ) -> std::result::Result { + let variant: AesCtrVariant = match (key_len, encryption_length) { + (128, 32) => Self::Aes128Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (128, 64) => Self::Aes128Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (128, 128) => Self::Aes128Ctr128(Ctr128BE::new_from_slices(key, counter)?), + (192, 32) => Self::Aes192Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (192, 64) => Self::Aes192Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (192, 128) => Self::Aes192Ctr128(Ctr128BE::new_from_slices(key, counter)?), + (256, 32) => Self::Aes256Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (256, 64) => Self::Aes256Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (256, 128) => Self::Aes256Ctr128(Ctr128BE::new_from_slices(key, counter)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { + let mut ciphertext = data.to_vec(); + match self { + Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + } + Ok(ciphertext) + } + + pub fn decrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { + let mut ciphertext = data.to_vec(); + match self { + Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + } + Ok(ciphertext) + } +} + +pub enum AesGcmVariant { + Aes128Gcm96(AesGcm), + Aes192Gcm96(AesGcm), + Aes256Gcm96(AesGcm), + Aes128Gcm104(AesGcm), + Aes192Gcm104(AesGcm), + Aes256Gcm104(AesGcm), + Aes128Gcm112(AesGcm), + Aes192Gcm112(AesGcm), + Aes256Gcm112(AesGcm), + Aes128Gcm120(AesGcm), + Aes192Gcm120(AesGcm), + Aes256Gcm120(AesGcm), + Aes128Gcm128(AesGcm), + Aes192Gcm128(AesGcm), + Aes256Gcm128(AesGcm), +} + +impl AesGcmVariant { + pub fn new( + key_len: u16, + tag_length: u8, + key: &[u8], + ) -> std::result::Result { + let variant = match (key_len, tag_length) { + (128, 96) => Self::Aes128Gcm96(AesGcm::new_from_slice(key)?), + (192, 96) => Self::Aes192Gcm96(AesGcm::new_from_slice(key)?), + (256, 96) => Self::Aes256Gcm96(AesGcm::new_from_slice(key)?), + (128, 104) => Self::Aes128Gcm104(AesGcm::new_from_slice(key)?), + (192, 104) => Self::Aes192Gcm104(AesGcm::new_from_slice(key)?), + (256, 104) => Self::Aes256Gcm104(AesGcm::new_from_slice(key)?), + (128, 112) => Self::Aes128Gcm112(AesGcm::new_from_slice(key)?), + (192, 112) => Self::Aes192Gcm112(AesGcm::new_from_slice(key)?), + (256, 112) => Self::Aes256Gcm112(AesGcm::new_from_slice(key)?), + (128, 120) => Self::Aes128Gcm120(AesGcm::new_from_slice(key)?), + (192, 120) => Self::Aes192Gcm120(AesGcm::new_from_slice(key)?), + (256, 120) => Self::Aes256Gcm120(AesGcm::new_from_slice(key)?), + (128, 128) => Self::Aes128Gcm128(AesGcm::new_from_slice(key)?), + (192, 128) => Self::Aes192Gcm128(AesGcm::new_from_slice(key)?), + (256, 128) => Self::Aes256Gcm128(AesGcm::new_from_slice(key)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt( + &self, + nonce: &[u8], + msg: &[u8], + aad: Option<&[u8]>, + ) -> std::result::Result, aes_gcm::Error> { + let plaintext: Payload = Payload { + msg, + aad: aad.unwrap_or_default(), + }; + let nonce: &ctr::cipher::Array<_, _> = + &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; + match self { + Self::Aes128Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm128(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm128(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm128(v) => v.encrypt(nonce, plaintext), + } + } + + pub fn decrypt( + &self, + nonce: &[u8], + msg: &[u8], + aad: Option<&[u8]>, + ) -> std::result::Result, aes_gcm::Error> { + let ciphertext: Payload = Payload { + msg, + aad: aad.unwrap_or_default(), + }; + let nonce: &ctr::cipher::Array<_, _> = + &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; + match self { + Self::Aes128Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm128(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm128(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm128(v) => v.decrypt(nonce, ciphertext), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub enum EllipticCurve { + P256, + P384, + P521, +} + +str_enum!(EllipticCurve,P256 => "P-256", P384 => "P-384", P521 => "P-521"); + +pub enum EncryptionMode { + Encryption, + Wrapping(u8), //padding byte +} + +pub fn rsa_hash_digest<'a>( + ctx: &Ctx<'_>, + key: &'a CryptoKey, + data: &'a [u8], + algorithm_name: &str, +) -> Result<(&'a ShaAlgorithm, Digest)> { + let hash = match &key.algorithm { + KeyAlgorithm::Rsa { hash, .. } => hash, + _ => return algorithm_mismatch_error(ctx, algorithm_name), + }; + if !matches!( + hash, + ShaAlgorithm::SHA256 | ShaAlgorithm::SHA384 | ShaAlgorithm::SHA512 + ) { + return Err(Exception::throw_message( + ctx, + "Only Sha-256, Sha-384 or Sha-512 is supported for RSA", + )); + } + + let digest = ring::digest::digest(hash.digest_algorithm(), data); + + Ok((hash, digest)) +} + +pub fn extract_aes_length(ctx: &Ctx<'_>, key: &CryptoKey, expected_algorithm: &str) -> Result { + let length = match key.algorithm { + KeyAlgorithm::Aes { length } => length, + _ => return algorithm_mismatch_error(ctx, expected_algorithm), + }; + Ok(length) +} + +pub fn to_name_and_maybe_object<'js, 'a>( + ctx: &Ctx<'js>, + value: Value<'js>, +) -> Result<(String, std::result::Result, &'a str>)> { + let obj; + let name = if let Some(string) = value.as_string() { + obj = Err("Not an object"); + string.to_string()? + } else if let Some(object) = value.into_object() { + let name = object.get_required("name", "algorithm")?; + obj = Ok(object); + name + } else { + return Err(Exception::throw_message( + ctx, + "algorithm must be a string or an object", + )); + }; + Ok((name, obj)) +} + +pub fn algorithm_mismatch_error(ctx: &Ctx<'_>, expected_algorithm: &str) -> Result { + Err(Exception::throw_message( + ctx, + &["Key algorithm must be ", expected_algorithm].concat(), + )) +} + +pub fn algorithm_not_supported_error(ctx: &Ctx<'_>) -> Result { + Err(Exception::throw_message(ctx, "Algorithm not supported")) +} diff --git a/modules/src/crypto/subtle/sign.rs b/modules/src/crypto/subtle/sign.rs new file mode 100644 index 0000000..2abd160 --- /dev/null +++ b/modules/src/crypto/subtle/sign.rs @@ -0,0 +1,160 @@ +use crate::utils::{bytes::ObjectBytes, result::ResultExt}; +use ring::{ + hmac::{Context as HmacContext, Key as HmacKey}, + signature::{EcdsaKeyPair, Ed25519KeyPair}, +}; +use rsa::{ + pkcs1::DecodeRsaPrivateKey, + pss::Pss, + sha2::{Sha256, Sha384, Sha512}, + Pkcs1v15Sign, RsaPrivateKey, +}; +use rsquickjs::{ArrayBuffer, Class, Ctx, Exception, Result}; + +use crate::crypto::{sha_hash::ShaAlgorithm, subtle::CryptoKey, SYSTEM_RANDOM}; + +use super::{ + algorithm_mismatch_error, key_algorithm::KeyAlgorithm, rsa_hash_digest, + sign_algorithm::SigningAlgorithm, +}; + +pub async fn subtle_sign<'js>( + ctx: Ctx<'js>, + algorithm: SigningAlgorithm, + key: Class<'js, CryptoKey>, + data: ObjectBytes<'js>, +) -> Result> { + let key = key.borrow(); + key.check_validity("sign").or_throw(&ctx)?; + + let bytes = sign(&ctx, &algorithm, &key, data.as_bytes(&ctx)?)?; + ArrayBuffer::new(ctx, bytes) +} + +fn sign( + ctx: &Ctx<'_>, + algorithm: &SigningAlgorithm, + key: &CryptoKey, + data: &[u8], +) -> Result> { + let handle = key.handle.as_ref(); + Ok(match algorithm { + SigningAlgorithm::Ecdsa { hash } => { + // Get hash algorithm from key + if !matches!(&key.algorithm, KeyAlgorithm::Ec { .. }) { + return algorithm_mismatch_error(ctx, "ECDSA"); + }; + + let hash_alg = match hash { + ShaAlgorithm::SHA256 => &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, + ShaAlgorithm::SHA384 => &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING, + _ => { + return Err(Exception::throw_message( + ctx, + "Ecdsa.hash only support Sha256 or Sha384", + )) + } + }; + let rng = &(*SYSTEM_RANDOM); + let key_pair = EcdsaKeyPair::from_pkcs8(hash_alg, handle, rng).or_throw(ctx)?; + let signature = key_pair.sign(rng, data).or_throw(ctx)?; + + signature.as_ref().to_vec() + } + SigningAlgorithm::Ed25519 => { + // Verify key algorithm + if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { + return algorithm_mismatch_error(ctx, "Ed25519"); + } + let key_pair = Ed25519KeyPair::from_pkcs8(handle).or_throw(ctx)?; + let signature = key_pair.sign(data); + + signature.as_ref().to_vec() + } + SigningAlgorithm::Hmac => { + let hash = if let KeyAlgorithm::Hmac { hash, .. } = &key.algorithm { + hash + } else { + return algorithm_mismatch_error(ctx, "HMAC"); + }; + + let hmac_alg = hash.hmac_algorithm(); + + let key = HmacKey::new(*hmac_alg, handle); + let mut hmac = HmacContext::with_key(&key); + hmac.update(data); + + hmac.sign().as_ref().to_vec() + } + SigningAlgorithm::RsaPss { salt_length } => { + let salt_length = *salt_length as usize; + + let mut rng = rand::rng(); + + let private_key = RsaPrivateKey::from_pkcs1_der(&key.handle).or_throw(ctx)?; + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; + let digest = digest.as_ref(); + + match hash { + ShaAlgorithm::SHA256 => private_key + .sign_with_rng( + &mut rng, + Pss::::new_with_salt(salt_length), + digest, + ) + .or_throw(ctx), + ShaAlgorithm::SHA384 => private_key + .sign_with_rng( + &mut rng, + Pss::::new_with_salt(salt_length), + digest, + ) + .or_throw(ctx), + ShaAlgorithm::SHA512 => private_key + .sign_with_rng( + &mut rng, + Pss::::new_with_salt(salt_length), + digest, + ) + .or_throw(ctx), + ShaAlgorithm::SHA1 => unreachable!(), + }? + } + SigningAlgorithm::RsassaPkcs1v15 => { + let mut rng = rand::rng(); + + let private_key = RsaPrivateKey::from_pkcs1_der(&key.handle).or_throw(ctx)?; + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; + let digest = digest.as_ref(); + + match hash { + ShaAlgorithm::SHA256 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .or_throw(ctx), + ShaAlgorithm::SHA384 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .or_throw(ctx), + ShaAlgorithm::SHA512 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .or_throw(ctx), + ShaAlgorithm::SHA1 => unreachable!(), + }? + } + }) +} + +// // Helper function for RSA signing +// fn rsa_sign( +// ctx: &Ctx<'_>, +// key: &CryptoKey, +// algorithm_name: &str, +// data: &[u8], +// sign_fn: F, +// ) -> Result> +// where +// F: FnOnce(&ShaAlgorithm, &[u8], &rsa::RsaPrivateKey) -> Result>, +// { +// let (hash, digest) = rsa_hash_digest(ctx, key, data, algorithm_name)?; + +// sign_fn(hash, digest.as_ref()) +// } diff --git a/modules/src/crypto/subtle/sign_algorithm.rs b/modules/src/crypto/subtle/sign_algorithm.rs new file mode 100644 index 0000000..d1e07fa --- /dev/null +++ b/modules/src/crypto/subtle/sign_algorithm.rs @@ -0,0 +1,41 @@ +use crate::utils::{object::ObjectExt, result::ResultExt}; +use rsquickjs::{Ctx, FromJs, Result, Value}; + +use crate::crypto::sha_hash::ShaAlgorithm; + +use super::{ + algorithm_not_supported_error, key_algorithm::extract_sha_hash, to_name_and_maybe_object, +}; + +#[derive(Debug)] +pub enum SigningAlgorithm { + Ecdsa { hash: ShaAlgorithm }, + Ed25519, + RsaPss { salt_length: u32 }, + RsassaPkcs1v15, + Hmac, +} + +impl<'js> FromJs<'js> for SigningAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let (name, obj) = to_name_and_maybe_object(ctx, value)?; + + let algorithm = match name.as_str() { + "Ed25519" => SigningAlgorithm::Ed25519, + "HMAC" => SigningAlgorithm::Hmac, + "RSASSA-PKCS1-v1_5" => SigningAlgorithm::RsassaPkcs1v15, + "ECDSA" => { + let obj = obj.or_throw(ctx)?; + let hash = extract_sha_hash(ctx, &obj)?; + SigningAlgorithm::Ecdsa { hash } + } + "RSA-PSS" => { + let salt_length = obj.or_throw(ctx)?.get_required("saltLength", "algorithm")?; + + SigningAlgorithm::RsaPss { salt_length } + } + _ => return algorithm_not_supported_error(ctx), + }; + Ok(algorithm) + } +} diff --git a/modules/src/crypto/subtle/verify.rs b/modules/src/crypto/subtle/verify.rs new file mode 100644 index 0000000..0b9f8d9 --- /dev/null +++ b/modules/src/crypto/subtle/verify.rs @@ -0,0 +1,152 @@ +use crate::utils::{bytes::ObjectBytes, result::ResultExt}; +use ecdsa::signature::hazmat::PrehashVerifier; +use ring::{ + hmac::{Context as HmacContext, Key as HmacKey}, + signature::UnparsedPublicKey, +}; +use rsa::{ + pkcs1::DecodeRsaPublicKey, + pkcs1v15::Pkcs1v15Sign, + pss::Pss, + sha2::{Sha256, Sha384, Sha512}, + RsaPublicKey, +}; +use rsquickjs::{Class, Ctx, Result}; + +use crate::crypto::{ + sha_hash::ShaAlgorithm, + subtle::{digest, CryptoKey}, +}; + +use super::{ + algorithm_mismatch_error, key_algorithm::KeyAlgorithm, rsa_hash_digest, + sign_algorithm::SigningAlgorithm, EllipticCurve, +}; + +pub async fn subtle_verify<'js>( + ctx: Ctx<'js>, + algorithm: SigningAlgorithm, + key: Class<'js, CryptoKey>, + signature: ObjectBytes<'js>, + data: ObjectBytes<'js>, +) -> Result { + let key = key.borrow(); + key.check_validity("verify").or_throw(&ctx)?; + + verify( + &ctx, + &algorithm, + &key, + signature.as_bytes(&ctx)?, + data.as_bytes(&ctx)?, + ) +} + +fn verify( + ctx: &Ctx<'_>, + algorithm: &SigningAlgorithm, + key: &CryptoKey, + signature: &[u8], + data: &[u8], +) -> Result { + let handle = key.handle.as_ref(); + Ok(match algorithm { + SigningAlgorithm::Ecdsa { hash } => { + let curve = match &key.algorithm { + KeyAlgorithm::Ec { curve, .. } => curve, + _ => return algorithm_mismatch_error(ctx, "ECDSA"), + }; + + let hash = digest::digest(hash, data); + + match curve { + EllipticCurve::P256 => { + let verifying_key = + p256::ecdsa::VerifyingKey::from_sec1_bytes(handle).or_throw(ctx)?; + let signature = p256::ecdsa::Signature::from_slice(signature).or_throw(ctx)?; + verifying_key.verify_prehash(&hash, &signature).is_ok() + } + EllipticCurve::P384 => { + let verifying_key = + p384::ecdsa::VerifyingKey::from_sec1_bytes(handle).or_throw(ctx)?; + let signature = p384::ecdsa::Signature::from_slice(signature).or_throw(ctx)?; + verifying_key.verify_prehash(&hash, &signature).is_ok() + } + EllipticCurve::P521 => { + let verifying_key = + p521::ecdsa::VerifyingKey::from_sec1_bytes(handle).or_throw(ctx)?; + let signature = p521::ecdsa::Signature::from_slice(signature).or_throw(ctx)?; + verifying_key.verify_prehash(&hash, &signature).is_ok() + } + } + } + SigningAlgorithm::Ed25519 => { + if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { + return algorithm_mismatch_error(ctx, "Ed25519"); + } + + let public_key = UnparsedPublicKey::new(&ring::signature::ED25519, handle); + public_key.verify(data, signature).is_ok() + } + SigningAlgorithm::Hmac => { + let hash = match &key.algorithm { + KeyAlgorithm::Hmac { hash, .. } => hash, + _ => return algorithm_mismatch_error(ctx, "HMAC"), + }; + + let key = HmacKey::new(*hash.hmac_algorithm(), handle); + let mut hmac = HmacContext::with_key(&key); + hmac.update(data); + hmac.sign().as_ref() == signature + } + SigningAlgorithm::RsaPss { salt_length } => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; + let digest = digest.as_ref(); + let public_key = RsaPublicKey::from_pkcs1_der(&key.handle).or_throw(ctx)?; + + match hash { + ShaAlgorithm::SHA256 => public_key + .verify( + Pss::::new_with_salt(*salt_length as usize), + digest, + signature, + ) + .is_ok(), + ShaAlgorithm::SHA384 => public_key + .verify( + Pss::::new_with_salt(*salt_length as usize), + digest, + signature, + ) + .is_ok(), + ShaAlgorithm::SHA512 => public_key + .verify( + Pss::::new_with_salt(*salt_length as usize), + digest, + signature, + ) + .is_ok(), + _ => unreachable!(), + } + } + SigningAlgorithm::RsassaPkcs1v15 => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; + let public_key = RsaPublicKey::from_pkcs1_der(&key.handle).or_throw(ctx)?; + + let digest = digest.as_ref(); + + match hash { + ShaAlgorithm::SHA256 => public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok(), + ShaAlgorithm::SHA384 => public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok(), + ShaAlgorithm::SHA512 => public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok(), + _ => unreachable!(), + } + } + }) +} diff --git a/modules/src/crypto/subtle/wrapping.rs b/modules/src/crypto/subtle/wrapping.rs new file mode 100644 index 0000000..5bc6c9d --- /dev/null +++ b/modules/src/crypto/subtle/wrapping.rs @@ -0,0 +1,92 @@ +use crate::utils::json::{parse::json_parse, stringify::json_stringify}; +use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rsquickjs::{Array, ArrayBuffer, Class, Ctx, Exception, Result, Value}; + +use crate::crypto::subtle::CryptoKey; + +use super::{ + encryption::{self, encrypt_decrypt}, + encryption_algorithm::EncryptionAlgorithm, + export_key::{export_key, ExportOutput}, + import_key::import_key, + key_algorithm::{KeyFormat, KeyFormatData}, + EncryptionMode, +}; + +pub async fn subtle_wrap_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key: Class<'js, CryptoKey>, + wrapping_key: Class<'js, CryptoKey>, + wrap_algo: EncryptionAlgorithm, +) -> Result> { + let key = key.borrow(); + + let export = export_key(&ctx, format, &key)?; + + let (bytes, padding) = match export { + ExportOutput::Bytes(bytes) => (bytes, 0), + ExportOutput::Object(value) => { + let json = json_stringify(&ctx, value.into_value())?.unwrap(); + (json.into_bytes(), b' ') + } + }; + + let wrapping_key = wrapping_key.borrow(); + wrapping_key.check_validity("wrapKey").or_throw(&ctx)?; + + let bytes = encrypt_decrypt( + &ctx, + &wrap_algo, + &wrapping_key, + &bytes, + EncryptionMode::Wrapping(padding), + encryption::EncryptionOperation::Encrypt, + )?; + + ArrayBuffer::new(ctx, bytes) +} + +//cant take more than 7 args +pub async fn subtle_unwrap_key<'js>( + format: KeyFormat, + wrapped_key: ArrayBuffer<'js>, + unwrapping_key: Class<'js, CryptoKey>, + unwrap_algo: EncryptionAlgorithm, + unwrapped_key_algo: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result> { + let unwrapping_key = unwrapping_key.borrow(); + let ctx = wrapped_key.ctx().clone(); + unwrapping_key.check_validity("unwrapKey").or_throw(&ctx)?; + + let bytes = wrapped_key + .as_bytes() + .ok_or_else(|| Exception::throw_message(&ctx, "ArrayBuffer is detached"))?; + + let padding = match format { + KeyFormat::Jwk => b' ', + _ => 0, + }; + + let bytes = encrypt_decrypt( + &ctx, + &unwrap_algo, + &unwrapping_key, + bytes, + EncryptionMode::Wrapping(padding), + encryption::EncryptionOperation::Decrypt, + )?; + + let key_format = match format { + KeyFormat::Jwk => { + KeyFormatData::Jwk(json_parse(&ctx, bytes)?.into_object_or_throw(&ctx, "wrappedKey")?) + } + KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::Vec(bytes)), + KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::Vec(bytes)), + KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::Vec(bytes)), + }; + + import_key(ctx, key_format, unwrapped_key_algo, extractable, key_usages) +} diff --git a/modules/src/fetch/fetch.rs b/modules/src/fetch/fetch.rs index 25f6819..3124dac 100644 --- a/modules/src/fetch/fetch.rs +++ b/modules/src/fetch/fetch.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::{collections::HashSet, convert::Infallible, sync::Arc, time::Instant}; use crate::abort::AbortSignal; diff --git a/modules/src/fetch/form_data.rs b/modules/src/fetch/form_data.rs index 2578371..33e4245 100644 --- a/modules/src/fetch/form_data.rs +++ b/modules/src/fetch/form_data.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::{ io::Write, sync::{Arc, Mutex}, diff --git a/modules/src/fetch/headers.rs b/modules/src/fetch/headers.rs index 43f3b6f..d354e2f 100644 --- a/modules/src/fetch/headers.rs +++ b/modules/src/fetch/headers.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 #![allow(clippy::uninlined_format_args)] use std::{collections::BTreeMap, rc::Rc}; diff --git a/modules/src/fetch/incoming.rs b/modules/src/fetch/incoming.rs index 8cbc40b..6ee63d1 100644 --- a/modules/src/fetch/incoming.rs +++ b/modules/src/fetch/incoming.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::{ error::Error as StdError, future::Future, diff --git a/modules/src/fetch/mod.rs b/modules/src/fetch/mod.rs index ffba8d7..b2205d2 100644 --- a/modules/src/fetch/mod.rs +++ b/modules/src/fetch/mod.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use crate::buffer::Blob; use crate::http::client::build_client; use crate::utils::{ diff --git a/modules/src/fetch/request.rs b/modules/src/fetch/request.rs index 4ef0353..95179eb 100644 --- a/modules/src/fetch/request.rs +++ b/modules/src/fetch/request.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::sync::RwLock; use crate::abort::AbortSignal; diff --git a/modules/src/fetch/response.rs b/modules/src/fetch/response.rs index 2625ac0..f5523b2 100644 --- a/modules/src/fetch/response.rs +++ b/modules/src/fetch/response.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::{ collections::{BTreeMap, HashMap}, io::Read, diff --git a/modules/src/intl/cldr_data.rs b/modules/src/intl/cldr_data.rs index 9f55b8f..e3d4c5b 100644 --- a/modules/src/intl/cldr_data.rs +++ b/modules/src/intl/cldr_data.rs @@ -1,6 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - //! Baked CLDR locale data for date/time formatting. //! //! This module contains pre-extracted patterns from the Unicode CLDR project @@ -132,7 +129,7 @@ pub fn get_locale_data(locale: &str) -> &'static LocaleData { } else { &EN_US } - }, + } } } diff --git a/modules/src/intl/date_time_format.rs b/modules/src/intl/date_time_format.rs index d94ead8..7110a1a 100644 --- a/modules/src/intl/date_time_format.rs +++ b/modules/src/intl/date_time_format.rs @@ -1,6 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - //! Minimal Intl.DateTimeFormat implementation for timezone support. //! This provides just enough functionality to support dayjs and similar libraries. @@ -221,7 +218,7 @@ fn format_timezone_name(local_dt: &DateTime, timezone: &Tz, style: &str) -> } result - }, + } "long" => timezone.name().to_string(), _ => timezone.name().to_string(), } diff --git a/modules/src/intl/mod.rs b/modules/src/intl/mod.rs index efa3265..98bc789 100644 --- a/modules/src/intl/mod.rs +++ b/modules/src/intl/mod.rs @@ -1,6 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - //! Minimal Intl module for LLRT. //! //! Provides a subset of the `Intl` API focused on timezone support, @@ -121,7 +118,7 @@ fn date_to_locale_string<'js>( &time_str, locale_data.datetime_pattern, )) - }, + } (Some(ds), None) => { // Date only let date_pattern = get_date_pattern(ds, locale_data); @@ -131,7 +128,7 @@ fn date_to_locale_string<'js>( locale_data, hour12, )) - }, + } (None, Some(ts)) => { // Time only let time_pattern = get_time_pattern(ts, locale_data); @@ -141,7 +138,7 @@ fn date_to_locale_string<'js>( locale_data, hour12, )) - }, + } (None, None) => { // Default: short date and medium time (matching browser behavior) let date_str = format_with_pattern( @@ -161,7 +158,7 @@ fn date_to_locale_string<'js>( &time_str, locale_data.datetime_pattern, )) - }, + } } } diff --git a/modules/src/intl/pattern_formatter.rs b/modules/src/intl/pattern_formatter.rs index 178274b..b088a03 100644 --- a/modules/src/intl/pattern_formatter.rs +++ b/modules/src/intl/pattern_formatter.rs @@ -1,6 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - //! CLDR pattern parser and formatter. //! //! Parses Unicode CLDR date/time patterns and formats dates accordingly. @@ -38,20 +35,20 @@ pub fn format_with_pattern( result.push(c); } } - }, + } // Pattern letters 'y' | 'Y' => { let count = 1 + consume_same(&mut chars, ch); format_year(&mut result, dt.year(), count); - }, + } 'M' | 'L' => { let count = 1 + consume_same(&mut chars, ch); format_month(&mut result, dt.month() as usize, count, locale_data); - }, + } 'd' => { let count = 1 + consume_same(&mut chars, ch); format_day(&mut result, dt.day(), count); - }, + } 'E' | 'e' | 'c' => { let count = 1 + consume_same(&mut chars, ch); format_weekday( @@ -60,7 +57,7 @@ pub fn format_with_pattern( count, locale_data, ); - }, + } 'a' => { consume_same(&mut chars, ch); // Only show AM/PM if we're using 12-hour format @@ -73,7 +70,7 @@ pub fn format_with_pattern( result.push_str(locale_data.pm); } } - }, + } 'h' => { let count = 1 + consume_same(&mut chars, ch); let hour = dt.hour(); @@ -91,7 +88,7 @@ pub fn format_with_pattern( // 24-hour format (0-23) format_number(&mut result, hour, count); } - }, + } 'H' => { let count = 1 + consume_same(&mut chars, ch); let hour = dt.hour(); @@ -108,31 +105,31 @@ pub fn format_with_pattern( // 24-hour format (0-23) format_number(&mut result, hour, count); } - }, + } 'm' => { let count = 1 + consume_same(&mut chars, ch); format_number(&mut result, dt.minute(), count); - }, + } 's' => { let count = 1 + consume_same(&mut chars, ch); format_number(&mut result, dt.second(), count); - }, + } 'z' => { let count = 1 + consume_same(&mut chars, ch); format_timezone(&mut result, dt, count); - }, + } 'Z' | 'O' | 'v' | 'V' | 'X' | 'x' => { let count = 1 + consume_same(&mut chars, ch); format_timezone(&mut result, dt, count); - }, + } // Skip these pattern letters (not commonly needed) 'G' | 'q' | 'Q' | 'w' | 'W' | 'D' | 'F' | 'g' | 'A' | 'S' => { consume_same(&mut chars, ch); - }, + } // Pass through literal characters _ => { result.push(ch); - }, + } } } @@ -184,7 +181,7 @@ fn format_month(result: &mut String, month: usize, width: usize, locale_data: &L // Numeric, no padding let mut buf = itoa::Buffer::new(); result.push_str(buf.format(month)); - }, + } 2 => { // Numeric, zero-padded let mut buf = itoa::Buffer::new(); @@ -192,19 +189,19 @@ fn format_month(result: &mut String, month: usize, width: usize, locale_data: &L result.push('0'); } result.push_str(buf.format(month)); - }, + } 3 => { // Abbreviated if (1..=12).contains(&month) { result.push_str(locale_data.months_abbr[month - 1]); } - }, + } _ => { // Wide (4+) if (1..=12).contains(&month) { result.push_str(locale_data.months_wide[month - 1]); } - }, + } } } @@ -219,11 +216,11 @@ fn format_weekday(result: &mut String, weekday: usize, width: usize, locale_data 1..=3 => { // Abbreviated result.push_str(locale_data.days_abbr[weekday]); - }, + } _ => { // Wide (4+) result.push_str(locale_data.days_wide[weekday]); - }, + } } } diff --git a/modules/src/lib.rs b/modules/src/lib.rs index 341d84a..1efe077 100644 --- a/modules/src/lib.rs +++ b/modules/src/lib.rs @@ -9,6 +9,9 @@ pub mod buffer; pub mod path; pub mod permissions; +#[cfg(feature = "crypto")] +pub mod crypto; + #[cfg(feature = "event")] pub mod event; @@ -43,6 +46,8 @@ pub mod async_hooks; pub mod hooking; pub mod module; pub mod navigator; +pub mod serdeserclone; +pub mod text; pub mod timers; pub mod utils; @@ -56,11 +61,15 @@ pub fn init( permissions::init(ctx.clone(), permissions)?; exceptions::init(ctx)?; async_hooks::init(ctx)?; - + text::init(ctx)?; + serdeserclone::init(ctx)?; module::module::init(ctx)?; buffer::init(ctx)?; timers::init(ctx)?; - + #[cfg(feature = "crypto")] + { + crypto::init(ctx)?; + } #[cfg(feature = "source")] { script::init(ctx)?; diff --git a/modules/src/module/module_builder.rs b/modules/src/module/module_builder.rs index d21abcb..0781fa0 100644 --- a/modules/src/module/module_builder.rs +++ b/modules/src/module/module_builder.rs @@ -47,6 +47,7 @@ impl Default for ModuleBuilder { builder = builder.with_module(crate::async_hooks::AsyncHooksModule); builder = builder.with_module(crate::timers::TimersModule); builder = builder.with_module(crate::buffer::BufferModule); + builder = builder.with_module(crate::text::TextModule); #[cfg(feature = "assert")] { builder = builder.with_module(crate::modules::assert::AssertModule); @@ -61,7 +62,7 @@ impl Default for ModuleBuilder { } #[cfg(feature = "crypto")] { - builder = builder.with_module(crate::modules::crypto::CryptoModule); + builder = builder.with_module(crate::crypto::CryptoModule); } #[cfg(feature = "dgram")] { diff --git a/modules/src/serdeserclone.rs b/modules/src/serdeserclone.rs new file mode 100644 index 0000000..c43c89c --- /dev/null +++ b/modules/src/serdeserclone.rs @@ -0,0 +1,33 @@ +use rsquickjs::{prelude::Func, ArrayBuffer, Ctx, Result, TypedArray, Value}; + +pub fn init<'js>(ctx: &Ctx<'js>) -> Result<()> { + let globals = ctx.globals(); + globals.set( + "internalSerialize", + Func::from( + |ctx: Ctx<'js>, value: rsquickjs::Value<'js>| -> Result> { + value.serialize(ctx) + }, + ), + )?; + + globals.set( + "internalDeserialize", + Func::from( + |ctx: Ctx<'js>, value: rsquickjs::Value<'js>| -> Result> { + value.deserialize(ctx) + }, + ), + )?; + + globals.set( + "structuredClone", + Func::from( + |ctx: Ctx<'js>, value: rsquickjs::Value<'js>| -> Result> { + value.structured_clone(ctx) + }, + ), + )?; + + Ok(()) +} diff --git a/modules/src/text/mod.rs b/modules/src/text/mod.rs new file mode 100644 index 0000000..8f73032 --- /dev/null +++ b/modules/src/text/mod.rs @@ -0,0 +1,72 @@ +pub mod text_decoder; +pub mod text_encoder; + +use crate::utils::{ + console::format_plain, + module::{export_default, ModuleInfo}, +}; +use rsquickjs::{ + function::Func, + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Function, Object, Result, +}; +use text_decoder::TextDecoder; +use text_encoder::TextEncoder; + +fn inherits<'js>(ctor: Function<'js>, super_ctor: Function<'js>) -> Result<()> { + let super_proto: Object<'js> = super_ctor.get("prototype")?; + let proto: Object<'js> = ctor.get("prototype")?; + proto.set_prototype(Some(&super_proto))?; + ctor.set("super_", super_ctor)?; + Ok(()) +} + +pub struct TextModule; + +impl ModuleDef for TextModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(TextDecoder))?; + declare.declare(stringify!(TextEncoder))?; + declare.declare(stringify!(format))?; + declare.declare(stringify!(inherits))?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + let globals = ctx.globals(); + + let encoder: Function = globals.get(stringify!(TextEncoder))?; + let decoder: Function = globals.get(stringify!(TextDecoder))?; + + default.set(stringify!(TextEncoder), encoder)?; + default.set(stringify!(TextDecoder), decoder)?; + default.set( + "format", + Func::from(|ctx, args| format_plain(ctx, true, args)), + )?; + default.set("inherits", Func::from(inherits))?; + + Ok(()) + }) + } +} + +impl From for ModuleInfo { + fn from(val: TextModule) -> Self { + ModuleInfo { + name: "util", + module: val, + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + Class::::define(&globals)?; + Class::::define(&globals)?; + + Ok(()) +} diff --git a/modules/src/text/text_decoder.rs b/modules/src/text/text_decoder.rs new file mode 100644 index 0000000..81c5a0f --- /dev/null +++ b/modules/src/text/text_decoder.rs @@ -0,0 +1,76 @@ +use crate::utils::encoding::Encoder; +use crate::utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rsquickjs::{atom::PredefinedAtom, function::Opt, Ctx, Object, Result}; + +#[rsquickjs::class] +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +pub struct TextDecoder { + #[qjs(skip_trace)] + encoder: Encoder, + fatal: bool, + ignore_bom: bool, +} + +#[rsquickjs::methods] +impl<'js> TextDecoder { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>, label: Opt, options: Opt>) -> Result { + let mut fatal = false; + let mut ignore_bom = false; + + let encoder = Encoder::from_optional_str(label.as_deref()).or_throw_range(&ctx, "")?; + + if let Some(options) = options.0 { + if let Some(opt) = options.get_optional("fatal")? { + fatal = opt; + } + if let Some(opt) = options.get_optional("ignoreBOM")? { + ignore_bom = opt; + } + } + + Ok(TextDecoder { + encoder, + fatal, + ignore_bom, + }) + } + + #[qjs(get)] + fn encoding(&self) -> &str { + self.encoder.as_label() + } + + #[qjs(get)] + fn fatal(&self) -> bool { + self.fatal + } + + #[qjs(get, rename = "ignoreBOM")] + fn ignore_bom(&self) -> bool { + self.ignore_bom + } + + #[qjs(get, rename = PredefinedAtom::SymbolToStringTag)] + pub fn to_string_tag(&self) -> &'static str { + stringify!(TextDecoder) + } + + pub fn decode(&self, ctx: Ctx<'js>, bytes: ObjectBytes<'js>) -> Result { + let bytes = bytes.as_bytes(&ctx)?; + let start_pos = if !self.ignore_bom { + match (&self.encoder, bytes.get(..3)) { + (Encoder::Utf16le, Some([0xFF, 0xFE, ..])) => 2, + (Encoder::Utf16be, Some([0xFE, 0xFF, ..])) => 2, + (Encoder::Utf8, Some([0xEF, 0xBB, 0xBF])) => 3, + _ => 0, + } + } else { + 0 + }; + + self.encoder + .encode_to_string(&bytes[start_pos..], !self.fatal) + .or_throw_type(&ctx, "") + } +} diff --git a/modules/src/text/text_encoder.rs b/modules/src/text/text_encoder.rs new file mode 100644 index 0000000..7502124 --- /dev/null +++ b/modules/src/text/text_encoder.rs @@ -0,0 +1,92 @@ +use crate::utils::result::ResultExt; +use rsquickjs::{ + atom::PredefinedAtom, function::Opt, Ctx, Exception, Object, Result, TypedArray, Value, +}; + +#[derive(rsquickjs::class::Trace, rsquickjs::JsLifetime)] +#[rsquickjs::class] +pub struct TextEncoder {} + +impl Default for TextEncoder { + fn default() -> Self { + Self::new() + } +} + +#[rsquickjs::methods(rename_all = "camelCase")] +impl TextEncoder { + #[qjs(constructor)] + pub fn new() -> Self { + Self {} + } + + #[qjs(get)] + fn encoding(&self) -> &str { + "utf-8" + } + + #[qjs(get, rename = PredefinedAtom::SymbolToStringTag)] + pub fn to_string_tag(&self) -> &'static str { + stringify!(TextEncoder) + } + + pub fn encode<'js>(&self, ctx: Ctx<'js>, string: Opt>) -> Result> { + if let Some(string) = string.0 { + if let Some(string) = string.as_string() { + let string = string.to_string()?; + return TypedArray::new(ctx.clone(), string.as_bytes()) + .map(|m: TypedArray<'_, u8>| m.into_value()); + } else if !string.is_undefined() { + return Err(Exception::throw_message( + &ctx, + "The \"string\" argument must be a string.", + )); + } + } + + TypedArray::new(ctx.clone(), []).map(|m: TypedArray<'_, u8>| m.into_value()) + } + + pub fn encode_into<'js>( + &self, + ctx: Ctx<'js>, + src: String, + dst: Value<'js>, + ) -> Result> { + if let Ok(typed_array) = TypedArray::::from_value(dst) { + let dst_length = typed_array.len(); + let dst_offset: usize = typed_array.get("byteOffset")?; + let array_buffer = typed_array.arraybuffer()?; + let raw = array_buffer + .as_raw() + .ok_or("ArrayBuffer is detached") + .or_throw(&ctx)?; + + let dst = unsafe { + std::slice::from_raw_parts_mut(raw.ptr.as_ptr().add(dst_offset), dst_length) + }; + + let mut written = 0; + let dst_len = dst.len(); + for ch in src.chars() { + let len = ch.len_utf8(); + if written + len > dst_len { + break; + } + written += len; + } + dst[..written].copy_from_slice(&src.as_bytes()[..written]); + let read: usize = src[..written].chars().map(char::len_utf16).sum(); + + let obj = Object::new(ctx)?; + obj.set("read", read)?; + obj.set("written", written)?; + Ok(obj) + } else { + Err(Exception::throw_type( + &ctx, + "The \"dest\" argument must be an instance of Uint8Array.", + )) + } + } +} diff --git a/modules/src/tls/config.rs b/modules/src/tls/config.rs index 3480e0a..595fc4f 100644 --- a/modules/src/tls/config.rs +++ b/modules/src/tls/config.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::sync::{Arc, OnceLock}; use rustls::{ diff --git a/modules/src/tls/no_verification.rs b/modules/src/tls/no_verification.rs index 048030a..42a67a8 100644 --- a/modules/src/tls/no_verification.rs +++ b/modules/src/tls/no_verification.rs @@ -1,5 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 use std::sync::Arc; use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; diff --git a/modules/src/utils/mod.rs b/modules/src/utils/mod.rs index a07631d..abff472 100644 --- a/modules/src/utils/mod.rs +++ b/modules/src/utils/mod.rs @@ -20,3 +20,57 @@ pub mod provider; pub mod result; pub mod test; pub mod time; + +#[macro_export] +macro_rules! count_members { + () => (0); + ($head:tt $(,$tail:tt)*) => (1 + count_members!($($tail),*)); +} + +#[macro_export] +macro_rules! iterable_enum { + ($name:ident, $($variant:ident),*) => { + impl $name { + const VARIANTS: &'static [$name] = &[$($name::$variant,)*]; + pub fn iter() -> std::slice::Iter<'static, $name> { + Self::VARIANTS.iter() + } + + #[allow(dead_code)] + fn _ensure_all_variants(s: Self) { + match s { + $($name::$variant => {},)* + } + } + } + }; +} + +#[macro_export] +macro_rules! str_enum { + ($name:ident, $($variant:ident => $str:expr),*) => { + impl $name { + pub fn as_str(&self) -> &'static str { + match self { + $($name::$variant => $str,)* + } + } + } + + impl AsRef for $name { + fn as_ref(&self) -> &str { + self.as_str() + } + } + + impl TryFrom<&str> for $name { + type Error = String; + fn try_from(s: &str) -> std::result::Result { + match s { + $($str => Ok($name::$variant),)* + _ => Err(["'", s, "' not available"].concat()) + } + } + } + }; +} diff --git a/repl/Cargo.toml b/repl/Cargo.toml index 5ff9dee..2e1692c 100644 --- a/repl/Cargo.toml +++ b/repl/Cargo.toml @@ -9,11 +9,11 @@ description = "Winter TC JavaScript modules for quickjs" keywords = ["quickjs", "ecmascript", "javascript", "es6", "es2020"] [dependencies] -rsquickjs = { workspace = true, features = ["macro"]} +rsquickjs = { workspace = true, features = ["macro"] } xmas-js-modules = { workspace = true } tracing-subscriber = "0.3.17" tracing = "0.1.44" -rustyline = {version = "17.0.1", features = ["derive"]} +rustyline = { version = "17.0.1", features = ["derive"] } tokio = { version = "1", features = ["full"] } anyhow = "1.0.100" syntect = "5.3.0" @@ -21,5 +21,3 @@ colored = "3.0.0" # itoa = "1.0.15" # ryu = "1.0.20" # simd-json = "0.17.0" -# rand = "0.9.2" - diff --git a/repl/src/lib.rs b/repl/src/lib.rs index 1fdccbe..2567aa7 100644 --- a/repl/src/lib.rs +++ b/repl/src/lib.rs @@ -1,7 +1,7 @@ use colored::*; use core::alloc; use rsquickjs::prelude::Rest; -use rsquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Value}; +use rsquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, TypedArray, Value}; use rustyline::completion::FilenameCompleter; use rustyline::error::ReadlineError; use rustyline::highlight::{Highlighter, MatchingBracketHighlighter}; @@ -9,6 +9,7 @@ use rustyline::hint::HistoryHinter; use rustyline::validate::MatchingBracketValidator; use rustyline::{Completer, Helper, Hinter, Validator}; use rustyline::{CompletionType, Config, EditMode, Editor}; +use std::any; use std::io::stdout; use std::ptr::NonNull; use syntect::easy::HighlightLines; @@ -339,7 +340,6 @@ pub async fn repl() -> anyhow::Result<()> { let _ = write_log(stdout(), &ctx, Rest(vec![v])); Ok(()) }) - .unwrap_or_else(|err| eprintln!("{}: {}", "Error".red().bold(), err)); }, Err(err) => { @@ -367,3 +367,38 @@ pub async fn repl() -> anyhow::Result<()> { Ok(()) }).await } + +#[tokio::test] +async fn fuck() { + let runtime = AsyncRuntime::new().unwrap(); + let context = AsyncContext::full(&runtime).await.unwrap(); + let allocator = xmas_js_modules::script::allocator(); + rsquickjs::async_with!(context => |ctx| { + xmas_js_modules::init(&ctx, Permissions::allow_all(), xmas_js_modules::console::LogType::Stdio).unwrap(); + + let t = ctx.get_background_task_poller(); + let ast = xmas_js_modules::script::parse("tsx", "internalSerialize(1)", &allocator).or_throw(&ctx).unwrap(); + let transformed = xmas_js_modules::script::transform( + &format!(".tsx"), + None, + false, + &allocator, + ast, + ) + .or_throw(&ctx).unwrap(); + ctx.eval_promise::<_>(transformed.as_bytes()).unwrap().into_future::().await + .catch(&ctx) + .and_then(|v| { + let v = if v.is_object() { + v.as_object().unwrap().get("value").unwrap() + } else { + v + }; + let _ = write_log(stdout(), &ctx, Rest(vec![v])); + Ok(()) + }) + .unwrap_or_else(|err| eprintln!("{}: {}", "Error".red().bold(), err)); + + }) + .await +} diff --git a/rsquickjs/core/src/context/ctx.rs b/rsquickjs/core/src/context/ctx.rs index b65f0fd..5b71bd6 100644 --- a/rsquickjs/core/src/context/ctx.rs +++ b/rsquickjs/core/src/context/ctx.rs @@ -494,7 +494,7 @@ impl<'js> Ctx<'js> { let stack_level = std::ffi::c_int::try_from(stack_level).unwrap(); let atom = unsafe { qjs::JS_GetScriptOrModuleName(self.as_ptr(), stack_level) }; #[allow(clippy::useless_conversion)] //needed for multi platform binding support - if qjs::__JS_ATOM_NULL == TryInto::::try_into(atom).unwrap() { + if qjs::__JS_ATOM_NULL as u32 == atom as u32 { unsafe { qjs::JS_FreeAtom(self.as_ptr(), atom) }; return None; } diff --git a/rsquickjs/core/src/value.rs b/rsquickjs/core/src/value.rs index bc2e2ee..2c0316b 100644 --- a/rsquickjs/core/src/value.rs +++ b/rsquickjs/core/src/value.rs @@ -47,44 +47,70 @@ unsafe impl Send for Value<'_> {} unsafe impl Sync for Value<'_> {} impl<'js> Value<'js> { - /// Serialize a value to a byte array - pub fn serialize(&self) -> Result> { - unsafe { - let len_ptr: *mut usize = &mut 0; - let flags = qjs::JS_WRITE_OBJ_BYTECODE - | qjs::JS_WRITE_OBJ_REFERENCE - | qjs::JS_WRITE_OBJ_SAB - | qjs::JS_WRITE_OBJ_STRIP_SOURCE; - let buf = qjs::JS_WriteObject( - self.ctx.as_ptr(), - len_ptr as _, - self.as_js_value(), - flags as i32, - ); - if buf.is_null() { - return Err(Error::new_from_js( - "Failed to serialize object", - "ArrayBuffer", - )); - } - let len = *len_ptr; - let vec = Vec::from_raw_parts(buf, len, len); - Ok(vec) + /// serialize the value into a byte vector + pub fn serialize(&self, ctx: Ctx<'js>) -> Result { + let mut len: u64 = 0; + let flags = qjs::JS_WRITE_OBJ_BYTECODE + | qjs::JS_WRITE_OBJ_REFERENCE + | qjs::JS_WRITE_OBJ_STRIP_SOURCE; + let buf = unsafe { + qjs::JS_WriteObject(ctx.as_ptr(), &mut len as *mut _, self.value, flags as i32) + }; + if buf.is_null() { + return Err(Error::new_from_js( + "SerializationError", + "Failed to serialize value", + )); + }; + let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len as usize) }; + let typedarray = TypedArray::new_copy(ctx, slice)?; + Ok(typedarray.into_value()) + } + + pub fn deserialize(&self, ctx: Ctx<'js>) -> Result { + let typedarray: TypedArray<'js, u8> = TypedArray::from_value(self.clone())?; + let buf = typedarray.as_bytes().ok_or_else(|| { + Error::new_from_js( + "DeserializationError", + "Failed to get bytes from ArrayBuffer", + ) + })?; + let flags = qjs::JS_READ_OBJ_BYTECODE | qjs::JS_READ_OBJ_REFERENCE; + let value = unsafe { + qjs::JS_ReadObject(ctx.as_ptr(), buf.as_ptr(), buf.len() as u64, flags as i32) + }; + if unsafe { qjs::JS_IsException(value) } { + return Err(Error::new_from_js( + "DeserializationError", + "Failed to deserialize value", + )); } + Ok(unsafe { Value::from_js_value_const(ctx, value) }) } - /// Deserialize a value from a byte array - pub fn deserialize(ctx: Ctx<'js>, data: &[u8]) -> Result { - unsafe { - let flags = - qjs::JS_READ_OBJ_BYTECODE | qjs::JS_READ_OBJ_REFERENCE | qjs::JS_READ_OBJ_SAB; - let value = - qjs::JS_ReadObject(ctx.as_ptr(), data.as_ptr(), data.len() as u64, flags as i32); - if qjs::JS_IsException(value) { - return Err(Error::new_from_js("Failed to deserialize object", "Value")); - } - Ok(Self::from_js_value(ctx, value)) + pub fn structured_clone(&self, ctx: Ctx<'js>) -> Result { + let mut len: u64 = 0; + let flags = qjs::JS_WRITE_OBJ_BYTECODE + | qjs::JS_WRITE_OBJ_REFERENCE + | qjs::JS_WRITE_OBJ_STRIP_SOURCE; + let buf = unsafe { + qjs::JS_WriteObject(ctx.as_ptr(), &mut len as *mut _, self.value, flags as i32) + }; + if buf.is_null() { + return Err(Error::new_from_js( + "SerializationError", + "Failed to serialize value", + )); + }; + let flags = qjs::JS_READ_OBJ_BYTECODE | qjs::JS_READ_OBJ_REFERENCE; + let value = unsafe { qjs::JS_ReadObject(ctx.as_ptr(), buf, len as u64, flags as i32) }; + if unsafe { qjs::JS_IsException(value) } { + return Err(Error::new_from_js( + "DeserializationError", + "Failed to deserialize value", + )); } + Ok(unsafe { Value::from_js_value_const(ctx, value) }) } } @@ -403,6 +429,11 @@ impl<'js> Value<'js> { unsafe { qjs::JS_IsArray(self.value) } } + #[inline] + pub fn is_array_buffer(&self) -> bool { + unsafe { qjs::JS_IsArrayBuffer(self.value) } + } + /// Check if the value is a function #[inline] pub fn is_function(&self) -> bool {