Skip to content

Commit

Permalink
feat(runtime): support AES-CTR for SubtleCrypto#encrypt & `SubtleCr…
Browse files Browse the repository at this point in the history
…ypto#decrypt` (#863)

Co-authored-by: QuiiBz <[email protected]>
  • Loading branch information
akitaSummer and QuiiBz authored May 14, 2023
1 parent 22f5cc1 commit 4e6968a
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 5 deletions.
9 changes: 9 additions & 0 deletions .changeset/brown-colts-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@lagon/cli': patch
'@lagon/runtime': patch
'@lagon/serverless': patch
'@lagon/docs': patch
'@lagon/js-runtime': patch
---

Add AES-CTR to `SubtleCrypto#encrypt` & `SubtleCrypto#decrypt`
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 80 additions & 4 deletions crates/runtime/tests/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,78 @@ async fn crypto_decrypt_aes_cbc() {
);
}

#[tokio::test]
async fn crypto_encrypt_aes_ctr() {
utils::setup();
let (send, receiver) = utils::create_isolate(IsolateOptions::new(
"export async function handler() {
const key = await crypto.subtle.generateKey(
{
name: 'AES-CTR',
length: 256,
},
true,
['encrypt'],
);
const counter = crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-CTR', counter, length: 32 },
key,
new TextEncoder().encode('hello, world'),
);
return new Response(`${ciphertext instanceof Uint8Array} ${ciphertext.length}`);
}"
.into(),
));
send(Request::default());

assert_eq!(
receiver.recv_async().await.unwrap().as_response(),
Response::from("true 12")
);
}

#[tokio::test]
async fn crypto_decrypt_aes_ctr() {
utils::setup();
let (send, receiver) = utils::create_isolate(IsolateOptions::new(
"export async function handler() {
const key = await crypto.subtle.generateKey(
{
name: 'AES-CTR',
length: 256,
},
true,
['encrypt'],
);
const counter = crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-CTR', counter, length: 32 },
key,
new TextEncoder().encode('hello, world'),
);
const text = await crypto.subtle.decrypt(
{ name: 'AES-CTR', counter, length: 32 },
key,
ciphertext,
);
return new Response(new TextDecoder().decode(text));
}"
.into(),
));
send(Request::default());

assert_eq!(
receiver.recv_async().await.unwrap().as_response(),
Response::from("hello, world")
);
}

#[tokio::test]
async fn crypto_hkdf_derive_bits() {
utils::setup();
Expand Down Expand Up @@ -478,8 +550,9 @@ async fn crypto_pbkdf2_derive_bits() {
#[tokio::test]
async fn crypto_ecdh_derive_bits() {
utils::setup();
let (send, receiver) = utils::create_isolate(IsolateOptions::new(
"export async function handler() {
let (send, receiver) = utils::create_isolate(
IsolateOptions::new(
"export async function handler() {
const keypair_1 = await crypto.subtle.generateKey(
{
name: 'ECDH',
Expand Down Expand Up @@ -517,8 +590,11 @@ async fn crypto_ecdh_derive_bits() {
);
return new Response(`${result_1.byteLength * 8} ${result_2.byteLength * 8}`);
}"
.into(),
));
.into(),
)
.tick_timeout(Duration::from_secs(5))
.total_timeout(Duration::from_secs(10)),
);
send(Request::default());

assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions crates/runtime_crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ num-traits = "0.2.15"
rsa = { version = "=0.9.2", default-features = false, features = ["std", "sha2"] }
p256 = { version = "0.13.2", features = ["ecdh"] }
p384 = "0.13.0"
ctr = "0.9.1"
17 changes: 17 additions & 0 deletions crates/runtime_crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub enum Algorithm {
Hmac,
AesGcm(Vec<u8>),
AesCbc(Vec<u8>),
AesCtr(Vec<u8>, u32),
RsaPss(u32),
RsassaPkcs1v15,
Ecdsa(Sha),
Expand Down Expand Up @@ -106,6 +107,22 @@ pub fn extract_algorithm_object(

return Ok(Algorithm::AesCbc(iv));
}

if name == "AES-CTR" {
let counter_key = v8_string(scope, "counter");
let counter = match algorithm.get(scope, counter_key.into()) {
Some(counter) => extract_v8_uint8array(counter)?,
None => return Err(anyhow!("AES-CTR counter not found")),
};

let length_key = v8_string(scope, "length").into();
let length = match algorithm.get(scope, length_key) {
Some(lv) => extract_v8_uint32(scope, lv)?,
None => return Err(anyhow!("AES-CTR length not found")),
};

return Ok(Algorithm::AesCtr(counter, length));
}
}

Err(anyhow!("Algorithm not supported"))
Expand Down
27 changes: 27 additions & 0 deletions crates/runtime_crypto/src/methods/decrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
use aes_gcm::{aead::Aead, KeyInit, Nonce};
use anyhow::{anyhow, Result};

use ctr::cipher::StreamCipher;
use ctr::Ctr128BE;
use ctr::Ctr32BE;
use ctr::Ctr64BE;

use crate::{Aes256Gcm, Algorithm};

type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
Expand All @@ -25,6 +30,28 @@ pub fn decrypt(algorithm: Algorithm, key_value: Vec<u8>, data: Vec<u8>) -> Resul
Err(_) => Err(anyhow!("Decryption failed")),
}
}
Algorithm::AesCtr(counter, length) => match length {
32 => decrypt_aes_ctr_gen::<Ctr32BE<aes::Aes256>>(&key_value, &counter, &data),
64 => decrypt_aes_ctr_gen::<Ctr64BE<aes::Aes256>>(&key_value, &counter, &data),
128 => decrypt_aes_ctr_gen::<Ctr128BE<aes::Aes256>>(&key_value, &counter, &data),
_ => Err(anyhow!(
"invalid counter length. Currently supported 32/64/128 bits",
)),
},
_ => Err(anyhow!("Algorithm not supported")),
}
}

