diff --git a/.changeset/pink-carpets-hope.md b/.changeset/pink-carpets-hope.md new file mode 100644 index 000000000..78496b4a2 --- /dev/null +++ b/.changeset/pink-carpets-hope.md @@ -0,0 +1,7 @@ +--- +'@lagon/cli': patch +'@lagon/runtime': patch +'@lagon/serverless': patch +--- + +Support RSA-OAEP for `SubtleCrypto#encrypt` & `SubtleCrypto#decrypto` diff --git a/crates/runtime/tests/crypto.rs b/crates/runtime/tests/crypto.rs index 41a0573ab..336bf8d24 100644 --- a/crates/runtime/tests/crypto.rs +++ b/crates/runtime/tests/crypto.rs @@ -546,6 +546,94 @@ async fn crypto_decrypt_aes_ctr() { .await; } +#[tokio::test] +async fn crypto_encrypt_rsa_oaep() { + utils::setup(); + let (send, receiver) = utils::create_isolate( + IsolateOptions::new( + "export async function handler() { + const key = await crypto.subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + }, + true, + ['encrypt'], + ); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'RSA-OAEP' }, + key.publicKey, + new TextEncoder().encode('hello, world'), + ); + return new Response(`${ciphertext instanceof Uint8Array} ${ciphertext.length}`); +}" + .into(), + ) + .tick_timeout(Duration::from_secs(5)) + .total_timeout(Duration::from_secs(10)), + ); + send(Request::default()); + + utils::assert_response( + &receiver, + Response::builder() + .header(CONTENT_TYPE, "text/plain;charset=UTF-8") + .body("true 128".into()) + .unwrap(), + ) + .await; +} + +#[tokio::test] +async fn crypto_decrypt_rsa_oaep() { + utils::setup(); + let (send, receiver) = utils::create_isolate( + IsolateOptions::new( + "export async function handler() { + const key = await crypto.subtle.generateKey( + { + name: 'RSA-OAEP', + modulusLength: 1024, + publicExponent: new Uint8Array([1, 0, 1]), + }, + true, + ['encrypt'], + ); + const counter = crypto.getRandomValues(new Uint8Array(16)); + + const ciphertext = await crypto.subtle.encrypt( + { name: 'RSA-OAEP' }, + key.publicKey, + new TextEncoder().encode('hello, world'), + ); + + const text = await crypto.subtle.decrypt( + { name: 'RSA-OAEP' }, + key.privateKey, + ciphertext, + ); + + return new Response(new TextDecoder().decode(text)); +}" + .into(), + ) + .tick_timeout(Duration::from_secs(5)) + .total_timeout(Duration::from_secs(10)), + ); + send(Request::default()); + + utils::assert_response( + &receiver, + Response::builder() + .header(CONTENT_TYPE, "text/plain;charset=UTF-8") + .body("hello, world".into()) + .unwrap(), + ) + .await; +} + #[tokio::test] async fn crypto_hkdf_derive_bits() { utils::setup(); diff --git a/crates/runtime_crypto/src/lib.rs b/crates/runtime_crypto/src/lib.rs index b0e3c08b1..d98787382 100644 --- a/crates/runtime_crypto/src/lib.rs +++ b/crates/runtime_crypto/src/lib.rs @@ -29,6 +29,7 @@ pub enum Algorithm { RsaPss(u32), RsassaPkcs1v15, Ecdsa(Sha), + RsaOaep(Option>), } #[derive(Debug)] @@ -123,6 +124,19 @@ pub fn extract_algorithm_object( return Ok(Algorithm::AesCtr(counter, length)); } + + if name == "RSA-OAEP" { + let label_key = v8_string(scope, "label").into(); + let label = match algorithm.get(scope, label_key) { + Some(label) => match label.is_uint8_array() { + false => None, + true => Some(extract_v8_uint8array(label)?), + }, + None => None, + }; + + return Ok(Algorithm::RsaOaep(label)); + } } Err(anyhow!("Algorithm not supported")) diff --git a/crates/runtime_crypto/src/methods/decrypt.rs b/crates/runtime_crypto/src/methods/decrypt.rs index e1b50be6f..72705b5fa 100644 --- a/crates/runtime_crypto/src/methods/decrypt.rs +++ b/crates/runtime_crypto/src/methods/decrypt.rs @@ -6,6 +6,10 @@ use ctr::cipher::StreamCipher; use ctr::Ctr128BE; use ctr::Ctr32BE; use ctr::Ctr64BE; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::Oaep; +use rsa::RsaPrivateKey; +use sha2::Sha256; use crate::{Aes256Gcm, Algorithm}; @@ -38,6 +42,17 @@ pub fn decrypt(algorithm: Algorithm, key_value: Vec, data: Vec) -> Resul "invalid counter length. Currently supported 32/64/128 bits", )), }, + Algorithm::RsaOaep(label) => { + let private_key = RsaPrivateKey::from_pkcs1_der(&key_value)?; + let padding = match label { + Some(buf) => Oaep::new_with_label::(String::from_utf8(buf)?), + None => Oaep::new::(), + }; + + private_key + .decrypt(padding, &data) + .map_err(|e| anyhow!(e.to_string())) + } _ => Err(anyhow!("Algorithm not supported")), } } diff --git a/crates/runtime_crypto/src/methods/encrypt.rs b/crates/runtime_crypto/src/methods/encrypt.rs index 23322af88..43dcc6ce9 100644 --- a/crates/runtime_crypto/src/methods/encrypt.rs +++ b/crates/runtime_crypto/src/methods/encrypt.rs @@ -6,6 +6,11 @@ use ctr::cipher::StreamCipher; use ctr::Ctr128BE; use ctr::Ctr32BE; use ctr::Ctr64BE; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::rand_core::OsRng; +use rsa::Oaep; +use rsa::RsaPrivateKey; +use sha2::Sha256; use crate::{Aes256Gcm, Algorithm}; @@ -35,6 +40,19 @@ pub fn encrypt(algorithm: Algorithm, key_value: Vec, data: Vec) -> Resul "invalid counter length. Currently supported 32/64/128 bits", )), }, + Algorithm::RsaOaep(label) => { + let public_key = RsaPrivateKey::from_pkcs1_der(&key_value)?.to_public_key(); + let mut rng = OsRng; + let padding = match label { + Some(buf) => Oaep::new_with_label::(String::from_utf8(buf)?), + None => Oaep::new::(), + }; + let encrypted = public_key + .encrypt(&mut rng, padding, &data) + .map_err(|_| anyhow!("Encryption failed"))?; + + Ok(encrypted) + } _ => Err(anyhow!("Algorithm not supported")), } } diff --git a/packages/docs/pages/runtime-apis.mdx b/packages/docs/pages/runtime-apis.mdx index 304862241..798d552c5 100644 --- a/packages/docs/pages/runtime-apis.mdx +++ b/packages/docs/pages/runtime-apis.mdx @@ -104,7 +104,7 @@ The standard `CryptoSubtle` object. [See the documentation on MDN](https://devel | RSA-PSS | ✅ | | | | | | ECDSA | ✅ | | | | | | HMAC | ✅ | | | | | -| RSA-OAEP | | ❌ | | | ❌ | +| RSA-OAEP | | ✅ | | | ✅ | | AES-CTR | | ✅ | | | ✅ | | AES-CBC | | ✅ | | | ✅ | | AES-GCM | | ✅ | | | ✅ |