diff options
author | Silas Bartha <[email protected]> | 2024-10-11 19:01:52 -0400 |
---|---|---|
committer | Silas Bartha <[email protected]> | 2024-10-11 19:01:52 -0400 |
commit | 6c94102afc70ce28eee3d17aad997a056aaf9195 (patch) | |
tree | 11232fc59c356ce4f3b52cb140d779a3f1dc2006 /src | |
parent | 5b5f1bed5e8da9d799e5910793477ba0360d5135 (diff) |
gltf, wav, and binary codecsv0.3.0
Diffstat (limited to 'src')
-rw-r--r-- | src/binary/mod.rs | 2 | ||||
-rw-r--r-- | src/binary/reverse_appendix.rs | 35 | ||||
-rw-r--r-- | src/codec.rs | 6 | ||||
-rw-r--r-- | src/gltf/extras.rs | 91 | ||||
-rw-r--r-- | src/gltf/mod.rs | 2 | ||||
-rw-r--r-- | src/jpeg/segment.rs | 10 | ||||
-rw-r--r-- | src/lib.rs | 12 | ||||
-rw-r--r-- | src/lossless/lsb.rs | 16 | ||||
-rw-r--r-- | src/wav/lsb.rs | 153 | ||||
-rw-r--r-- | src/wav/mod.rs | 2 |
10 files changed, 313 insertions, 16 deletions
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<Vec<u8>, crate::Error> { + let mut encoded = Vec::<u8>::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<u8>, Vec<u8>), 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::<Vec<_>>().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::<Vec<_>>(); + + 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<Vec<u8>, CodecError>; + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result<Vec<u8>, 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<u8>, Vec<u8>), CodecError>; + fn decode(&self, encoded: &[u8]) -> Result<(Vec<u8>, Vec<u8>), 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<Vec<u8>, 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::<Value>(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<u8>, Vec<u8>), 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::<Value>(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<Vec<u8>, CodecError> + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result<Vec<u8>, 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::<u64>()) as u64).div_ceil((u16::MAX as usize - size_of::<u16>()) as u64); @@ -29,11 +29,11 @@ impl Codec for JpegSegmentCodec { Ok(jpeg.encoder().bytes().to_vec()) } - fn decode(&self, encoded: &[u8]) -> Result<(Vec<u8>, Vec<u8>), CodecError> + fn decode(&self, encoded: &[u8]) -> Result<(Vec<u8>, Vec<u8>), 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(); @@ -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<Vec<u8>, CodecError> + fn encode(&self, carrier: &[u8], payload: &[u8]) -> Result<Vec<u8>, 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::<u8>::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<u8>, Vec<u8>), CodecError> + fn decode(&self, carrier: &[u8]) -> Result<(Vec<u8>, Vec<u8>), 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::<u8>::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<Vec<u8>, 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::<f32, 4>(payload, &mut reader, &mut writer); + } + hound::SampleFormat::Int => { + match reader.spec().bits_per_sample { + 8 => { + encode::<i8, 1>(payload, &mut reader, &mut writer); + } + 16 => { + encode::<i16, 2>(payload, &mut reader, &mut writer); + } + 32 => { + encode::<i32, 4>(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<u8>, Vec<u8>), crate::Error> { + if let Ok(mut reader) = hound::WavReader::new(Cursor::new(encoded)) { + let decoded = match reader.spec().sample_format { + SampleFormat::Float => decode::<f32, 4>(&mut reader)?, + SampleFormat::Int => match reader.spec().bits_per_sample { + 8 => decode::<i8, 1>(&mut reader)?, + 16 => decode::<i16, 2>(&mut reader)?, + 32 => decode::<i32, 4>(&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<T, const N: usize>( + payload: &[u8], + reader: &mut WavReader<Cursor<&[u8]>>, + writer: &mut WavWriter<Cursor<&mut Vec<u8>>>, +) where + T: Sample + ToBytes<Bytes = [u8; N]> + FromBytes<Bytes = [u8; N]>, +{ + let payload_len = ((payload.len() + size_of::<u32>()) as u32).to_le_bytes(); + let mut payload_iter = payload_len.iter().chain(payload.iter()); + for sample_chunk in &reader.samples::<T>().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<T, const N: usize>( + writer: &mut WavWriter<Cursor<&mut Vec<u8>>>, + payload_byte: u8, + sample_chunk: Chunk<WavSamples<Cursor<&[u8]>, T>>, +) where + T: Sample + ToBytes<Bytes = [u8; N]> + FromBytes<Bytes = [u8; N]>, +{ + 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<T, const N: usize>(reader: &mut WavReader<Cursor<&[u8]>>) -> Result<Vec<u8>, Error> +where + T: Sample + ToBytes<Bytes = [u8; N]> + FromBytes<Bytes = [u8; N]>, +{ + let mut decoded = vec![]; + let mut length_bytes = [0_u8; 4]; + for (i, sample_chunk) in reader + .samples::<T>() + .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::<u32>(); + if payload_length > reader.samples::<T>().len() { + return Err(Error::DataNotEncoded); + } + + for sample_chunk in &reader.samples::<T>().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::*; |