/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/crypto/subtle_encrypt.rs
675 строк
19 KB
em
perf(ext/crypto): port WebCrypto from JS to Rust (#34966)
13 июн 2026, 15:07
Не верифицирован
13 июн 2026, 15:07
c5fee2e
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! `SubtleCrypto.encrypt()` body in Rust. //! //! `WebIdlConverter` for the per-operation `AlgorithmIdentifier` (`RSA-OAEP`, //! `AES-CBC`, `AES-CTR`, `AES-GCM`, `AES-OCB`, `ChaCha20-Poly1305`), the //! per-algorithm spec validation (`OperationError` on bad iv length, bad //! tag length, etc.), and the dispatch into the existing per-algorithm //! `encrypt_*` helpers in [`crate::encrypt`]. 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; /// Normalized per-algorithm encrypt parameters. Each variant carries /// exactly the dictionary members the matching `encrypt_*` helper needs. /// /// `Unknown` is produced when the input has a string-coercible `.name` /// that the encrypt registry doesn't know about; the impl method turns /// it into a `NotSupportedError` `DOMException` (not the `TypeError` a /// converter-level error would emit). 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); } // Route through the strict `BufferSource` guard so a `SharedArrayBuffer` // (or a view backed by one) and any non-BufferSource value rejects with // `TypeError`, matching the JS `webidl.converters.BufferSource` contract. // The previous silent-`None` path let AES-GCM `additionalData`, RSA-OAEP // `label`, cSHAKE `functionName`/`customization`, and ChaCha20-Poly1305 // `iv` through with garbage shapes. 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(); // SAFETY: V8 guarantees byte_offset + byte_length stay within the // backing store and a non-detached buffer has a non-null data pointer. 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(); } // SAFETY: as above. unsafe { let base = ab.data().unwrap().as_ptr() as *const u8; std::slice::from_raw_parts(base, byte_length).to_vec() } } /// `[EnforceRange] unsigned long` conversion. WebCrypto uses this for /// AES-CTR `length` and AES-GCM `tagLength`; `uint32_value` (ToUint32 /// truncation) would let `2**32 + 5` wrap to `5` past the JS converter /// that asserted `TypeError`. 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" )), )), } } /// Validate the per-algorithm prerequisites (key type, iv length, tag /// length, counter length, etc.) and dispatch to the existing /// [`crate::encrypt`] backend helpers. 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())); } // Spec order: validate `tagLength` before `iv` length so a bad // tag length combined with an unsupported IV length rejects with // the `OperationError` WebCrypto requires (WPT // `aes_gcm_256_iv` "illegal tag length" subtests). Mirrors the // decrypt path. 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, } => { // Match the AES-GCM/OCB "Plaintext too large" cap for parity with // the legacy JS impl. RFC 8439 §2.8 caps ChaCha20-Poly1305 at // `(2^32 - 1) * 64` bytes (< 2^39); the AES cap is the tighter // value the spec uses for AES-GCM, and ArrayBuffer maxes well // below either limit, so this is defense in depth. 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)) }