Skip to main content

px_native/cipher/
splice.rs

1//! `vQ(salt, payload, offsets)` — interleave salt characters into the
2//! base64 payload at byte positions produced by `vN`. Final step of
3//! the eT15wiaE sensor encryption stack.
4//!
5//! JS reference (un-flattened from the switch/with/case VM at line
6//! 6065 of the captured init.js):
7//!
8//! ```text
9//! function vQ(salt, payload, offsets) {
10//!   const bf = salt.split('');
11//!   let bd = '', be = 0;
12//!   for (let bg = 0; bg < salt.length; bg++) {
13//!     bd += payload.substring(be, offsets[bg] - bg - 1) + bf[bg];
14//!     be = offsets[bg] - bg - 1;
15//!   }
16//!   bd += payload.substring(be);
17//!   return bd;
18//! }
19//! ```
20
21use px_errors::AppError;
22
23/// Returns the spliced payload. Matches the JS `String.prototype.substring`
24/// semantics that the reference implementation relies on: arguments
25/// less than 0 become 0, greater than `len` become `len`, and if
26/// `start > end` they are swapped before slicing.
27pub fn v_q(salt: &[u8], payload: &[u8], offsets: &[i64]) -> Result<Vec<u8>, AppError> {
28    if offsets.len() < salt.len() {
29        return Err(AppError::InternalError(format!(
30            "vQ: need {} offsets for salt, got {}",
31            salt.len(),
32            offsets.len()
33        )));
34    }
35    let mut bd: Vec<u8> = Vec::with_capacity(payload.len() + salt.len());
36    let mut be: i64 = 0;
37    let plen = payload.len();
38    for (bg, salt_byte) in salt.iter().enumerate() {
39        let cut = offsets[bg] - (bg as i64) - 1;
40        bd.extend_from_slice(substring(payload, be, cut, plen));
41        bd.push(*salt_byte);
42        be = cut;
43    }
44    let plen_i = plen as i64;
45    bd.extend_from_slice(substring(payload, be, plen_i, plen));
46    Ok(bd)
47}
48
49/// JS `String.prototype.substring(start, end)` over a byte slice.
50fn substring(buf: &[u8], start: i64, end: i64, len: usize) -> &[u8] {
51    let len_i = len as i64;
52    let mut s = start.clamp(0, len_i);
53    let mut e = end.clamp(0, len_i);
54    if s > e {
55        std::mem::swap(&mut s, &mut e);
56    }
57    &buf[s as usize..e as usize]
58}
59
60#[cfg(test)]
61#[allow(clippy::expect_used)]
62mod tests {
63    use super::*;
64    use crate::cipher::offsets::v_n;
65
66    #[test]
67    fn round_trip_against_offsets_prng() {
68        let salt = b"tag";
69        let payload = b"AAAAAAAAAA";
70        let offsets = v_n(payload.len(), payload.len(), b"seed");
71        let out = v_q(salt, payload, &offsets).expect("splice");
72        assert_eq!(out.len(), payload.len() + salt.len());
73    }
74
75    #[test]
76    fn empty_salt_returns_payload() {
77        let out = v_q(b"", b"abcdef", &[]).expect("splice");
78        assert_eq!(out, b"abcdef");
79    }
80}