From 6c94102afc70ce28eee3d17aad997a056aaf9195 Mon Sep 17 00:00:00 2001 From: Silas Bartha Date: Fri, 11 Oct 2024 19:01:52 -0400 Subject: gltf, wav, and binary codecs --- src/binary/mod.rs | 2 + src/binary/reverse_appendix.rs | 35 ++++++++++ src/codec.rs | 6 +- src/gltf/extras.rs | 91 ++++++++++++++++++++++++ src/gltf/mod.rs | 2 + src/jpeg/segment.rs | 10 +-- src/lib.rs | 12 ++++ src/lossless/lsb.rs | 16 ++--- src/wav/lsb.rs | 153 +++++++++++++++++++++++++++++++++++++++++ src/wav/mod.rs | 2 + 10 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 src/binary/mod.rs create mode 100644 src/binary/reverse_appendix.rs create mode 100644 src/gltf/extras.rs create mode 100644 src/gltf/mod.rs create mode 100644 src/wav/lsb.rs create mode 100644 src/wav/mod.rs (limited to 'src') diff --git a/src/binary/mod.rs b/src/binary/mod.rs new file mode 100644 index 0000000..66f6c30 --- /dev/null +++ b/src/binary/mod.rs @@ -0,0 +1,2 @@ +mod reverse_appendix; +pub use reverse_appendix::*; diff --git a/src/binary/reverse_appendix.rs b/src/binary/reverse_appendix.rs new file mode 100644 index 0000000..d131526 --- /dev/null +++ b/src/binary/reverse_appendix.rs @@ -0,0 +1,35 @@ +use crate::{Codec, Error}; + +/// Reverses payload binary data and writes it ass-first past the end of the original data. A +/// length marker is also prepended to the payload *before reversing* so the decoder knows how long +/// the payload is. +pub struct BinaryReverseAppendixCodec; + +impl Codec for BinaryReverseAppendixCodec { + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, crate::Error> { + let mut encoded = Vec::::new(); + encoded.extend(carrier.iter()); + let payload_len = (payload.len() as u64 + 8).to_le_bytes(); + encoded.extend(payload_len.iter().chain(payload.iter()).rev()); + Ok(encoded) + } + + fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), crate::Error> { + if encoded.len() < 8 { + return Err(Error::DataNotEncoded); + } + + let encoded_len = encoded.len(); + let payload_len = u64::from_le_bytes(encoded.iter().rev().take(8).cloned().collect::>().try_into().unwrap()) as usize; + if encoded_len < payload_len + 8 || payload_len < 8 { + return Err(Error::DataNotEncoded); + } + + let carrier_len = encoded_len - payload_len; + + let carrier = encoded[..carrier_len].to_vec(); + let payload = encoded.iter().rev().skip(8).take(payload_len - 8).cloned().collect::>(); + + Ok((carrier, payload)) + } +} diff --git a/src/codec.rs b/src/codec.rs index 11fa76e..c46860f 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -3,16 +3,16 @@ use thiserror::Error; /// Codecs enable the concealment of payload data inside the data of a carrier. pub trait Codec { /// Embeds payload data inside carrier, returning the result. - fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, CodecError>; + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, Error>; /// Extracts payload data from an encoded carrier, returning the carrier with data removed and the /// payload data. - fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), CodecError>; + fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), Error>; } /// Errors produced by a codec #[derive(Debug, Error)] -pub enum CodecError { +pub enum Error { /// Variant used when data is determined not to be encoded. Note that a codec may have no way /// of knowing this, so this may not be returned even if the data was not encoded #[error("Data was not encoded with this codec")] diff --git a/src/gltf/extras.rs b/src/gltf/extras.rs new file mode 100644 index 0000000..eade453 --- /dev/null +++ b/src/gltf/extras.rs @@ -0,0 +1,91 @@ +use std::borrow::Cow; + +use base64::Engine; +use gltf::Gltf; +use serde_json::{json, value::to_raw_value, Value}; + +use crate::{Codec, Error}; + +/// Codec for embedding data in a GLTF file "extras" entry. It uses the extras entry in the first +/// scene in the file and stores the data as base64. +#[derive(Default)] +pub struct ExtrasEntryCodec; + +impl Codec for ExtrasEntryCodec { + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, crate::Error> { + let gltf = match Gltf::from_slice(carrier) { + Ok(gltf) => gltf, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + + let mut json = gltf.document.into_json(); + let mut scene = json.scenes.remove(0); + let mut extras = match serde_json::from_str::(scene.extras.clone().unwrap_or(to_raw_value(&json!({})).unwrap()).get()) { + Ok(extras) => extras, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + match &mut extras { + Value::Object(object) => { + let base64_payload = base64::engine::general_purpose::STANDARD.encode(payload); + object.insert("occule".into(), Value::String(base64_payload)); + }, + _ => return Err(Error::DataInvalid("Carrier has extras in non-object format, not gonna mess with that.".into())) + } + let extras = match to_raw_value(&extras) { + Ok(raw) => Some(raw), + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + scene.extras = extras; + json.scenes.insert(0, scene); + let json_string = match gltf_json::serialize::to_string(&json) { + Ok(json_string) => json_string, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + + let mut glb = match gltf::binary::Glb::from_slice(carrier) { + Ok(glb) => glb, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + glb.header.length = (glb.header.length as usize - glb.json.len() + align_to_multiple_of_four(json_string.len())) as u32; + glb.json = Cow::Owned(json_string.into_bytes()); + + Ok(match glb.to_vec() { + Ok(vec) => vec, + Err(e) => return Err(Error::DependencyError(e.to_string())) + }) + } + + fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), crate::Error> { + let gltf = match Gltf::from_slice(encoded) { + Ok(gltf) => gltf, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }; + let mut json = gltf.document.into_json(); + let mut extras = match &json.scenes[0].extras { + Some(extras) => match serde_json::from_str::(extras.get()) { + Ok(Value::Object(value)) => value, + _ => return Err(Error::DataNotEncoded), + }, + None => return Err(Error::DataNotEncoded), + }; + let payload = match extras.remove("occule".into()) { + Some(Value::String(payload)) => match base64::engine::general_purpose::STANDARD.decode(payload) { + Ok(payload) => payload, + Err(e) => return Err(Error::DependencyError(e.to_string())), + }, + _ => return Err(Error::DataNotEncoded), + }; + + json.scenes[0].extras = match to_raw_value(&Value::Object(extras)) { + Ok(extras) => Some(extras), + Err(e) => return Err(Error::DependencyError(e.to_string())) + }; + + // TODO: remove payload from carrier + Ok((encoded.to_vec(), payload)) + } +} + +fn align_to_multiple_of_four(n: usize) -> usize { + (n + 3) & !3 +} diff --git a/src/gltf/mod.rs b/src/gltf/mod.rs new file mode 100644 index 0000000..42b0bae --- /dev/null +++ b/src/gltf/mod.rs @@ -0,0 +1,2 @@ +mod extras; +pub use extras::*; \ No newline at end of file diff --git a/src/jpeg/segment.rs b/src/jpeg/segment.rs index cd1a651..c0512a1 100644 --- a/src/jpeg/segment.rs +++ b/src/jpeg/segment.rs @@ -2,7 +2,7 @@ use std::{mem::size_of, usize}; use img_parts::jpeg::{markers, Jpeg, JpegSegment}; -use crate::{codec::Codec, CodecError}; +use crate::{codec::Codec, Error}; /// Codec for storing payload data in JPEG comment (COM) segments. Can store an arbitrary amount of /// data, as long as the number of comment segments does not exceed u64::MAX. @@ -13,11 +13,11 @@ pub struct JpegSegmentCodec { } impl Codec for JpegSegmentCodec { - fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, CodecError> + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, Error> { let mut jpeg = match Jpeg::from_bytes(carrier.to_vec().into()) { Ok(v) => v, - Err(e) => return Err(CodecError::DependencyError(e.to_string())) + Err(e) => return Err(Error::DependencyError(e.to_string())) }; let mut payload_bytes = payload.to_vec(); let segment_count = ((payload_bytes.len() + size_of::()) as u64).div_ceil((u16::MAX as usize - size_of::()) as u64); @@ -29,11 +29,11 @@ impl Codec for JpegSegmentCodec { Ok(jpeg.encoder().bytes().to_vec()) } - fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), CodecError> + fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), Error> { let mut jpeg = match Jpeg::from_bytes(encoded.to_vec().into()) { Ok(v) => v, - Err(e) => return Err(CodecError::DependencyError(e.to_string())) + Err(e) => return Err(Error::DependencyError(e.to_string())) }; let segment = jpeg.segments_mut().remove(self.start_index); let segment_bytes = segment.contents(); diff --git a/src/lib.rs b/src/lib.rs index 7cbae7c..6c9237f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,3 +13,15 @@ pub mod jpeg; /// Codecs for carriers in lossless image formats (PNG, WebP, etc.). #[cfg(feature = "lossless")] pub mod lossless; + +/// Codecs for carriers in gltf model format. +#[cfg(feature = "gltf")] +pub mod gltf; + +/// Codecs for binary files. +#[cfg(feature = "bin")] +pub mod binary; + +/// Codecs for wav files +#[cfg(feature = "wav")] +pub mod wav; diff --git a/src/lossless/lsb.rs b/src/lossless/lsb.rs index 2ce1456..1c9a213 100644 --- a/src/lossless/lsb.rs +++ b/src/lossless/lsb.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, io::{BufWriter, Cursor}}; use image::{DynamicImage, GenericImageView, Pixel}; -use crate::{codec::Codec, CodecError}; +use crate::{codec::Codec, Error}; /// Least-significant bit (LSB) steganography encodes data in the least-significant bits of colors /// in an image. This implementation reduces the colors in the carrier (irreversibly) in order to @@ -12,14 +12,14 @@ use crate::{codec::Codec, CodecError}; pub struct LsbCodec; impl Codec for LsbCodec { - fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, CodecError> + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, Error> { let image_format = image::guess_format(carrier).unwrap(); let mut image: DynamicImage = image::load_from_memory(carrier).unwrap(); let payload: &[u8] = payload; if image.pixels().count() < payload.len() { - return Err(CodecError::DataInvalid("Payload Too Big for Carrier".into())); + return Err(Error::DataInvalid("Payload Too Big for Carrier".into())); } let mut payload_iter = payload.iter(); @@ -43,17 +43,17 @@ impl Codec for LsbCodec { } } }, - _ => return Err(CodecError::DataInvalid("Unsupported Image Color Format".into())) + _ => return Err(Error::DataInvalid("Unsupported Image Color Format".into())) } let mut buf = BufWriter::new(Cursor::new(Vec::::new())); if let Err(e) = image.write_to(&mut buf, image_format) { - return Err(CodecError::DependencyError(e.to_string())) + return Err(Error::DependencyError(e.to_string())) } Ok(buf.into_inner().unwrap().into_inner()) } - fn decode(&self, carrier: &[u8]) -> Result<(Vec, Vec), CodecError> + fn decode(&self, carrier: &[u8]) -> Result<(Vec, Vec), Error> { let image_format = image::guess_format(carrier).unwrap(); let mut image: DynamicImage = image::load_from_memory(carrier).unwrap(); @@ -78,12 +78,12 @@ impl Codec for LsbCodec { } } }, - _ => return Err(CodecError::DataInvalid("Unsupported Image Color Format".into())) + _ => return Err(Error::DataInvalid("Unsupported Image Color Format".into())) } let mut buf = BufWriter::new(Cursor::new(Vec::::new())); if let Err(e) = image.write_to(&mut buf, image_format) { - return Err(CodecError::DependencyError(e.to_string())) + return Err(Error::DependencyError(e.to_string())) } Ok((buf.into_inner().unwrap().into_inner(), payload)) } diff --git a/src/wav/lsb.rs b/src/wav/lsb.rs new file mode 100644 index 0000000..17e6e28 --- /dev/null +++ b/src/wav/lsb.rs @@ -0,0 +1,153 @@ +use std::io::Cursor; + +use crate::{Codec, Error}; + +use hound::{self, Sample, SampleFormat, WavReader, WavSamples, WavWriter}; +use itertools::{Chunk, Itertools}; +use num_traits::{FromBytes, ToBytes}; + +/// A Least-Significant Bit (LSB) Codec for WAV files. Stores 1 bit of payload data in each sample +/// of a WAV file. Supported sample formats are 8, 16, and 32-bit PCM, and 32-bit float. +pub struct LsbCodec; + +impl Codec for LsbCodec { + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result, crate::Error> { + if let Ok(mut reader) = hound::WavReader::new(Cursor::new(carrier)) { + let mut encoded = vec![]; + { + let mut writer = WavWriter::new(Cursor::new(&mut encoded), reader.spec()).unwrap(); + match reader.spec().sample_format { + hound::SampleFormat::Float => { + encode::(payload, &mut reader, &mut writer); + } + hound::SampleFormat::Int => { + match reader.spec().bits_per_sample { + 8 => { + encode::(payload, &mut reader, &mut writer); + } + 16 => { + encode::(payload, &mut reader, &mut writer); + } + 32 => { + encode::(payload, &mut reader, &mut writer); + } + _ => return Err(Error::DataInvalid( + "Provided WAV data has an unsupported number of bits per sample." + .into(), + )), + } + } + } + writer.flush().unwrap(); + } + Ok(encoded) + } else { + Err(Error::DataInvalid( + "Could not create WAV reader from provided data".into(), + )) + } + } + + fn decode(&self, encoded: &[u8]) -> Result<(Vec, Vec), crate::Error> { + if let Ok(mut reader) = hound::WavReader::new(Cursor::new(encoded)) { + let decoded = match reader.spec().sample_format { + SampleFormat::Float => decode::(&mut reader)?, + SampleFormat::Int => match reader.spec().bits_per_sample { + 8 => decode::(&mut reader)?, + 16 => decode::(&mut reader)?, + 32 => decode::(&mut reader)?, + _ => return Err(Error::DataNotEncoded), + }, + }; + Ok((encoded.to_vec(), decoded)) + } else { + Err(Error::DataInvalid( + "Could not create WAV reader from provided data".into(), + )) + } + } +} + +fn encode( + payload: &[u8], + reader: &mut WavReader>, + writer: &mut WavWriter>>, +) where + T: Sample + ToBytes + FromBytes, +{ + let payload_len = ((payload.len() + size_of::()) as u32).to_le_bytes(); + let mut payload_iter = payload_len.iter().chain(payload.iter()); + for sample_chunk in &reader.samples::().chunks(8) { + match payload_iter.next() { + Some(payload_byte) => { + encode_byte(writer, *payload_byte, sample_chunk); + } + None => { + for sample in sample_chunk { + writer.write_sample(sample.unwrap()).unwrap(); + } + } + } + } +} + +fn encode_byte( + writer: &mut WavWriter>>, + payload_byte: u8, + sample_chunk: Chunk, T>>, +) where + T: Sample + ToBytes + FromBytes, +{ + for (i, sample) in sample_chunk.enumerate() { + let sample = sample.unwrap(); + let mut sample_bytes = sample.to_le_bytes(); + let payload_bit = (payload_byte >> (7 - i)) & 0b0000_0001; + sample_bytes[1] &= 0b1111_1110; + sample_bytes[1] |= payload_bit; + writer + .write_sample(T::from_le_bytes(&sample_bytes)) + .unwrap(); + } +} + +fn decode(reader: &mut WavReader>) -> Result, Error> +where + T: Sample + ToBytes + FromBytes, +{ + let mut decoded = vec![]; + let mut length_bytes = [0_u8; 4]; + for (i, sample_chunk) in reader + .samples::() + .take(8 * 4) + .chunks(8) + .into_iter() + .enumerate() + { + for (j, sample) in sample_chunk.enumerate() { + let sample = sample.unwrap(); + let sample_bytes = sample.to_le_bytes(); + let payload_bit = (sample_bytes[1] & 0b0000_0001) << (7 - j); + length_bytes[i] |= payload_bit; + } + } + + let payload_length = u32::from_le_bytes(length_bytes) as usize - size_of::(); + if payload_length > reader.samples::().len() { + return Err(Error::DataNotEncoded); + } + + for sample_chunk in &reader.samples::().chunks(8) { + let mut byte = 0_u8; + for (i, sample) in sample_chunk.enumerate() { + let sample = sample.unwrap(); + let sample_bytes = sample.to_le_bytes(); + let payload_bit = (sample_bytes[1] & 0b0000_0001) << (7 - i); + byte |= payload_bit; + } + decoded.push(byte); + if decoded.len() >= payload_length { + break; + } + } + Ok(decoded) +} diff --git a/src/wav/mod.rs b/src/wav/mod.rs new file mode 100644 index 0000000..a6bda54 --- /dev/null +++ b/src/wav/mod.rs @@ -0,0 +1,2 @@ +mod lsb; +pub use lsb::*; -- cgit v1.2.3