use napi::bindgen_prelude::{
Array, BigInt, Buffer, FromNapiValue, JsObjectValue, JsValue, Object, ToNapiValue, Uint8Array,
Unknown, Utf16String,
};
use napi::{sys, Env, Error, Result, Status, ValueType};
use vitaminc_aead_value::FfiValue;
use vitaminc_protected::{Controlled, Protected};
use crate::value::NapiValue;
pub(crate) const MAX_DEPTH: usize = 128;
const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1;
pub(crate) fn forbidden_key(key: &str) -> bool {
matches!(key, "__proto__" | "constructor" | "prototype")
}
fn depth_error() -> Error {
Error::new(Status::InvalidArg, "value is nested too deeply")
}
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) fn own_enumerable_keys(obj: &Object<'_>) -> Result<Vec<String>> {
let raw = obj.value();
let mut names = std::ptr::null_mut();
let status = unsafe {
sys::napi_get_all_property_names(
raw.env,
raw.value,
sys::KeyCollectionMode::own_only,
sys::KeyFilter::enumerable | sys::KeyFilter::skip_symbols,
sys::KeyConversion::numbers_to_strings,
&mut names,
)
};
if status != sys::Status::napi_ok {
return Err(Error::new(
Status::GenericFailure,
"failed to enumerate object properties",
));
}
let names = unsafe { Array::from_napi_value(raw.env, names) }?;
let mut keys = Vec::with_capacity(eager_capacity(names.len()));
for i in 0..names.len() {
let key = names.get::<String>(i)?.ok_or_else(|| {
Error::new(Status::GenericFailure, "property name vanished during read")
})?;
keys.push(key);
}
Ok(keys)
}
pub(crate) fn get_property_unknown<'e>(obj: &Object<'e>, key: &str) -> Result<Unknown<'e>> {
let raw = obj.value();
let mut js_key = std::ptr::null_mut();
let status = unsafe {
sys::napi_create_string_utf8(
raw.env,
key.as_ptr().cast(),
key.len() as isize,
&mut js_key,
)
};
if status != sys::Status::napi_ok {
return Err(Error::new(
Status::GenericFailure,
"failed to create property key",
));
}
let mut value = std::ptr::null_mut();
let status = unsafe { sys::napi_get_property(raw.env, raw.value, js_key, &mut value) };
if status != sys::Status::napi_ok {
return Err(Error::new(
Status::GenericFailure,
"failed to read object property",
));
}
unsafe { Unknown::from_napi_value(raw.env, value) }
}
fn ensure_plain_object(obj: &Object<'_>) -> Result<()> {
let proto = obj.get_prototype()?;
if proto.get_type()? == ValueType::Null {
return Ok(());
}
let env = Env::from_raw(obj.value().env);
let baseline = Object::new(&env)?;
let object_prototype = baseline.get_prototype()?;
if env.strict_equals(proto, object_prototype)? {
Ok(())
} else {
Err(Error::new(
Status::InvalidArg,
"only plain objects 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 define_own_property(
obj: &Object<'_>,
key: &str,
value: sys::napi_value,
) -> Result<()> {
let raw = obj.value();
let mut js_key = std::ptr::null_mut();
let status = unsafe {
sys::napi_create_string_utf8(
raw.env,
key.as_ptr().cast(),
key.len() as isize,
&mut js_key,
)
};
if status != sys::Status::napi_ok {
return Err(Error::new(
Status::GenericFailure,
"failed to create property key",
));
}
let descriptor = sys::napi_property_descriptor {
utf8name: std::ptr::null(),
name: js_key,
method: None,
getter: None,
setter: None,
value,
attributes: sys::PropertyAttributes::writable
| sys::PropertyAttributes::enumerable
| sys::PropertyAttributes::configurable,
data: std::ptr::null_mut(),
};
let status = unsafe { sys::napi_define_properties(raw.env, raw.value, 1, &descriptor) };
if status != sys::Status::napi_ok {
return Err(Error::new(
Status::GenericFailure,
"failed to define object property",
));
}
Ok(())
}
fn js_to_value(unknown: Unknown<'_>, depth: usize) -> Result<FfiValue> {
if depth > MAX_DEPTH {
return Err(depth_error());
}
match unknown.get_type()? {
ValueType::Null => Ok(FfiValue::Null),
ValueType::Undefined => Ok(FfiValue::Undefined),
ValueType::Boolean => Ok(FfiValue::Bool(bool::from_unknown(unknown)?)),
ValueType::Number => Ok(FfiValue::Float64(f64::from_unknown(unknown)?)),
ValueType::BigInt => {
let big = BigInt::from_unknown(unknown)?;
let (signed, i64_lossless) = big.get_i64();
if i64_lossless {
return Ok(FfiValue::Int64(signed));
}
let (_sign, unsigned, u64_lossless) = big.get_u64();
if u64_lossless {
return Ok(FfiValue::UInt64(unsigned));
}
Err(Error::new(
Status::InvalidArg,
"BigInt value does not fit in a 64-bit integer",
))
}
ValueType::String => {
let utf16 = Utf16String::from_unknown(unknown)?;
let s = String::from_utf16(&utf16).map_err(|_| {
Error::new(
Status::InvalidArg,
"string contains an unpaired surrogate and cannot be encrypted losslessly",
)
})?;
Ok(FfiValue::String(s.into()))
}
ValueType::Object => {
if unknown.is_buffer()? {
let buf = Buffer::from_unknown(unknown)?;
Ok(FfiValue::Bytes(Protected::new(buf.to_vec())))
} else if unknown.is_typedarray()? {
let arr = Uint8Array::from_unknown(unknown)?;
Ok(FfiValue::Bytes(Protected::new(arr.to_vec())))
} else if unknown.is_array()? {
let arr = Array::from_unknown(unknown)?;
let obj = Object::from_unknown(unknown)?;
let len = arr.len();
let mut items = Vec::with_capacity(eager_capacity(len));
for i in 0..len {
if !obj.has_own_property(&i.to_string())? {
return Err(Error::new(
Status::InvalidArg,
"sparse arrays are not supported",
));
}
let element: Unknown = arr.get(i)?.ok_or_else(|| {
Error::new(Status::InvalidArg, "sparse arrays are not supported")
})?;
items.push(js_to_value(element, depth + 1)?);
}
Ok(FfiValue::Array(items))
} else if unknown.is_date()? {
Err(Error::new(
Status::InvalidArg,
"Date values are not supported yet; pass a number or ISO string",
))
} else {
let obj = Object::from_unknown(unknown)?;
ensure_plain_object(&obj)?;
let keys = own_enumerable_keys(&obj)?;
let mut entries = Vec::with_capacity(keys.len());
for key in keys {
if forbidden_key(&key) {
return Err(forbidden_key_error(&key));
}
let value = get_property_unknown(&obj, &key)?;
entries.push((key, js_to_value(value, depth + 1)?));
}
Ok(FfiValue::Object(entries))
}
}
other => Err(Error::new(
Status::InvalidArg,
format!("JS type cannot be encrypted: {other}"),
)),
}
}
impl FromNapiValue for NapiValue {
unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {
let unknown = unsafe { Unknown::from_napi_value(env, napi_val) }?;
js_to_value(unknown, 0).map(NapiValue)
}
}
fn value_to_js(env: sys::napi_env, value: FfiValue) -> Result<sys::napi_value> {
match value {
FfiValue::Null => unsafe {
napi::bindgen_prelude::Null::to_napi_value(env, napi::bindgen_prelude::Null)
},
FfiValue::Undefined => unsafe { <()>::to_napi_value(env, ()) },
FfiValue::Bool(b) => unsafe { bool::to_napi_value(env, b) },
FfiValue::Float64(n) => unsafe { f64::to_napi_value(env, n) },
FfiValue::Float32(f) => unsafe { f64::to_napi_value(env, f64::from(f)) },
FfiValue::Int32(i) => unsafe { f64::to_napi_value(env, i as f64) },
FfiValue::UInt32(u) => unsafe { f64::to_napi_value(env, u as f64) },
FfiValue::Int64(i) => {
if i.unsigned_abs() <= MAX_SAFE_INTEGER as u64 {
unsafe { f64::to_napi_value(env, i as f64) }
} else {
unsafe { BigInt::to_napi_value(env, BigInt::from(i)) }
}
}
FfiValue::UInt64(u) => {
if u <= MAX_SAFE_INTEGER as u64 {
unsafe { f64::to_napi_value(env, u as f64) }
} else {
unsafe { BigInt::to_napi_value(env, BigInt::from(u)) }
}
}
FfiValue::String(s) => {
let utf8 = std::str::from_utf8(s.risky_ref())
.map_err(|_| Error::new(Status::GenericFailure, "invalid UTF-8 string value"))?;
unsafe { <&str>::to_napi_value(env, utf8) }
}
FfiValue::Bytes(b) => {
let buf = Buffer::from(b.risky_ref().to_vec());
unsafe { Buffer::to_napi_value(env, buf) }
}
FfiValue::Array(items) => {
let raw_env = Env::from_raw(env);
let mut arr = raw_env.create_array(items.len() as u32)?;
for (i, item) in items.into_iter().enumerate() {
let js = ValueHandle(item);
arr.set(i as u32, js)?;
}
unsafe { Array::to_napi_value(env, arr) }
}
FfiValue::Object(entries) => {
let raw_env = Env::from_raw(env);
let obj = Object::new(&raw_env)?;
for (key, value) in entries {
if forbidden_key(&key) {
return Err(forbidden_key_error(&key));
}
let js_value = value_to_js(env, value)?;
define_own_property(&obj, &key, js_value)?;
}
unsafe { Object::to_napi_value(env, obj) }
}
FfiValue::Passthrough(inner) => value_to_js(env, *inner),
}
}
struct ValueHandle(FfiValue);
impl ToNapiValue for ValueHandle {
unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
value_to_js(env, val.0)
}
}
impl ToNapiValue for NapiValue {
unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
value_to_js(env, val.0)
}
}
#[cfg(test)]
mod tests {
use super::{eager_capacity, forbidden_key, MAX_EAGER_CAPACITY, MAX_SAFE_INTEGER};
#[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 max_safe_integer_matches_js() {
assert_eq!(MAX_SAFE_INTEGER, 9_007_199_254_740_991);
}
#[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);
}
}