1use napi::bindgen_prelude::{
36 Array, Buffer, BufferSlice, FromNapiValue, JsObjectValue, JsValue, Object, ToNapiValue,
37 TypeName, Unknown,
38};
39use napi::{sys, Env, Error, Result, Status, ValueType};
40use vitaminc_aead::{CipherText, Frame, PassthroughFrame};
41use vitaminc_protected::Controlled;
42
43use crate::js::{
44 define_own_properties, depth_error, eager_capacity, forbidden_key, get_own,
45 get_property_unknown, new_array, own_enumerable_keys, own_property, MAX_DEPTH,
46};
47use crate::js_builder::JsBuilder;
48use crate::js_ref::{frame_error, JsRef};
49
50const T_CIPHERTEXT: &str = "ct";
51const T_SEQ: &str = "seq";
52const T_EMPTY_SEQ: &str = "eseq";
53const T_MAP: &str = "map";
54const T_EMPTY_MAP: &str = "emap";
55const T_PASSTHROUGH: &str = "pt";
56
57pub struct JsCipherText<Leaf, P>(pub CipherText<Leaf, P>);
66
67pub trait NapiPassthrough: Sized {
73 fn to_js(self, env: &Env) -> Result<Unknown<'_>>;
77 fn from_js(value: Unknown<'_>, depth: usize) -> Result<Self>;
80 fn exceeds_depth(&self, depth: usize) -> bool;
82 fn drop_flat(self);
85}
86
87impl NapiPassthrough for () {
90 fn to_js(self, env: &Env) -> Result<Unknown<'_>> {
91 ().into_unknown(env)
92 }
93
94 fn from_js(value: Unknown<'_>, depth: usize) -> Result<Self> {
95 if depth > MAX_DEPTH {
96 return Err(depth_error());
97 }
98 if value.get_type()? == ValueType::Undefined {
100 Ok(())
101 } else {
102 Err(shape_error())
103 }
104 }
105
106 fn exceeds_depth(&self, depth: usize) -> bool {
107 depth > MAX_DEPTH
108 }
109
110 fn drop_flat(self) {}
111}
112
113impl NapiPassthrough for PassthroughFrame {
117 fn to_js(self, env: &Env) -> Result<Unknown<'_>> {
118 let frame = Frame::parse(&self.0).map_err(|_| shape_error())?;
120 JsBuilder::build(env, frame)
121 }
122
123 fn from_js(value: Unknown<'_>, depth: usize) -> Result<Self> {
124 let frame = vitaminc_aead::to_frame(JsRef::at_depth(value, depth)).map_err(frame_error)?;
125 Ok(PassthroughFrame(frame.risky_ref().clone()))
126 }
127
128 fn exceeds_depth(&self, depth: usize) -> bool {
132 Frame::parse_nested(&self.0, depth).is_err()
133 }
134
135 fn drop_flat(self) {}
137}
138
139fn exceeds_depth<Leaf, P: NapiPassthrough>(node: &CipherText<Leaf, P>) -> bool {
142 let mut stack = vec![(node, 0)];
143 while let Some((node, depth)) = stack.pop() {
144 if depth > MAX_DEPTH {
145 return true;
146 }
147 match node {
148 CipherText::Sequence(items) => stack.extend(items.iter().map(|item| (item, depth + 1))),
149 CipherText::Map(entries) => {
150 stack.extend(entries.iter().map(|(_, value)| (value, depth + 1)))
151 }
152 CipherText::Passthrough(p) if p.exceeds_depth(depth + 1) => return true,
153 _ => {}
154 }
155 }
156 false
157}
158
159fn drop_flat<Leaf, P: NapiPassthrough>(node: CipherText<Leaf, P>) {
162 let mut stack = vec![node];
163 while let Some(node) = stack.pop() {
164 match node {
165 CipherText::Sequence(items) => stack.extend(items),
166 CipherText::Map(entries) => stack.extend(entries.into_iter().map(|(_, value)| value)),
167 CipherText::Passthrough(p) => p.drop_flat(),
168 _ => {}
169 }
170 }
171}
172
173fn ciphertext_depth_error() -> Error {
174 Error::new(Status::InvalidArg, "ciphertext is nested too deeply")
175}
176
177fn shape_error() -> Error {
178 Error::new(Status::InvalidArg, "malformed ciphertext value")
179}
180
181fn node_to_js<Leaf, P>(env: &Env, node: CipherText<Leaf, P>) -> Result<Object<'_>>
184where
185 Leaf: AsRef<[u8]>,
186 P: NapiPassthrough,
187{
188 let leaf = |tag: &'static str, leaf: Leaf| -> Result<(&'static str, Unknown<'_>)> {
189 Ok((tag, Buffer::from(leaf.as_ref().to_vec()).into_unknown(env)?))
190 };
191 let (tag, value) = match node {
192 CipherText::Single(l) => leaf(T_CIPHERTEXT, l)?,
193 CipherText::EmptySequence(l) => leaf(T_EMPTY_SEQ, l)?,
194 CipherText::EmptyMap(l) => leaf(T_EMPTY_MAP, l)?,
195 CipherText::Sequence(items) => {
196 let mut arr = new_array(env, items.len())?;
197 let mut elements = Vec::with_capacity(items.len());
198 for (i, item) in items.into_iter().enumerate() {
199 let node = node_to_js(env, item)?;
200 elements.push(own_property(env, &i.to_string(), &node)?);
201 }
202 define_own_properties(&mut arr, &elements)?;
203 (T_SEQ, arr.into_unknown(env)?)
204 }
205 CipherText::Map(entries) => {
206 let mut map = Object::new(env)?;
207 let mut properties = Vec::with_capacity(entries.len());
208 for (key, value) in entries {
209 if forbidden_key(&key) {
213 return Err(Error::new(
214 Status::InvalidArg,
215 format!("ciphertext map key is not allowed: {key}"),
216 ));
217 }
218 let node = node_to_js(env, value)?;
219 properties.push(own_property(env, &key, &node)?);
220 }
221 define_own_properties(&mut map, &properties)?;
222 (T_MAP, map.into_unknown(env)?)
223 }
224 CipherText::Passthrough(p) => (T_PASSTHROUGH, p.to_js(env)?),
225 };
226 let mut obj = Object::new(env)?;
229 let tag = tag.into_unknown(env)?;
230 define_own_properties(
231 &mut obj,
232 &[
233 own_property(env, T_KEY, &tag)?,
234 own_property(env, V_KEY, &value)?,
235 ],
236 )?;
237 Ok(obj)
238}
239
240const T_KEY: &str = "t";
241const V_KEY: &str = "v";
242
243impl<Leaf, P> TypeName for JsCipherText<Leaf, P> {
246 fn type_name() -> &'static str {
247 "CipherText"
248 }
249
250 fn value_type() -> ValueType {
251 ValueType::Object
252 }
253}
254
255impl<Leaf, P> ToNapiValue for JsCipherText<Leaf, P>
256where
257 Leaf: AsRef<[u8]>,
258 P: NapiPassthrough,
259{
260 unsafe fn to_napi_value(env: sys::napi_env, val: Self) -> Result<sys::napi_value> {
261 if exceeds_depth(&val.0) {
262 drop_flat(val.0);
263 return Err(ciphertext_depth_error());
264 }
265 Ok(node_to_js(&Env::from_raw(env), val.0)?.raw())
266 }
267}
268
269fn node_object(value: Unknown<'_>) -> Result<Object<'_>> {
274 if value.get_type()? != ValueType::Object {
275 return Err(shape_error());
276 }
277 Object::from_unknown(value)
278}
279
280fn own_field<'e>(obj: &Object<'e>, key: &str) -> Result<Unknown<'e>> {
285 if !obj.has_own_property(key)? {
286 return Err(shape_error());
287 }
288 get_property_unknown(obj, key)
289}
290
291fn node_from_js<Leaf, P>(obj: &Object<'_>, depth: usize) -> Result<CipherText<Leaf, P>>
292where
293 Leaf: From<Vec<u8>>,
294 P: NapiPassthrough,
295{
296 if depth > MAX_DEPTH {
297 return Err(ciphertext_depth_error());
298 }
299 let t = String::from_unknown(own_field(obj, T_KEY)?)?;
300 match t.as_str() {
301 T_CIPHERTEXT | T_EMPTY_SEQ | T_EMPTY_MAP => {
302 let buf = BufferSlice::from_unknown(own_field(obj, V_KEY)?)?;
306 let leaf = Leaf::from(buf.to_vec());
307 Ok(match t.as_str() {
308 T_CIPHERTEXT => CipherText::Single(leaf),
309 T_EMPTY_SEQ => CipherText::EmptySequence(leaf),
310 _ => CipherText::EmptyMap(leaf),
311 })
312 }
313 T_SEQ => {
314 let arr = Array::from_unknown(own_field(obj, V_KEY)?)?;
315 let elements = Object::from_unknown(arr.to_unknown())?;
317 let len = arr.len();
318 let mut items = Vec::with_capacity(eager_capacity(len));
323 for i in 0..len {
324 if !elements.has_own_property(&i.to_string())? {
328 return Err(shape_error());
329 }
330 let element = arr.get::<Unknown>(i)?.ok_or_else(shape_error)?;
331 items.push(node_from_js(&node_object(element)?, depth + 1)?);
332 }
333 Ok(CipherText::Sequence(items))
334 }
335 T_MAP => {
336 let map = node_object(own_field(obj, V_KEY)?)?;
337 let keys = own_enumerable_keys(&map)?;
342 let mut entries = Vec::with_capacity(keys.len());
343 for key in keys {
344 if forbidden_key(&key.name) {
345 return Err(Error::new(
346 Status::InvalidArg,
347 format!("ciphertext map key is not allowed: {}", *key.name),
348 ));
349 }
350 let value = node_object(get_own(&map, &key)?)?;
351 entries.push((key.name.to_string(), node_from_js(&value, depth + 1)?));
352 }
353 Ok(CipherText::Map(entries))
354 }
355 T_PASSTHROUGH => {
356 let env = Env::from_raw(obj.value().env);
363 let value = if obj.has_own_property(V_KEY)? {
364 own_field(obj, V_KEY)?
365 } else {
366 ().into_unknown(&env)?
367 };
368 Ok(CipherText::Passthrough(P::from_js(value, depth + 1)?))
369 }
370 _ => Err(shape_error()),
371 }
372}
373
374impl<Leaf, P> FromNapiValue for JsCipherText<Leaf, P>
375where
376 Leaf: From<Vec<u8>>,
377 P: NapiPassthrough,
378{
379 unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> Result<Self> {
380 let value = unsafe { Unknown::from_napi_value(env, napi_val) }?;
386 let obj = node_object(value)?;
387 Ok(JsCipherText(node_from_js(&obj, 0)?))
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::{drop_flat, exceeds_depth, CipherText, NapiPassthrough, MAX_DEPTH};
394 use vitaminc_aead::{to_frame, PassthroughFrame};
395 use vitaminc_protected::Controlled;
396
397 type Node<P> = CipherText<Vec<u8>, P>;
398
399 #[test]
400 fn a_node_is_a_javascript_object_named_ciphertext() {
401 use napi::bindgen_prelude::{TypeName, ValueType};
402 type Js = super::JsCipherText<Vec<u8>, PassthroughFrame>;
403 assert_eq!(Js::type_name(), "CipherText");
404 assert_eq!(Js::value_type(), ValueType::Object);
405 }
406
407 fn nested<P>(levels: usize, wrap: fn(Node<P>) -> Node<P>, leaf: Node<P>) -> Node<P> {
408 (0..levels).fold(leaf, |node, _| wrap(node))
409 }
410
411 fn seq<P>(node: Node<P>) -> Node<P> {
412 CipherText::Sequence(vec![CipherText::Single(vec![1]), node])
413 }
414
415 fn map<P>(node: Node<P>) -> Node<P> {
416 CipherText::Map(vec![
417 ("a".into(), CipherText::EmptySequence(vec![1])),
418 ("k".into(), node),
419 ])
420 }
421
422 fn payload(levels: usize) -> Node<PassthroughFrame> {
424 struct Arrays(usize);
425 impl vitaminc_aead::Encrypt for Arrays {
426 fn visit<S: vitaminc_aead::Sink>(self, sink: S) -> Result<S::Ok, S::Error> {
427 use vitaminc_aead::SeqSink;
428 if self.0 == 0 {
429 return sink.scalar(None, vitaminc_aead::Physical::Bool(true));
430 }
431 let mut seq = sink.seq(Some(1))?;
432 Arrays(self.0 - 1).visit(seq.element()?)?;
433 seq.end()
434 }
435 }
436 let frame = to_frame(Arrays(levels)).expect("within the frame's own bound");
437 CipherText::Passthrough(PassthroughFrame(frame.risky_ref().clone()))
438 }
439
440 #[test]
441 fn depth_is_measured_to_the_limit_for_every_container() {
442 for wrap in [seq::<()>, map::<()>] {
443 let leaf = || CipherText::EmptySequence(vec![1]);
444 assert!(!exceeds_depth(&nested(MAX_DEPTH, wrap, leaf())));
445 assert!(exceeds_depth(&nested(MAX_DEPTH + 1, wrap, leaf())));
446 }
447 }
448
449 #[test]
450 fn a_payload_continues_the_count() {
451 assert!(!exceeds_depth(&nested(
453 MAX_DEPTH - 1,
454 seq,
455 CipherText::Passthrough(())
456 )));
457 assert!(exceeds_depth(&nested(
458 MAX_DEPTH,
459 seq,
460 CipherText::Passthrough(())
461 )));
462 assert!(!().exceeds_depth(MAX_DEPTH));
463 assert!(().exceeds_depth(MAX_DEPTH + 1));
464 assert!(!exceeds_depth(&nested(60, seq, payload(67))));
466 assert!(exceeds_depth(&nested(60, seq, payload(68))));
467 assert!(PassthroughFrame(vec![0xF0, 1, 0, 0, 0]).exceeds_depth(0));
469 }
470
471 #[test]
472 fn trees_too_deep_for_the_stack_are_measured_and_dropped_flat() {
473 for wrap in [seq::<PassthroughFrame>, map::<PassthroughFrame>] {
474 let deep = nested(1_000_000, wrap, CipherText::Single(vec![1]));
475 assert!(exceeds_depth(&deep));
476 drop_flat(deep);
477 }
478 drop_flat(nested(1_000_000, seq, CipherText::Passthrough(())));
479 }
480}