/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/crypto/subtle_decrypt.rs
608 строк
17 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.decrypt()` body in Rust. //! //! Mirrors [`crate::subtle_encrypt`] for decryption: a `SubtleDecryptParams` //! `WebIdlConverter` parses the per-algorithm dictionary, [`run`] validates //! the spec preconditions (key type, iv length, tag length, counter //! length, etc.) and dispatches into the existing per-algorithm //! `decrypt_*` helpers in [`crate::decrypt`]. 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_error::JsErrorBox; use crate::CryptoError; use crate::crypto_key::CryptoKeyType; use crate::decrypt; use crate::subtle_encrypt::extract_name_and_obj; use crate::subtle_encrypt::v8_str; use crate::subtle_key::SubtleKey; /// Normalized per-algorithm decrypt parameters. Each variant carries the /// exact dictionary members the matching `decrypt_*` helper needs. /// /// `Unknown` defers `NotSupportedError` to [`run`] so it raises a /// `DOMException` instead of the `TypeError` a converter-level error /// would emit. pub enum SubtleDecryptParams { 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 SubtleDecryptParams { 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 SubtleDecryptParams { 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_decrypt_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_decrypt_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"), ) } 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 `SharedArrayBuffer` // (or a view backed by one) and any non-BufferSource value rejects with // `TypeError`, matching the JS `webidl.converters.BufferSource` contract // for optional dictionary members (e.g. AES-GCM `additionalData`, // RSA-OAEP `label`, ChaCha20-Poly1305 `iv`). 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`: NaN/Infinity, negative, and /// `> 2**32-1` all reject; otherwise truncate toward zero. See /// [`crate::subtle_encrypt::to_enforce_range_u32`] for the matching /// encrypt-side helper. 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 and dispatch to the existing /// [`crate::decrypt`] backend helpers. pub fn run( params: SubtleDecryptParams, key: SubtleKey, data: Vec<u8>, ) -> Result<Vec<u8>, CryptoError> { // Decrypt step 8: algorithm-name match is an `OperationError` // (`encrypt` uses `InvalidAccessError`; the JS shim modelled both). if params.canonical_name() != key.algorithm_name { return Err(op_error(format!( "Decryption algorithm \"{}\" does not match key algorithm", params.canonical_name() ))); } if !key.has_usage("decrypt") { return Err(invalid_access( "The requested operation is not valid for the provided key".to_string(), )); } match params { SubtleDecryptParams::RsaOaep { label } => { if key.key_type != CryptoKeyType::Private { 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()) })?; decrypt::decrypt_rsa_oaep( &key.raw, hash, label.unwrap_or_default(), &data, ) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::AesCbc { iv } => { if iv.len() != 16 { return Err(op_error("Counter must be 16 bytes".to_string())); } let length = key.algorithm_length.ok_or_else(|| { op_error("AES-CBC key is missing 'length'".to_string()) })?; decrypt::decrypt_aes_cbc(&key.raw, length as usize, iv, &data) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::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(format!( "Counter length must not be 0 or greater than 128: received {length}" ))); } let key_length = key.algorithm_length.ok_or_else(|| { op_error("AES-CTR key is missing 'length'".to_string()) })?; decrypt::decrypt_aes_ctr( &key.raw, key_length as usize, &counter, length as usize, &data, ) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::AesGcm { iv, additional_data, tag_length, } => { 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}"))); } }; if data.len() < (tag_length / 8) as usize { return Err(op_error("The provided data is too small".to_string())); } 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()) })?; decrypt::decrypt_aes_gcm( &key.raw, key_length as usize, tag_length as usize, iv, additional_data, &data, ) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::AesOcb { iv, additional_data, tag_length, } => { 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}"))); } }; if data.len() < (tag_length / 8) as usize { return Err(op_error("The provided data is too small".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 key_length = key.algorithm_length.ok_or_else(|| { op_error("AES-OCB key is missing 'length'".to_string()) })?; decrypt::decrypt_aes_ocb( &key.raw, key_length as usize, tag_length as usize, iv, additional_data, &data, ) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::ChaCha20Poly1305 { iv, additional_data, tag_length, } => { 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(), )); } if data.len() < 16 { return Err(op_error("The provided data is too small".to_string())); } decrypt::decrypt_chacha20_poly1305(&key.raw, &iv, additional_data, &data) .map_err(decrypt_error_to_crypto) } SubtleDecryptParams::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 decrypt_error_to_crypto(e: decrypt::DecryptError) -> CryptoError { CryptoError::Other(JsErrorBox::from_err(e)) }