fn decrypt_aes_ctr_gen<B>(key: &[u8], counter: &[u8], data: &[u8]) -> Result<Vec<u8>>
where
B: KeyIvInit + StreamCipher,
{
let mut cipher = B::new(key.into(), counter.into());

let mut plaintext = data.to_vec();
cipher
.try_apply_keystream(&mut plaintext)
.map_err(|_| anyhow!("tried to decrypt too much data"))?;

Ok(plaintext)
}
27 changes: 27 additions & 0 deletions crates/runtime_crypto/src/methods/encrypt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
use aes_gcm::{aead::Aead, KeyInit, Nonce};
use anyhow::{anyhow, Result};

use ctr::cipher::StreamCipher;
use ctr::Ctr128BE;
use ctr::Ctr32BE;
use ctr::Ctr64BE;

use crate::{Aes256Gcm, Algorithm};

type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
Expand All @@ -22,6 +27,28 @@ pub fn encrypt(algorithm: Algorithm, key_value: Vec<u8>, data: Vec<u8>) -> Resul
iv.as_slice().into(),
)
.encrypt_padded_vec_mut::<Pkcs7>(&data)),
Algorithm::AesCtr(counter, length) => match length {
32 => encrypt_aes_ctr_gen::<Ctr32BE<aes::Aes256>>(&key_value, &counter, &data),
64 => encrypt_aes_ctr_gen::<Ctr64BE<aes::Aes256>>(&key_value, &counter, &data),
128 => encrypt_aes_ctr_gen::<Ctr128BE<aes::Aes256>>(&key_value, &counter, &data),
_ => Err(anyhow!(
"invalid counter length. Currently supported 32/64/128 bits",
)),
},
_ => Err(anyhow!("Algorithm not supported")),
}
}

fn encrypt_aes_ctr_gen<B>(key: &[u8], counter: &[u8], data: &[u8]) -> Result<Vec<u8>>
where
B: KeyIvInit + StreamCipher,
{
let mut cipher = B::new(key.into(), counter.into());

let mut ciphertext = data.to_vec();
cipher
.try_apply_keystream(&mut ciphertext)
.map_err(|_| anyhow!("tried to encrypt too much data"))?;

Ok(ciphertext)
}
2 changes: 1 addition & 1 deletion packages/docs/pages/runtime-apis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ The standard `CryptoSubtle` object. [See the documentation on MDN](https://devel
| ECDSA || | | | |
| HMAC || | | | |
| RSA-OAEP | || | ||
| AES-CTR | | | | | |
| AES-CTR | | | | | |
| AES-CBC | || | ||
| AES-GCM | || | ||
| SHA-1 | | || | |
Expand Down

1 comment on commit 4e6968a

@vercel
Copy link

@vercel vercel bot commented on 4e6968a May 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./packages/docs

lagon-docs.vercel.app
docs-git-main-lagon.vercel.app
docs.lagon.app
docs-lagon.vercel.app

Please sign in to comment.