use std::borrow::Cow;
use deno_core::v8;
use deno_core::webidl::ContextFn;
use deno_core::webidl::WebIdlConverter;
use deno_core::webidl::WebIdlError;
use deno_core::webidl::WebIdlErrorKind;
use deno_error::JsErrorBox;
use crate::CryptoError;
use crate::crypto_key::CryptoKeyType;
use crate::encrypt;
use crate::subtle_key::SubtleKey;
pub enum SubtleEncryptParams {
RsaOaep {
label: Option<Vec<u8>>,
},
AesCbc {
iv: Vec<u8>,
},
AesCtr {
counter: Vec<u8>,
length: u32,
},
AesGcm {
iv: Vec<u8>,
additional_data: Option<Vec<u8>>,
tag_length: Option<u32>,
},
AesOcb {
iv: Vec<u8>,
additional_data: Option<Vec<u8>>,
tag_length: Option<u32>,
},
ChaCha20Poly1305 {
iv: Option<Vec<u8>>,
additional_data: Option<Vec<u8>>,
tag_length: Option<u32>,
},
Unknown(String),
}
impl SubtleEncryptParams {
pub fn canonical_name(&self) -> &str {
match self {
Self::RsaOaep { .. } => "RSA-OAEP",
Self::AesCbc { .. } => "AES-CBC",
Self::AesCtr { .. } => "AES-CTR",
Self::AesGcm { .. } => "AES-GCM",
Self::AesOcb { .. } => "AES-OCB",
Self::ChaCha20Poly1305 { .. } => "ChaCha20-Poly1305",
Self::Unknown(n) => n,
}
}
}
impl<'a> WebIdlConverter<'a> for SubtleEncryptParams {
type Options = ();
fn convert<'b>(
scope: &mut v8::PinScope<'a, '_>,
value: v8::Local<'a, v8::Value>,
prefix: Cow<'static, str>,
context: ContextFn<'b>,
_options: &Self::Options,
) -> Result<Self, WebIdlError> {
let (name_str, maybe_obj) =
extract_name_and_obj(scope, value, prefix.clone(), context.borrowed())?;
let Some(canonical) = canonical_encrypt_name(&name_str) else {
return Ok(Self::Unknown(name_str));
};
match canonical {
"RSA-OAEP" => {
let label = match maybe_obj {
Some(o) => read_optional_buffer_source(
scope,
o,
"label",
prefix.clone(),
&context,
)?,
None => None,
};
Ok(Self::RsaOaep { label })
}
"AES-CBC" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let iv = read_required_buffer_source(
scope,
obj,
"iv",
prefix.clone(),
&context,
)?;
Ok(Self::AesCbc { iv })
}
"AES-CTR" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let counter = read_required_buffer_source(
scope,
obj,
"counter",
prefix.clone(),
&context,
)?;
let length =
read_required_u32(scope, obj, "length", prefix.clone(), &context)?;
Ok(Self::AesCtr { counter, length })
}
"AES-GCM" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let iv = read_required_buffer_source(
scope,
obj,
"iv",
prefix.clone(),
&context,
)?;
let additional_data = read_optional_buffer_source(
scope,
obj,
"additionalData",
prefix.clone(),
&context,
)?;
let tag_length =
read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
Ok(Self::AesGcm {
iv,
additional_data,
tag_length,
})
}
"AES-OCB" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let iv = read_required_buffer_source(
scope,
obj,
"iv",
prefix.clone(),
&context,
)?;
let additional_data = read_optional_buffer_source(
scope,
obj,
"additionalData",
prefix.clone(),
&context,
)?;
let tag_length =
read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
Ok(Self::AesOcb {
iv,
additional_data,
tag_length,
})
}
"ChaCha20-Poly1305" => {
let obj =
maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?;
let iv = read_optional_buffer_source(
scope,
obj,
"iv",
prefix.clone(),
&context,
)?;
let additional_data = read_optional_buffer_source(
scope,
obj,
"additionalData",
prefix.clone(),
&context,
)?;
let tag_length =
read_optional_u32(scope, obj, "tagLength", prefix.clone(), &context)?;
Ok(Self::ChaCha20Poly1305 {
iv,
additional_data,
tag_length,
})
}
_ => unreachable!(),
}
}
}
fn canonical_encrypt_name(name: &str) -> Option<&'static str> {
const NAMES: &[&str] = &[
"RSA-OAEP",
"AES-CBC",
"AES-CTR",
"AES-GCM",
"AES-OCB",
"ChaCha20-Poly1305",
];
NAMES.iter().copied().find(|n| n.eq_ignore_ascii_case(name))
}
fn missing_dict(
prefix: Cow<'static, str>,
context: &ContextFn<'_>,
) -> WebIdlError {
WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error("Algorithm requires a parameter dictionary"),
)
}
pub(crate) fn extract_name_and_obj<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
value: v8::Local<'a, v8::Value>,
prefix: Cow<'static, str>,
context: ContextFn<'b>,
) -> Result<(String, Option<v8::Local<'a, v8::Object>>), WebIdlError> {
if value.is_string() {
let s = value.to_rust_string_lossy(scope);
return Ok((s, None));
}
if let Ok(obj) = v8::Local::<v8::Object>::try_from(value) {
let name_key = v8_str(scope, "name");
let name_val = obj
.get(scope, name_key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if name_val.is_undefined() {
return Err(WebIdlError::other(
prefix,
context,
JsErrorBox::type_error("required member 'name' is undefined"),
));
}
let s = name_val
.to_string(scope)
.ok_or_else(|| {
WebIdlError::other(
prefix.clone(),
context.borrowed(),
JsErrorBox::type_error(
"algorithm.name is not convertible to DOMString",
),
)
})?
.to_rust_string_lossy(scope);
return Ok((s, Some(obj)));
}
Err(WebIdlError::new(
prefix,
context,
WebIdlErrorKind::ConvertToConverterType("AlgorithmIdentifier"),
))
}
pub(crate) fn v8_str<'s>(
scope: &mut v8::PinScope<'s, '_>,
s: &str,
) -> v8::Local<'s, v8::String> {
v8::String::new_from_one_byte(
scope,
s.as_bytes(),
v8::NewStringType::Internalized,
)
.unwrap()
}
fn read_required_buffer_source<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Vec<u8>, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("required dictionary member '{field}'")),
));
}
value_to_buffer_source(scope, val, field, prefix, context)
}
fn read_optional_buffer_source<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Option<Vec<u8>>, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() || val.is_null() {
return Ok(None);
}
value_to_buffer_source(scope, val, field, prefix, context).map(Some)
}
fn value_to_buffer_source<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
value: v8::Local<'a, v8::Value>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Vec<u8>, WebIdlError> {
if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(value) {
if let Some(ab) = view.buffer(scope) {
let ab_val: v8::Local<v8::Value> = ab.into();
if ab_val.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is a view on a SharedArrayBuffer, which is not allowed"
)),
));
}
}
return Ok(view_to_bytes(scope, view));
}
if value.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is a SharedArrayBuffer, which is not allowed"
)),
));
}
if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
return Ok(arraybuffer_to_bytes(ab));
}
Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("'{field}' is not a BufferSource")),
))
}
fn view_to_bytes<'a>(
scope: &mut v8::PinScope<'a, '_>,
view: v8::Local<'a, v8::ArrayBufferView>,
) -> Vec<u8> {
let byte_length = view.byte_length();
if byte_length == 0 {
return Vec::new();
}
let byte_offset = view.byte_offset();
let ab = view.buffer(scope).unwrap();
unsafe {
let base = ab.data().unwrap().as_ptr() as *const u8;
std::slice::from_raw_parts(base.add(byte_offset), byte_length).to_vec()
}
}
fn arraybuffer_to_bytes(ab: v8::Local<v8::ArrayBuffer>) -> Vec<u8> {
let byte_length = ab.byte_length();
if byte_length == 0 {
return Vec::new();
}
unsafe {
let base = ab.data().unwrap().as_ptr() as *const u8;
std::slice::from_raw_parts(base, byte_length).to_vec()
}
}
fn to_enforce_range_u32<'a>(
scope: &mut v8::PinScope<'a, '_>,
val: v8::Local<'a, v8::Value>,
) -> Option<u32> {
let n = val.number_value(scope)?;
if !n.is_finite() {
return None;
}
let trunc = n.trunc();
if trunc < 0.0 || trunc > u32::MAX as f64 {
return None;
}
Some(trunc as u32)
}
fn read_required_u32<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<u32, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("required dictionary member '{field}'")),
));
}
to_enforce_range_u32(scope, val).ok_or_else(|| {
WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is outside the [0, 2**32-1] range"
)),
)
})
}
fn read_optional_u32<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
field: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Option<u32>, WebIdlError> {
let key = v8_str(scope, field);
let val = obj
.get(scope, key.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() || val.is_null() {
return Ok(None);
}
match to_enforce_range_u32(scope, val) {
Some(v) => Ok(Some(v)),
None => Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{field}' is outside the [0, 2**32-1] range"
)),
)),
}
}
pub fn run(
params: SubtleEncryptParams,
key: SubtleKey,
data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
if params.canonical_name() != key.algorithm_name {
return Err(invalid_access(format!(
"Encryption algorithm '{}' does not match key algorithm",
params.canonical_name()
)));
}
if !key.has_usage("encrypt") {
return Err(invalid_access(
"The requested operation is not valid for the provided key".to_string(),
));
}
match params {
SubtleEncryptParams::RsaOaep { label } => {
if key.key_type != CryptoKeyType::Public {
return Err(invalid_access("Key type not supported".to_string()));
}
let hash = key.algorithm_hash.ok_or_else(|| {
op_error("RSA-OAEP key is missing 'hash'".to_string())
})?;
encrypt::encrypt_rsa_oaep(
&key.raw,
hash,
label.unwrap_or_default(),
&data,
)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::AesCbc { iv } => {
if iv.len() != 16 {
return Err(op_error(
"Initialization vector must be 16 bytes".to_string(),
));
}
let length = key.algorithm_length.ok_or_else(|| {
op_error("AES-CBC key is missing 'length'".to_string())
})?;
encrypt::encrypt_aes_cbc(&key.raw, length as usize, iv, &data)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::AesCtr { counter, length } => {
if counter.len() != 16 {
return Err(op_error("Counter vector must be 16 bytes".to_string()));
}
if length == 0 || length > 128 {
return Err(op_error(
"Counter length must not be 0 or greater than 128".to_string(),
));
}
let key_length = key.algorithm_length.ok_or_else(|| {
op_error("AES-CTR key is missing 'length'".to_string())
})?;
encrypt::encrypt_aes_ctr(
&key.raw,
key_length as usize,
&counter,
length as usize,
&data,
)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::AesGcm {
iv,
additional_data,
tag_length,
} => {
if data.len() > ((1u64 << 39) - 256) as usize {
return Err(op_error("Plaintext too large".to_string()));
}
let tag_length = match tag_length {
None => 128u32,
Some(t) if [32, 64, 96, 104, 112, 120, 128].contains(&t) => t,
Some(t) => {
return Err(op_error(format!("Invalid tag length: {t}")));
}
};
let iv_len = iv.len();
if iv_len != 12 && iv_len != 16 {
return Err(not_supported(
"Initialization vector length not supported".to_string(),
));
}
let key_length = key.algorithm_length.ok_or_else(|| {
op_error("AES-GCM key is missing 'length'".to_string())
})?;
encrypt::encrypt_aes_gcm(
&key.raw,
key_length as usize,
tag_length as usize,
iv,
additional_data,
&data,
)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::AesOcb {
iv,
additional_data,
tag_length,
} => {
if data.len() > ((1u64 << 39) - 256) as usize {
return Err(op_error("Plaintext too large".to_string()));
}
let iv_len = iv.len();
if !(6..=15).contains(&iv_len) {
return Err(op_error(
"Invalid nonce length for AES-OCB (must be 6-15 bytes)".to_string(),
));
}
let tag_length = match tag_length {
None => 128u32,
Some(t) if [64, 96, 128].contains(&t) => t,
Some(t) => {
return Err(op_error(format!("Invalid tag length: {t}")));
}
};
let key_length = key.algorithm_length.ok_or_else(|| {
op_error("AES-OCB key is missing 'length'".to_string())
})?;
encrypt::encrypt_aes_ocb(
&key.raw,
key_length as usize,
tag_length as usize,
iv,
additional_data,
&data,
)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::ChaCha20Poly1305 {
iv,
additional_data,
tag_length,
} => {
if data.len() > ((1u64 << 39) - 256) as usize {
return Err(op_error("Plaintext too large".to_string()));
}
let Some(iv) = iv else {
return Err(CryptoError::Other(JsErrorBox::type_error(
"iv is required",
)));
};
if iv.len() != 12 {
return Err(op_error(
"ChaCha20-Poly1305 iv must be 12 bytes".to_string(),
));
}
if let Some(t) = tag_length
&& t != 128
{
return Err(op_error(
"ChaCha20-Poly1305 tagLength must be 128".to_string(),
));
}
encrypt::encrypt_chacha20_poly1305(&key.raw, &iv, additional_data, &data)
.map_err(encrypt_error_to_crypto)
}
SubtleEncryptParams::Unknown(name) => {
Err(CryptoError::Other(JsErrorBox::new(
"DOMExceptionNotSupportedError",
format!("Algorithm '{name}' is not supported"),
)))
}
}
}
fn invalid_access(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionInvalidAccessError", msg))
}
fn op_error(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionOperationError", msg))
}
fn not_supported(msg: String) -> CryptoError {
CryptoError::Other(JsErrorBox::new("DOMExceptionNotSupportedError", msg))
}
fn encrypt_error_to_crypto(e: encrypt::EncryptError) -> CryptoError {
CryptoError::Other(JsErrorBox::from_err(e))
}