Add Rust embed_crypto module replacing hand-rolled C crypto

ober

7e466fe910cddb6f25f9ef7cb1a460f13207bfd8

diff --git a/jerboa-native-rs/src/embed_crypto.rs b/jerboa-native-rs/src/embed_crypto.rs
new file mode 100644
index 0000000..326da3d
--- /dev/null
+++ b/jerboa-native-rs/src/embed_crypto.rs
@@ -0,0 +1,170 @@
+//! embed_crypto.rs — Drop-in Rust replacement for embed-crypto.c
+//!
+//! Provides the exact same C-ABI symbols with the exact same wire format:
+//!   nonce(12) || tag(16) || ciphertext(N)
+//!
+//! Uses ring's audited implementations instead of hand-rolled C.
+
+use ring::{aead, pbkdf2, rand};
+use crate::panic::ffi_wrap;
+use std::num::NonZeroU32;
+
+const NONCE_SIZE: usize = 12;
+const TAG_SIZE: usize = 16;
+const OVERHEAD: usize = NONCE_SIZE + TAG_SIZE; // 28
+
+/// PBKDF2-HMAC-SHA256 key derivation.
+/// Exact same signature as embed-crypto.h.
+#[no_mangle]
+pub extern "C" fn embed_pbkdf2_sha256(
+    password: *const u8,
+    password_len: usize,
+    salt: *const u8,
+    salt_len: usize,
+    iterations: u32,
+    out: *mut u8,
+    out_len: usize,
+) {
+    if password.is_null() || salt.is_null() || out.is_null() || out_len == 0 || iterations == 0 {
+        return;
+    }
+    let pw = unsafe { std::slice::from_raw_parts(password, password_len) };
+    let s = unsafe { std::slice::from_raw_parts(salt, salt_len) };
+    let output = unsafe { std::slice::from_raw_parts_mut(out, out_len) };
+    if let Some(iters) = NonZeroU32::new(iterations) {
+        pbkdf2::derive(pbkdf2::PBKDF2_HMAC_SHA256, iters, s, pw, output);
+    }
+}
+
+/// Encrypt plaintext with ChaCha20-Poly1305 AEAD.
+/// Wire format: out = nonce(12) || tag(16) || ciphertext(N)
+/// Returns total output length (plaintext_len + 28).
+#[no_mangle]
+pub extern "C" fn embed_encrypt(
+    key: *const u8,
+    nonce: *const u8,
+    plaintext: *const u8,
+    plaintext_len: usize,
+    out: *mut u8,
+) -> usize {
+    if key.is_null() || nonce.is_null() || out.is_null() {
+        return 0;
+    }
+
+    let k = unsafe { std::slice::from_raw_parts(key, 32) };
+    let n = unsafe { std::slice::from_raw_parts(nonce, NONCE_SIZE) };
+    let pt = if plaintext_len == 0 || plaintext.is_null() {
+        &[]
+    } else {
+        unsafe { std::slice::from_raw_parts(plaintext, plaintext_len) }
+    };
+    let output = unsafe { std::slice::from_raw_parts_mut(out, plaintext_len + OVERHEAD) };
+
+    // 1. Copy nonce to out[0..12]
+    output[..NONCE_SIZE].copy_from_slice(n);
+
+    // 2. Copy plaintext to out[28..28+pt_len] (after nonce+tag slot)
+    output[OVERHEAD..OVERHEAD + plaintext_len].copy_from_slice(pt);
+
+    // 3. Seal in place, get separate tag
+    let unbound_key = match aead::UnboundKey::new(&aead::CHACHA20_POLY1305, k) {
+        Ok(uk) => uk,
+        Err(_) => return 0,
+    };
+    let sealing_key = aead::LessSafeKey::new(unbound_key);
+    let nonce_val = match aead::Nonce::try_assume_unique_for_key(n) {
+        Ok(nv) => nv,
+        Err(_) => return 0,
+    };
+
+    match sealing_key.seal_in_place_separate_tag(
+        nonce_val,
+        aead::Aad::empty(),
+        &mut output[OVERHEAD..OVERHEAD + plaintext_len],
+    ) {
+        Ok(tag) => {
+            // 4. Copy tag to out[12..28]
+            output[NONCE_SIZE..OVERHEAD].copy_from_slice(tag.as_ref());
+            plaintext_len + OVERHEAD
+        }
+        Err(_) => 0,
+    }
+}
+
+/// Decrypt and verify ChaCha20-Poly1305 AEAD ciphertext.
+/// Input: nonce(12) || tag(16) || ciphertext(N)
+/// Returns 0 on success, -1 on failure.
+#[no_mangle]
+pub extern "C" fn embed_decrypt(
+    key: *const u8,
+    input: *const u8,
+    input_len: usize,
+    out: *mut u8,
+    out_len: *mut usize,
+) -> i32 {
+    ffi_wrap(|| {
+        if key.is_null() || input.is_null() || out.is_null() || out_len.is_null() {
+            return -1;
+        }
+        if input_len < OVERHEAD {
+            return -1;
+        }
+
+        let k = unsafe { std::slice::from_raw_parts(key, 32) };
+        let inp = unsafe { std::slice::from_raw_parts(input, input_len) };
+        let ct_len = input_len - OVERHEAD;
+
+        // Parse wire format: nonce(12) || tag(16) || ciphertext(ct_len)
+        let nonce_bytes = &inp[..NONCE_SIZE];
+        let tag_bytes = &inp[NONCE_SIZE..OVERHEAD];
+        let ct_bytes = &inp[OVERHEAD..];
+
+        // ring's open_in_place expects: ciphertext || tag (appended)
+        // We need at least ct_len + TAG_SIZE bytes in the output buffer
+        let work_buf = unsafe { std::slice::from_raw_parts_mut(out, ct_len + TAG_SIZE) };
+        work_buf[..ct_len].copy_from_slice(ct_bytes);
+        work_buf[ct_len..ct_len + TAG_SIZE].copy_from_slice(tag_bytes);
+
+        let unbound_key = match aead::UnboundKey::new(&aead::CHACHA20_POLY1305, k) {
+            Ok(uk) => uk,
+            Err(_) => return -1,
+        };
+        let opening_key = aead::LessSafeKey::new(unbound_key);
+        let nonce_val = match aead::Nonce::try_assume_unique_for_key(nonce_bytes) {
+            Ok(nv) => nv,
+            Err(_) => return -1,
+        };
+
+        match opening_key.open_in_place(
+            nonce_val,
+            aead::Aad::empty(),
+            &mut work_buf[..ct_len + TAG_SIZE],
+        ) {
+            Ok(plaintext) => {
+                unsafe { *out_len = plaintext.len(); }
+                0
+            }
+            Err(_) => -1,
+        }
+    })
+}
+
+/// Fill buffer with cryptographically secure random bytes.
+/// Returns 0 on success, -1 on failure.
+#[no_mangle]
+pub extern "C" fn embed_random_bytes(buf: *mut u8, len: usize) -> i32 {
+    ffi_wrap(|| {
+        if buf.is_null() {
+            return -1;
+        }
+        if len == 0 {
+            return 0;
+        }
+        let rng = rand::SystemRandom::new();
+        let output = unsafe { std::slice::from_raw_parts_mut(buf, len) };
+        match rand::SecureRandom::fill(&rng, output) {
+            Ok(()) => 0,
+            Err(_) => -1,
+        }
+    })
+}
diff --git a/jerboa-native-rs/src/lib.rs b/jerboa-native-rs/src/lib.rs
index e240255..3108588 100644
--- a/jerboa-native-rs/src/lib.rs
+++ b/jerboa-native-rs/src/lib.rs
@@ -1,5 +1,6 @@
 mod panic;
 mod crypto;
+mod embed_crypto;
 mod compress;
 mod regex_native;
 mod secure_mem;
@@ -15,4 +16,5 @@ mod seccomp;
 mod integrity;
 mod x25519;
 mod process_ctl;
+#[cfg(feature = "duckdb")]
 mod duckdb_native;