use std::borrow::Cow;
use aws_lc_rs::digest;
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 serde::Deserialize;
use crate::CryptoError;
use crate::key::CryptoHash;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase", tag = "name")]
pub enum SubtleDigestXof {
#[serde(rename = "cSHAKE128", rename_all = "camelCase")]
CShake128 {
output_length: u32,
#[serde(with = "serde_bytes", default)]
function_name: Option<Vec<u8>>,
#[serde(with = "serde_bytes", default)]
customization: Option<Vec<u8>>,
},
#[serde(rename = "cSHAKE256", rename_all = "camelCase")]
CShake256 {
output_length: u32,
#[serde(with = "serde_bytes", default)]
function_name: Option<Vec<u8>>,
#[serde(with = "serde_bytes", default)]
customization: Option<Vec<u8>>,
},
#[serde(rename = "TurboSHAKE128", rename_all = "camelCase")]
TurboShake128 {
output_length: u32,
domain_separation: Option<u8>,
},
#[serde(rename = "TurboSHAKE256", rename_all = "camelCase")]
TurboShake256 {
output_length: u32,
domain_separation: Option<u8>,
},
#[serde(rename = "KT128", rename_all = "camelCase")]
Kt128 {
output_length: u32,
#[serde(with = "serde_bytes", default)]
customization: Option<Vec<u8>>,
},
#[serde(rename = "KT256", rename_all = "camelCase")]
Kt256 {
output_length: u32,
#[serde(with = "serde_bytes", default)]
customization: Option<Vec<u8>>,
},
#[serde(rename = "KangarooTwelve", rename_all = "camelCase")]
KangarooTwelve {
output_length: u32,
#[serde(with = "serde_bytes", default)]
customization: Option<Vec<u8>>,
},
}
pub enum DigestAlgorithm {
Sha(CryptoHash),
Xof(SubtleDigestXof),
Unknown(String),
}
impl<'a> WebIdlConverter<'a> for DigestAlgorithm {
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_digest_name(&name_str) else {
return Ok(Self::Unknown(name_str));
};
match canonical {
"SHA-1" => Ok(Self::Sha(CryptoHash::Sha1)),
"SHA-256" => Ok(Self::Sha(CryptoHash::Sha256)),
"SHA-384" => Ok(Self::Sha(CryptoHash::Sha384)),
"SHA-512" => Ok(Self::Sha(CryptoHash::Sha512)),
"SHA3-256" => Ok(Self::Sha(CryptoHash::Sha3_256)),
"SHA3-384" => Ok(Self::Sha(CryptoHash::Sha3_384)),
"SHA3-512" => Ok(Self::Sha(CryptoHash::Sha3_512)),
"cSHAKE128" | "cSHAKE256" | "TurboSHAKE128" | "TurboSHAKE256"
| "KT128" | "KT256" | "KangarooTwelve" => {
let obj = maybe_obj.ok_or_else(|| {
WebIdlError::other(
prefix.clone(),
context.borrowed(),
JsErrorBox::type_error(format!(
"'{canonical}' requires a parameter dictionary"
)),
)
})?;
let xof = parse_xof_dict(
scope,
obj,
canonical,
prefix.clone(),
context.borrowed(),
)?;
Ok(Self::Xof(xof))
}
_ => unreachable!("canonical_digest_name returned an unknown variant"),
}
}
}
fn canonical_digest_name(name: &str) -> Option<&'static str> {
const NAMES: &[&str] = &[
"SHA-1",
"SHA-256",
"SHA-384",
"SHA-512",
"SHA3-256",
"SHA3-384",
"SHA3-512",
"cSHAKE128",
"cSHAKE256",
"TurboSHAKE128",
"TurboSHAKE256",
"KT128",
"KT256",
"KangarooTwelve",
];
NAMES
.iter()
.copied()
.find(|canon| canon.eq_ignore_ascii_case(name))
}
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"),
))
}
fn parse_xof_dict<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
canonical: &'static str,
prefix: Cow<'static, str>,
context: ContextFn<'b>,
) -> Result<SubtleDigestXof, WebIdlError> {
let output_length =
read_required_u32(scope, obj, "outputLength", prefix.clone(), &context)?;
match canonical {
"cSHAKE128" => Ok(SubtleDigestXof::CShake128 {
output_length,
function_name: read_optional_buffer(
scope,
obj,
"functionName",
prefix.clone(),
&context,
)?,
customization: read_optional_buffer(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?,
}),
"cSHAKE256" => Ok(SubtleDigestXof::CShake256 {
output_length,
function_name: read_optional_buffer(
scope,
obj,
"functionName",
prefix.clone(),
&context,
)?,
customization: read_optional_buffer(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?,
}),
"TurboSHAKE128" => Ok(SubtleDigestXof::TurboShake128 {
output_length,
domain_separation: read_optional_u8(
scope,
obj,
"domainSeparation",
prefix.clone(),
&context,
)?,
}),
"TurboSHAKE256" => Ok(SubtleDigestXof::TurboShake256 {
output_length,
domain_separation: read_optional_u8(
scope,
obj,
"domainSeparation",
prefix.clone(),
&context,
)?,
}),
"KT128" => Ok(SubtleDigestXof::Kt128 {
output_length,
customization: read_optional_buffer(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?,
}),
"KT256" => Ok(SubtleDigestXof::Kt256 {
output_length,
customization: read_optional_buffer(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?,
}),
"KangarooTwelve" => Ok(SubtleDigestXof::KangarooTwelve {
output_length,
customization: read_optional_buffer(
scope,
obj,
"customization",
prefix.clone(),
&context,
)?,
}),
_ => unreachable!(),
}
}
fn read_required_u32<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
key: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<u32, WebIdlError> {
let key_v8 = v8_str(scope, key);
let val = obj
.get(scope, key_v8.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 '{key}'")),
));
}
val.uint32_value(scope).ok_or_else(|| {
WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("'{key}' must be convertible to u32")),
)
})
}
fn read_optional_u8<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
key: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Option<u8>, WebIdlError> {
let key_v8 = v8_str(scope, key);
let val = obj
.get(scope, key_v8.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() || val.is_null() {
return Ok(None);
}
let u = val.uint32_value(scope).unwrap_or(0);
if u > u8::MAX as u32 {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("'{key}' must be in the range [0, 0xFF]")),
));
}
Ok(Some(u as u8))
}
fn read_optional_buffer<'a, 'b>(
scope: &mut v8::PinScope<'a, '_>,
obj: v8::Local<'a, v8::Object>,
key: &'static str,
prefix: Cow<'static, str>,
context: &ContextFn<'b>,
) -> Result<Option<Vec<u8>>, WebIdlError> {
let key_v8 = v8_str(scope, key);
let val = obj
.get(scope, key_v8.into())
.unwrap_or_else(|| v8::undefined(scope).into());
if val.is_undefined() || val.is_null() {
return Ok(None);
}
if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(val) {
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!(
"'{key}' is a view on a SharedArrayBuffer, which is not allowed"
)),
));
}
}
return Ok(Some(value_to_byte_vec(scope, val)));
}
if val.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!(
"'{key}' is a SharedArrayBuffer, which is not allowed"
)),
));
}
if v8::Local::<v8::ArrayBuffer>::try_from(val).is_ok() {
return Ok(Some(value_to_byte_vec(scope, val)));
}
Err(WebIdlError::other(
prefix,
context.borrowed(),
JsErrorBox::type_error(format!("'{key}' is not a BufferSource")),
))
}
fn value_to_byte_vec<'a>(
scope: &mut v8::PinScope<'a, '_>,
value: v8::Local<'a, v8::Value>,
) -> Vec<u8> {
if let Ok(view) = v8::Local::<v8::ArrayBufferView>::try_from(value) {
let byte_offset = view.byte_offset();
let byte_length = view.byte_length();
if byte_length == 0 {
return Vec::new();
}
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()
}
} else if let Ok(ab) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
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()
}
} else {
Vec::new()
}
}
pub struct BufferSource(pub Vec<u8>);
impl<'a> WebIdlConverter<'a> for BufferSource {
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> {
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,
JsErrorBox::type_error(
"is a view on a SharedArrayBuffer, which is not allowed",
),
));
}
}
return Ok(BufferSource(value_to_byte_vec(scope, value)));
}
if value.is_shared_array_buffer() {
return Err(WebIdlError::other(
prefix,
context,
JsErrorBox::type_error("is not an ArrayBuffer or a view on one"),
));
}
if v8::Local::<v8::ArrayBuffer>::try_from(value).is_ok() {
return Ok(BufferSource(value_to_byte_vec(scope, value)));
}
Err(WebIdlError::new(
prefix,
context,
WebIdlErrorKind::ConvertToConverterType("BufferSource"),
))
}
}
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::Normal)
.unwrap()
}
pub fn run(
algorithm: DigestAlgorithm,
data: Vec<u8>,
) -> Result<Vec<u8>, CryptoError> {
match algorithm {
DigestAlgorithm::Sha(hash) => {
Ok(digest::digest(hash.into(), &data).as_ref().to_vec())
}
DigestAlgorithm::Xof(xof) => run_xof(xof, &data),
DigestAlgorithm::Unknown(name) => {
Err(CryptoError::UnsupportedDigestAlgorithm(name))
}
}
}
fn run_xof(
algorithm: SubtleDigestXof,
data: &[u8],
) -> Result<Vec<u8>, CryptoError> {
use sha3::digest::ExtendableOutput;
use sha3::digest::Update;
use sha3::digest::XofReader;
use sha3::digest::core_api::CoreWrapper;
let output_length = match &algorithm {
SubtleDigestXof::CShake128 { output_length, .. }
| SubtleDigestXof::CShake256 { output_length, .. }
| SubtleDigestXof::TurboShake128 { output_length, .. }
| SubtleDigestXof::TurboShake256 { output_length, .. }
| SubtleDigestXof::Kt128 { output_length, .. }
| SubtleDigestXof::Kt256 { output_length, .. }
| SubtleDigestXof::KangarooTwelve { output_length, .. } => *output_length,
};
if !output_length.is_multiple_of(8) {
return Err(CryptoError::InvalidXofParameters);
}
let is_turbo = matches!(
algorithm,
SubtleDigestXof::TurboShake128 { .. }
| SubtleDigestXof::TurboShake256 { .. }
);
let is_kangaroo = matches!(
algorithm,
SubtleDigestXof::Kt128 { .. }
| SubtleDigestXof::Kt256 { .. }
| SubtleDigestXof::KangarooTwelve { .. }
);
if (is_turbo || is_kangaroo) && output_length == 0 {
return Err(CryptoError::InvalidXofParameters);
}
if let SubtleDigestXof::TurboShake128 {
domain_separation, ..
}
| SubtleDigestXof::TurboShake256 {
domain_separation, ..
} = &algorithm
&& let Some(d) = domain_separation
&& !(0x01..=0x7F).contains(d)
{
return Err(CryptoError::InvalidXofParameters);
}
let out_len = (output_length / 8) as usize;
let mut out = vec![0u8; out_len];
match algorithm {
SubtleDigestXof::CShake128 {
function_name,
customization,
..
} => {
let core = sha3::CShake128Core::new_with_function_name(
function_name.as_deref().unwrap_or(&[]),
customization.as_deref().unwrap_or(&[]),
);
let mut h: sha3::CShake128 = CoreWrapper::from_core(core);
h.update(data);
h.finalize_xof().read(&mut out);
}
SubtleDigestXof::CShake256 {
function_name,
customization,
..
} => {
let core = sha3::CShake256Core::new_with_function_name(
function_name.as_deref().unwrap_or(&[]),
customization.as_deref().unwrap_or(&[]),
);
let mut h: sha3::CShake256 = CoreWrapper::from_core(core);
h.update(data);
h.finalize_xof().read(&mut out);
}
SubtleDigestXof::TurboShake128 {
domain_separation, ..
} => {
let d = domain_separation.unwrap_or(0x1F);
let core = sha3::TurboShake128Core::new(d);
let mut h: sha3::TurboShake128 = CoreWrapper::from_core(core);
h.update(data);
h.finalize_xof().read(&mut out);
}
SubtleDigestXof::TurboShake256 {
domain_separation, ..
} => {
let d = domain_separation.unwrap_or(0x1F);
let core = sha3::TurboShake256Core::new(d);
let mut h: sha3::TurboShake256 = CoreWrapper::from_core(core);
h.update(data);
h.finalize_xof().read(&mut out);
}
SubtleDigestXof::Kt128 { customization, .. }
| SubtleDigestXof::KangarooTwelve { customization, .. } => {
kangaroo_twelve_128(
data,
customization.as_deref().unwrap_or(&[]),
&mut out,
);
}
SubtleDigestXof::Kt256 { customization, .. } => {
kangaroo_twelve_256(
data,
customization.as_deref().unwrap_or(&[]),
&mut out,
);
}
}
Ok(out)
}
const KT_CHUNK_SIZE: usize = 8192;
fn encode_len(mut len: usize) -> Vec<u8> {
if len == 0 {
return vec![0];
}
let mut bytes = Vec::new();
while len > 0 {
bytes.push((len & 0xff) as u8);
len >>= 8;
}
bytes.reverse();
bytes.push(bytes.len() as u8);
bytes
}
fn kangaroo_twelve_128(data: &[u8], customization: &[u8], out: &mut [u8]) {
use tiny_keccak::Hasher;
let mut h = tiny_keccak::KangarooTwelve::new(customization);
h.update(data);
h.finalize(out);
}
fn kangaroo_twelve_256(data: &[u8], customization: &[u8], out: &mut [u8]) {
kangaroo_twelve_turbo::<136>(data, customization, out);
}
fn kangaroo_twelve_turbo<const RATE: usize>(
data: &[u8],
customization: &[u8],
out: &mut [u8],
) {
let mut input = Vec::with_capacity(data.len() + customization.len() + 9);
input.extend_from_slice(data);
input.extend_from_slice(customization);
input.extend_from_slice(&encode_len(customization.len()));
if input.len() <= KT_CHUNK_SIZE {
let mut h = TurboShakeNode::<RATE>::new(0x07);
h.update(&input);
h.squeeze(out);
return;
}
let cv_len = if RATE == 168 { 32 } else { 64 };
let mut h = TurboShakeNode::<RATE>::new(0x06);
h.update(&input[..KT_CHUNK_SIZE]);
h.update(&[0x03, 0, 0, 0, 0, 0, 0, 0]);
let mut chunks = 0usize;
let mut tail = &input[KT_CHUNK_SIZE..];
while !tail.is_empty() {
let take = tail.len().min(KT_CHUNK_SIZE);
let mut inner = TurboShakeNode::<RATE>::new(0x0b);
inner.update(&tail[..take]);
let mut cv = vec![0u8; cv_len];
inner.squeeze(&mut cv);
h.update(&cv);
chunks += 1;
tail = &tail[take..];
}
h.update(&encode_len(chunks));
h.update(&[0xff, 0xff]);
h.squeeze(out);
}
struct TurboShakeNode<const RATE: usize> {
state: [u64; 25],
offset: usize,
delim: u8,
squeezing: bool,
}
impl<const RATE: usize> TurboShakeNode<RATE> {
fn new(delim: u8) -> Self {
Self {
state: [0; 25],
offset: 0,
delim,
squeezing: false,
}
}
fn update(&mut self, mut input: &[u8]) {
debug_assert!(!self.squeezing);
while !input.is_empty() {
let take = input.len().min(RATE - self.offset);
xor_into_state(&mut self.state, self.offset, &input[..take]);
self.offset += take;
input = &input[take..];
if self.offset == RATE {
tiny_keccak::keccakp(&mut self.state);
self.offset = 0;
}
}
}
fn squeeze(&mut self, mut out: &mut [u8]) {
if !self.squeezing {
xor_into_state(&mut self.state, self.offset, &[self.delim]);
xor_into_state(&mut self.state, RATE - 1, &[0x80]);
tiny_keccak::keccakp(&mut self.state);
self.offset = 0;
self.squeezing = true;
}
while !out.is_empty() {
if self.offset == RATE {
tiny_keccak::keccakp(&mut self.state);
self.offset = 0;
}
let take = out.len().min(RATE - self.offset);
copy_from_state(&self.state, self.offset, &mut out[..take]);
self.offset += take;
out = &mut out[take..];
}
}
}
fn xor_into_state(state: &mut [u64; 25], offset: usize, bytes: &[u8]) {
for (i, byte) in bytes.iter().copied().enumerate() {
let pos = offset + i;
state[pos / 8] ^= (byte as u64) << ((pos % 8) * 8);
}
}
fn copy_from_state(state: &[u64; 25], offset: usize, out: &mut [u8]) {
for (i, byte) in out.iter_mut().enumerate() {
let pos = offset + i;
*byte = (state[pos / 8] >> ((pos % 8) * 8)) as u8;
}
}