From a1c9d567b105923fc003a48bfe3ffa88d6400fb0 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sun, 12 Apr 2026 10:58:04 -0400 Subject: [PATCH] feat: add embed_image_secret to WASM crate Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/idfoto-wasm/Cargo.toml | 1 + crates/idfoto-wasm/src/lib.rs | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/crates/idfoto-wasm/Cargo.toml b/crates/idfoto-wasm/Cargo.toml index efd3f53..16c1e24 100644 --- a/crates/idfoto-wasm/Cargo.toml +++ b/crates/idfoto-wasm/Cargo.toml @@ -19,3 +19,4 @@ getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] wasm-bindgen-test = "0.3" +image = { version = "0.25", default-features = false, features = ["jpeg"] } diff --git a/crates/idfoto-wasm/src/lib.rs b/crates/idfoto-wasm/src/lib.rs index efd5f07..0f82f8a 100644 --- a/crates/idfoto-wasm/src/lib.rs +++ b/crates/idfoto-wasm/src/lib.rs @@ -97,6 +97,16 @@ pub fn extract_image_secret(jpeg_bytes: &[u8]) -> Result, JsValue> { Ok(secret.to_vec()) } +/// Embed a 256-bit secret into a carrier JPEG image. +#[wasm_bindgen] +pub fn embed_image_secret(carrier_jpeg: &[u8], secret: &[u8]) -> Result, JsValue> { + let secret: [u8; 32] = secret + .try_into() + .map_err(|_| JsValue::from_str("secret must be exactly 32 bytes"))?; + idfoto_core::imgsecret::embed(carrier_jpeg, &secret) + .map_err(|e| JsValue::from_str(&e.to_string())) +} + /// Encrypt an [`Entry`] (given as a JSON string) under the master key. /// /// The `entry_json` must deserialize into an [`Entry`] struct. Returns the @@ -315,6 +325,32 @@ mod tests { assert_eq!(decrypted, b"hello wasm"); } + #[test] + fn embed_then_extract_round_trip() { + use image::codecs::jpeg::JpegEncoder; + use image::{ImageBuffer, ImageEncoder, Rgb}; + + let img = ImageBuffer::from_fn(400, 300, |x, y| { + Rgb([ + ((x * 7 + y * 13) % 256) as u8, + ((x * 11 + y * 3) % 256) as u8, + ((x * 5 + y * 17) % 256) as u8, + ]) + }); + let mut jpeg_buf = Vec::new(); + let encoder = JpegEncoder::new_with_quality(&mut jpeg_buf, 92); + encoder.write_image(img.as_raw(), 400, 300, image::ExtendedColorType::Rgb8).unwrap(); + + let secret = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, + 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, + 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1Cu8]; + + let stego = embed_image_secret(&jpeg_buf, &secret).unwrap(); + let extracted = extract_image_secret(&stego).unwrap(); + assert_eq!(extracted, secret); + } + #[test] fn encrypt_entry_decrypt_entry_round_trip() { let key = [0xABu8; 32];