summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLibravatar Silas Bartha <[email protected]>2024-10-11 19:01:52 -0400
committerLibravatar Silas Bartha <[email protected]>2024-10-11 19:01:52 -0400
commit6c94102afc70ce28eee3d17aad997a056aaf9195 (patch)
tree11232fc59c356ce4f3b52cb140d779a3f1dc2006 /src
parent5b5f1bed5e8da9d799e5910793477ba0360d5135 (diff)
gltf, wav, and binary codecsv0.3.0
Diffstat (limited to 'src')
-rw-r--r--src/binary/mod.rs2
-rw-r--r--src/binary/reverse_appendix.rs35
-rw-r--r--src/codec.rs6
-rw-r--r--src/gltf/extras.rs91
-rw-r--r--src/gltf/mod.rs2
-rw-r--r--src/jpeg/segment.rs10
-rw-r--r--src/lib.rs12
-rw-r--r--src/lossless/lsb.rs16
-rw-r--r--src/wav/lsb.rs153
-rw-r--r--src/wav/mod.rs2
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();
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<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::*;