use napi::bindgen_prelude::{
FromNapiValue, JsObjectValue, JsValue, KeyCollectionMode, KeyConversion, KeyFilter, Object,
ToNapiValue, Unknown,
};
use napi::{check_status, sys, Env, Error, Property, Result, Status, ValueType};
use zeroize::Zeroizing;
pub const MAX_DEPTH: usize = vitaminc_aead::frame::MAX_DEPTH;
pub(crate) fn forbidden_key(key: &str) -> bool {
matches!(key, "__proto__" | "constructor" | "prototype")
}
pub(crate) fn depth_error() -> Error {
Error::new(Status::InvalidArg, "value is nested too deeply")
}
pub(crate) fn forbidden_key_error(key: &str) -> Error {
Error::new(
Status::InvalidArg,
format!("property name is not allowed: {key}"),
)
}
const MAX_EAGER_CAPACITY: usize = 1024;
pub(crate) fn eager_capacity(reported_len: u32) -> usize {
(reported_len as usize).min(MAX_EAGER_CAPACITY)
}
pub(crate) struct OwnKey<'e> {
handle: Unknown<'e>,
pub(crate) name: Zeroizing<String>,
}
pub(crate) fn own_enumerable_keys<'e>(obj: &Object<'e>) -> Result<Vec<OwnKey<'e>>> {
let names = obj.get_all_property_names(
KeyCollectionMode::OwnOnly,
KeyFilter::Enumerable,
KeyConversion::NumbersToStrings,
)?;
let len = names.get_array_length()?;
let mut keys = Vec::with_capacity(eager_capacity(len));
for i in 0..len {
let handle = names.get_element::<Unknown>(i)?;
if handle.get_type()? == ValueType::Symbol {
continue;
}
let name = lossless_string(handle, "property name")?;
keys.push(OwnKey { handle, name });
}
Ok(keys)
}
pub(crate) fn get_own<'e>(obj: &Object<'e>, key: &OwnKey<'e>) -> Result<Unknown<'e>> {
if !obj.has_own_property_js(key.handle)? {
return Err(Error::new(
Status::InvalidArg,
format!(
"property was removed while it was being read: {}",
*key.name
),
));
}
obj.get_property_unchecked(key.handle)
}
pub(crate) fn lossless_string(value: Unknown<'_>, what: &str) -> Result<Zeroizing<String>> {
let env = value.value().env;
let raw = value.raw();
let mut len = 0usize;
let status =
unsafe { sys::napi_get_value_string_utf16(env, raw, std::ptr::null_mut(), 0, &mut len) };
check_status!(status, "failed to measure a {what}")?;
let mut units = Zeroizing::new(vec![0u16; len + 1]);
let mut written = 0usize;
let status = unsafe {
sys::napi_get_value_string_utf16(env, raw, units.as_mut_ptr(), len + 1, &mut written)
};
check_status!(status, "failed to read a {what}")?;
from_utf16(&units[..written.min(len)]).ok_or_else(|| {
Error::new(
Status::InvalidArg,
format!("{what} contains an unpaired surrogate and cannot be encrypted losslessly"),
)
})
}
fn from_utf16(units: &[u16]) -> Option<Zeroizing<String>> {
let mut utf8_len = 0usize;
for c in char::decode_utf16(units.iter().copied()) {
utf8_len += c.ok()?.len_utf8();
}
let mut text = Zeroizing::new(String::with_capacity(utf8_len));
for c in char::decode_utf16(units.iter().copied()).flatten() {
text.push(c);
}
Some(text)
}
pub(crate) fn get_property_unknown<'e>(obj: &Object<'e>, key: &str) -> Result<Unknown<'e>> {
let env = Env::from_raw(obj.value().env);
let js_key = key.into_unknown(&env)?;
obj.get_property_unchecked(js_key)
}
pub(crate) fn not_plain_error() -> Error {
Error::new(
Status::InvalidArg,
"only plain objects and the wrapper classes can be encrypted; Map, Set, class instances \
and other exotic objects lose their contents under key enumeration — convert the value \
to a plain object, array, string, number, Buffer, or Uint8Array first",
)
}
pub(crate) fn own_property<'e, V: JsValue<'e>>(
env: &Env,
key: &str,
value: &V,
) -> Result<Property> {
Ok(Property::new().with_name(env, key)?.with_value(value))
}
pub(crate) fn define_own_properties(obj: &mut Object<'_>, properties: &[Property]) -> Result<()> {
obj.define_properties(properties)
}
pub(crate) fn new_array<'e>(env: &'e Env, len: usize) -> Result<Object<'e>> {
let len = u32::try_from(len)
.map_err(|_| Error::new(Status::InvalidArg, "array is too long for JS"))?;
Object::from_unknown(env.create_array(len)?.to_unknown())
}
#[cfg(test)]
mod tests {
use super::{eager_capacity, forbidden_key, from_utf16, MAX_EAGER_CAPACITY};
#[test]
fn forbidden_keys_are_rejected() {
assert!(forbidden_key("__proto__"));
assert!(forbidden_key("constructor"));
assert!(forbidden_key("prototype"));
assert!(!forbidden_key("proto"));
assert!(!forbidden_key("name"));
assert!(!forbidden_key(""));
}
#[test]
fn eager_capacity_passes_small_lengths_through() {
assert_eq!(eager_capacity(0), 0);
assert_eq!(eager_capacity(1), 1);
assert_eq!(eager_capacity(37), 37);
assert_eq!(
eager_capacity(MAX_EAGER_CAPACITY as u32),
MAX_EAGER_CAPACITY
);
}
#[test]
fn eager_capacity_caps_attacker_reported_lengths() {
assert_eq!(
eager_capacity(MAX_EAGER_CAPACITY as u32 + 1),
MAX_EAGER_CAPACITY
);
assert_eq!(eager_capacity(u32::MAX), MAX_EAGER_CAPACITY);
}
#[test]
fn utf16_text_is_built_at_its_final_size() {
for text in ["", "a", "José", "😀b", "\u{10FFFF}"] {
let units: Vec<u16> = text.encode_utf16().collect();
let built = from_utf16(&units).expect("well formed");
assert_eq!(built.as_str(), text);
assert_eq!(built.capacity(), text.len(), "{text}");
}
}
#[test]
fn an_unpaired_surrogate_is_refused() {
for units in [&[0xD800][..], &[0x61, 0xDC00, 0x62], &[0xD83D, 0x61]] {
assert!(from_utf16(units).is_none(), "{units:x?}");
}
}
}