/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/crypto/subtle_derive_bits.rs
701 строка
20 KB
Bartek Iwańczuk
fix(ext/crypto): allow deriveBits with length 0 (#36283)
24 июл 2026, 15:07
Не верифицирован
24 июл 2026, 15:07
89e420c
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. //! `SubtleCrypto.deriveBits()` body in Rust. //! //! Per-algorithm `AlgorithmIdentifier` parameter parsing (PBKDF2 / //! HKDF / ECDH / X25519 / X448), the spec-mandated key-type / curve / //! length validation, and dispatch into the crate-internal helpers //! ([`crate::derive_bits_sync`], [`crate::x25519::x25519_derive_bits`], //! [`crate::x448::x448_derive_bits`]). use std::borrow::Cow; use argon2::Argon2; use argon2::ParamsBuilder; 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::KeyData; use crate::crypto_key::CryptoKey; use crate::crypto_key::CryptoKeyType; use crate::derive_bits_sync; use crate::key::Algorithm; use crate::key::CryptoHash; use crate::key::CryptoNamedCurve; use crate::shared::ShaHash; use crate::subtle_encrypt::extract_name_and_obj; use crate::subtle_encrypt::v8_str; use crate::subtle_key::SubtleKey; use crate::subtle_sign::read_optional_buffer_source; use crate::subtle_sign::read_required_u32; use crate::x448::x448_derive_bits; use crate::x25519::x25519_derive_bits; pub enum SubtleDeriveBitsParams { Pbkdf2 { hash_name: String, salt: Vec<u8>, iterations: u32, }, Hkdf { hash_name: String, salt: Vec<u8>, info: Vec<u8>, }, Argon2 { name: &'static str, memory: u32, passes: u32, parallelism: u32, nonce: Vec<u8>, secret_value: Option<Vec<u8>>, associated_data: Option<Vec<u8>>, }, Ecdh { public: Box<SubtleKey>, }, X25519 { public: Box<SubtleKey>, }, X448 { public: Box<SubtleKey>, }, Unknown(String), } impl SubtleDeriveBitsParams { pub fn canonical_name(&self) -> &str { match self { Self::Pbkdf2 { .. } => "PBKDF2", Self::Hkdf { .. } => "HKDF", Self::Argon2 { name, .. } => name, Self::Ecdh { .. } => "ECDH", Self::X25519 { .. } => "X25519", Self::X448 { .. } => "X448", Self::Unknown(n) => n, } } } impl<'a> WebIdlConverter<'a> for SubtleDeriveBitsParams { 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_derive_name(&name_str) else { return Ok(Self::Unknown(name_str)); }; let obj = maybe_obj.ok_or_else(|| missing_dict(prefix.clone(), &context))?; match canonical { "PBKDF2" => { let hash_name = read_required_hash_name( scope, obj, "hash", prefix.clone(), &context, )?; let salt = read_required_buffer_source( scope, obj, "salt", prefix.clone(), &context, )?; let iterations = read_required_u32( scope, obj, "iterations", prefix.clone(), &context, )?; Ok(Self::Pbkdf2 { hash_name, salt, iterations, }) } "HKDF" => { let hash_name = read_required_hash_name( scope, obj, "hash", prefix.clone(), &context, )?; let salt = read_required_buffer_source( scope, obj, "salt", prefix.clone(), &context, )?; let info = read_required_buffer_source( scope, obj, "info", prefix.clone(), &context, )?; Ok(Self::Hkdf { hash_name, salt, info, }) } "Argon2i" | "Argon2d" | "Argon2id" => { let memory = read_required_u32(scope, obj, "memory", prefix.clone(), &context)?; let passes = read_required_u32(scope, obj, "passes", prefix.clone(), &context)?; let parallelism = read_required_u32( scope, obj, "parallelism", prefix.clone(), &context, )?; let nonce = read_required_buffer_source( scope, obj, "nonce", prefix.clone(), &context, )?; let secret_value = read_optional_buffer_source( scope, obj, "secretValue", prefix.clone(), &context, )?; let associated_data = read_optional_buffer_source( scope, obj, "associatedData", prefix.clone(), &context, )?; Ok(Self::Argon2 { name: canonical, memory, passes, parallelism, nonce, secret_value, associated_data, }) } "ECDH" => { let public = read_required_public_key( scope, obj, "public", prefix.clone(), &context, )?; Ok(Self::Ecdh { public: Box::new(public), }) } "X25519" => { let public = read_required_public_key( scope, obj, "public", prefix.clone(), &context, )?; Ok(Self::X25519 { public: Box::new(public), }) } "X448" => { let public = read_required_public_key( scope, obj, "public", prefix.clone(), &context, )?; Ok(Self::X448 { public: Box::new(public), }) } _ => unreachable!(), } } } fn canonical_derive_name(name: &str) -> Option<&'static str> { const NAMES: &[&str] = &[ "PBKDF2", "HKDF", "Argon2i", "Argon2d", "Argon2id", "ECDH", "X25519", "X448", ]; 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"), ) } /// Read the `hash` field as an `AlgorithmIdentifier` (string or `{name}` dict) /// and return its raw name. Unlike [`crate::subtle_sign::read_required_hash`], /// no digest-registry lookup is performed here: that lookup is deferred to /// [`run`] so an unrecognized name maps to a spec-mandated `NotSupportedError` /// `DOMException` rather than the `TypeError` op2 produces when a /// `WebIdlConverter` returns `Err`. fn read_required_hash_name<'a, 'b>( scope: &mut v8::PinScope<'a, '_>, obj: v8::Local<'a, v8::Object>, field: &'static str, prefix: Cow<'static, str>, context: &ContextFn<'b>, ) -> Result<String, 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}'")), )); } if val.is_string() { return Ok(val.to_rust_string_lossy(scope)); } if let Ok(o) = v8::Local::<v8::Object>::try_from(val) { let name_key = v8_str(scope, "name"); let name_val = o .get(scope, name_key.into()) .unwrap_or_else(|| v8::undefined(scope).into()); let s = name_val.to_string(scope).ok_or_else(|| { WebIdlError::other( prefix.clone(), context.borrowed(), JsErrorBox::type_error(format!("'{field}.name' is not a DOMString")), ) })?; return Ok(s.to_rust_string_lossy(scope)); } Err(WebIdlError::other( prefix, context.borrowed(), JsErrorBox::type_error(format!( "'{field}' must be a HashAlgorithmIdentifier" )), )) } 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> { match read_optional_buffer_source(scope, obj, field, prefix.clone(), context)? { Some(v) => Ok(v), None => Err(WebIdlError::other( prefix, context.borrowed(), JsErrorBox::type_error(format!("required dictionary member '{field}'")), )), } } fn read_required_public_key<'a, 'b>( scope: &mut v8::PinScope<'a, '_>, obj: v8::Local<'a, v8::Object>, field: &'static str, prefix: Cow<'static, str>, context: &ContextFn<'b>, ) -> Result<SubtleKey, 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 Err(WebIdlError::other( prefix, context.borrowed(), JsErrorBox::type_error(format!("required dictionary member '{field}'")), )); } let Some(key_ptr) = deno_core::cppgc::try_unwrap_cppgc_object::<CryptoKey>(scope, val) else { return Err(WebIdlError::new( prefix, context.borrowed(), WebIdlErrorKind::ConvertToConverterType("CryptoKey"), )); }; let cryptokey: &CryptoKey = &key_ptr; let algorithm_name = cryptokey.algorithm_name(scope).ok_or_else(|| { WebIdlError::other( prefix.clone(), context.borrowed(), JsErrorBox::type_error("CryptoKey.algorithm.name is not a string"), ) })?; let usages = cryptokey.usages_as_vec(scope).ok_or_else(|| { WebIdlError::other( prefix.clone(), context.borrowed(), JsErrorBox::type_error("CryptoKey.usages has been tampered with"), ) })?; let Some(handle_ptr) = cryptokey.key_handle(scope) else { return Err(WebIdlError::other( prefix, context.borrowed(), JsErrorBox::type_error("CryptoKey handle has been tampered with"), )); }; let raw = handle_ptr.data().clone(); let algorithm_named_curve = read_algorithm_named_curve(scope, cryptokey); Ok(SubtleKey { algorithm_name, algorithm_length: None, algorithm_hash: None, algorithm_named_curve, usages, key_type: cryptokey.key_type(), extractable: cryptokey.extractable_(), raw, }) } fn read_algorithm_named_curve<'s>( scope: &mut v8::PinScope<'s, '_>, key: &CryptoKey, ) -> Option<String> { let alg = key.algorithm_local(scope)?; let key_v8 = v8::String::new_from_one_byte( scope, b"namedCurve", v8::NewStringType::Internalized, )?; let val = alg.get(scope, key_v8.into())?; if val.is_undefined() || val.is_null() { return None; } let s = val.to_string(scope)?; Some(s.to_rust_string_lossy(scope)) } const SUPPORTED_NAMED_CURVES: &[&str] = &["P-256", "P-384", "P-521"]; pub fn run( params: SubtleDeriveBitsParams, key: SubtleKey, length: Option<f64>, ) -> Result<Vec<u8>, CryptoError> { // Spec step 1: `normalizeAlgorithm` rejects unknown algorithm names with // `NotSupportedError` BEFORE the algorithm/key match. Match the spec order // so a stray Kelvin-sign `"H<U+212A>DF"` (or any other unregistered name) // produces `NotSupportedError`, not `InvalidAccessError`. if let SubtleDeriveBitsParams::Unknown(name) = ¶ms { return Err(not_supported(format!( "Algorithm '{name}' is not supported" ))); } // Spec step 7 (algorithm name match on baseKey). The `deriveBits` // usage check is the caller's responsibility -- the cppgc method // body enforces it for the external `SubtleCrypto.deriveBits` path, // while the internal `__deriveBitsInternal` path used by // `deriveKey` skips it (since `deriveKey` requires `deriveKey` // usage on the base key, not `deriveBits`). if params.canonical_name() != key.algorithm_name { return Err(invalid_access("Invalid algorithm name".to_string())); } let length_u32 = length.map(|l| l as u32); match params { SubtleDeriveBitsParams::Pbkdf2 { hash_name, salt, iterations, } => { let hash = parse_sha_hash(&hash_name) .ok_or_else(|| not_supported("Unrecognized algorithm name".into()))?; let length = length_u32.ok_or_else(|| op_error("Invalid length".to_string()))?; if length % 8 != 0 { return Err(op_error("Invalid length".to_string())); } if iterations == 0 { return Err(op_error("iterations must not be zero".to_string())); } // Per w3c/webcrypto#380 a length of 0 derives an empty bit string. // Short-circuit before `derive_bits_sync`, which asserts a positive // length (and the underlying PBKDF2 primitive panics on empty output). if length == 0 { return Ok(Vec::new()); } let key_data: KeyData = (&key.raw).into(); derive_bits_sync( key_data, None, Algorithm::Pbkdf2, Some(sha_to_crypto_hash(hash)), length as usize, Some(iterations), None, None, Some(salt), ) } SubtleDeriveBitsParams::Hkdf { hash_name, salt, info, } => { let hash = parse_sha_hash(&hash_name) .ok_or_else(|| not_supported("Unrecognized algorithm name".into()))?; let length = length_u32.ok_or_else(|| op_error("Invalid length".to_string()))?; if length % 8 != 0 { return Err(op_error("Invalid length".to_string())); } // Per w3c/webcrypto#380 a length of 0 derives an empty bit string. // Short-circuit before `derive_bits_sync`, which asserts a positive // length (and the underlying HKDF primitive panics on empty output). if length == 0 { return Ok(Vec::new()); } let key_data: KeyData = (&key.raw).into(); derive_bits_sync( key_data, None, Algorithm::Hkdf, Some(sha_to_crypto_hash(hash)), length as usize, None, None, Some(info), Some(salt), ) } SubtleDeriveBitsParams::Argon2 { name, memory, passes, parallelism, nonce, secret_value, associated_data, } => { let length = length_u32.ok_or_else(|| op_error("Invalid length".to_string()))?; if length == 0 || length % 8 != 0 { return Err(op_error("Invalid length".to_string())); } let mut builder = ParamsBuilder::new(); builder .m_cost(memory) .t_cost(passes) .p_cost(parallelism) .output_len((length / 8) as usize); if let Some(associated_data) = associated_data { let data = argon2::AssociatedData::new(&associated_data) .map_err(|_| op_error("Invalid associatedData".to_string()))?; builder.data(data); } let params = builder .build() .map_err(|_| op_error("Invalid Argon2 parameters".to_string()))?; let algorithm = match name { "Argon2i" => argon2::Algorithm::Argon2i, "Argon2d" => argon2::Algorithm::Argon2d, "Argon2id" => argon2::Algorithm::Argon2id, _ => unreachable!(), }; let argon2 = match secret_value.as_deref() { Some(secret_value) => Argon2::new_with_secret( secret_value, algorithm, argon2::Version::V0x13, params, ) .map_err(|_| op_error("Invalid secretValue".to_string()))?, None => Argon2::new(algorithm, argon2::Version::V0x13, params), }; let mut out = vec![0; (length / 8) as usize]; argon2 .hash_password_into(key.raw.bytes(), &nonce, &mut out) .map_err(|_| op_error("Argon2 derivation failed".to_string()))?; Ok(out) } SubtleDeriveBitsParams::Ecdh { public } => { if key.key_type != CryptoKeyType::Private { return Err(invalid_access("Invalid key type".to_string())); } if public.key_type != CryptoKeyType::Public { return Err(invalid_access("Invalid key type".to_string())); } if public.algorithm_name != key.algorithm_name { return Err(invalid_access("Algorithm mismatch".to_string())); } if public.algorithm_named_curve != key.algorithm_named_curve { return Err(invalid_access("'namedCurve' mismatch".to_string())); } let curve_name = key.algorithm_named_curve.as_deref().ok_or_else(|| { op_error("ECDH key is missing 'namedCurve'".to_string()) })?; if !SUPPORTED_NAMED_CURVES.contains(&curve_name) { return Err(not_supported("Not implemented".to_string())); } let named_curve = parse_named_curve(curve_name) .ok_or_else(|| not_supported("Not implemented".to_string()))?; let key_data: KeyData = (&key.raw).into(); let public_data: KeyData = (&public.raw).into(); let bits = derive_bits_sync( key_data, Some(public_data), Algorithm::Ecdh, None, length_u32.unwrap_or(0) as usize, None, Some(named_curve), None, None, )?; truncate_to_length(bits, length_u32) } SubtleDeriveBitsParams::X25519 { public } => { if key.key_type != CryptoKeyType::Private { return Err(invalid_access("Invalid key type".to_string())); } if public.key_type != CryptoKeyType::Public { return Err(invalid_access("Invalid key type".to_string())); } if public.algorithm_name != key.algorithm_name { return Err(invalid_access("Algorithm mismatch".to_string())); } let mut secret = [0u8; 32]; let is_identity = x25519_derive_bits(key.raw.bytes(), public.raw.bytes(), &mut secret) .map_err(|e| CryptoError::Other(JsErrorBox::from_err(e)))?; if is_identity { return Err(op_error("Invalid key".to_string())); } truncate_to_length(secret.to_vec(), length_u32) } SubtleDeriveBitsParams::X448 { public } => { if key.key_type != CryptoKeyType::Private { return Err(invalid_access("Invalid key type".to_string())); } if public.key_type != CryptoKeyType::Public { return Err(invalid_access("Invalid key type".to_string())); } if public.algorithm_name != key.algorithm_name { return Err(invalid_access("Algorithm mismatch".to_string())); } let mut secret = [0u8; 56]; let is_identity = x448_derive_bits(key.raw.bytes(), public.raw.bytes(), &mut secret) .map_err(|e| CryptoError::Other(JsErrorBox::from_err(e)))?; if is_identity { return Err(op_error("Invalid key".to_string())); } truncate_to_length(secret.to_vec(), length_u32) } // `Unknown` is rejected with `NotSupportedError` above before we even // check the key-algorithm match (matches the spec normalize step). SubtleDeriveBitsParams::Unknown(_) => unreachable!(), } } fn truncate_to_length( bytes: Vec<u8>, length: Option<u32>, ) -> Result<Vec<u8>, CryptoError> { let Some(length) = length else { return Ok(bytes); }; if (bytes.len() * 8) < length as usize { return Err(op_error("Invalid length".to_string())); } let n = length.div_ceil(8) as usize; Ok(bytes[..n].to_vec()) } /// Map a `HashAlgorithmIdentifier.name` string onto the digest-registry /// `ShaHash` enum. The lookup is case-sensitive and requires the exact /// canonical spelling per the spec ("SHA-256", "SHA3-256", etc.), so a stray /// "SHA384" or non-digest name like "PBKDF2" yields `None`. fn parse_sha_hash(name: &str) -> Option<ShaHash> { match name { "SHA-1" => Some(ShaHash::Sha1), "SHA-256" => Some(ShaHash::Sha256), "SHA-384" => Some(ShaHash::Sha384), "SHA-512" => Some(ShaHash::Sha512), "SHA3-256" => Some(ShaHash::Sha3_256), "SHA3-384" => Some(ShaHash::Sha3_384), "SHA3-512" => Some(ShaHash::Sha3_512), _ => None, } } fn sha_to_crypto_hash(h: ShaHash) -> CryptoHash { match h { ShaHash::Sha1 => CryptoHash::Sha1, ShaHash::Sha256 => CryptoHash::Sha256, ShaHash::Sha384 => CryptoHash::Sha384, ShaHash::Sha512 => CryptoHash::Sha512, ShaHash::Sha3_256 => CryptoHash::Sha3_256, ShaHash::Sha3_384 => CryptoHash::Sha3_384, ShaHash::Sha3_512 => CryptoHash::Sha3_512, } } fn parse_named_curve(name: &str) -> Option<CryptoNamedCurve> { match name { "P-256" => Some(CryptoNamedCurve::P256), "P-384" => Some(CryptoNamedCurve::P384), "P-521" => Some(CryptoNamedCurve::P521), _ => None, } } 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)) }